diff --git a/src/auto_task/APP_GENERATOR_PROMPT.md b/src/auto_task/APP_GENERATOR_PROMPT.md
index df9086579..50c483b6b 100644
--- a/src/auto_task/APP_GENERATOR_PROMPT.md
+++ b/src/auto_task/APP_GENERATOR_PROMPT.md
@@ -490,8 +490,10 @@ Every HTML page MUST include proper SEO meta tags:
{Page Title} - {App Name}
+
-
+
+
```
diff --git a/src/auto_task/app_generator.rs b/src/auto_task/app_generator.rs
index 5d5804694..60c9071ee 100644
--- a/src/auto_task/app_generator.rs
+++ b/src/auto_task/app_generator.rs
@@ -296,11 +296,6 @@ impl AppGenerator {
// Insert "Analyzing Request" at the beginning of sections
new_manifest.sections.insert(0, analyzing_section);
- // Add Deployment section at the end
- let deploy_section = ManifestSection::new("Deployment", SectionType::Deployment)
- .with_steps(1);
- new_manifest.add_section(deploy_section);
-
// Recalculate all global step offsets after insertion
new_manifest.recalculate_global_steps();
new_manifest.completed_steps = 1; // Analyzing is done
@@ -538,6 +533,17 @@ impl AppGenerator {
child.current_step = child.item_groups.iter()
.filter(|g| g.status == crate::auto_task::ItemStatus::Completed)
.count() as u32;
+
+ // Check if all item_groups in child are completed, then mark child as completed
+ let all_groups_completed = child.item_groups.iter()
+ .all(|g| g.status == crate::auto_task::ItemStatus::Completed);
+ if all_groups_completed && !child.item_groups.is_empty() {
+ child.status = SectionStatus::Completed;
+ child.completed_at = Some(Utc::now());
+ if let Some(started) = child.started_at {
+ child.duration_seconds = Some((Utc::now() - started).num_seconds() as u64);
+ }
+ }
break;
}
}
@@ -545,6 +551,17 @@ impl AppGenerator {
section.current_step = section.children.iter()
.map(|c| c.current_step)
.sum();
+
+ // Check if all children in section are completed, then mark section as completed
+ let all_children_completed = section.children.iter()
+ .all(|c| c.status == SectionStatus::Completed);
+ if all_children_completed && !section.children.is_empty() {
+ section.status = SectionStatus::Completed;
+ section.completed_at = Some(Utc::now());
+ if let Some(started) = section.started_at {
+ section.duration_seconds = Some((Utc::now() - started).num_seconds() as u64);
+ }
+ }
break;
}
}
@@ -597,11 +614,6 @@ impl AppGenerator {
.with_steps(1);
manifest.add_section(tools_section);
- // Section 5: Deployment
- let deploy_section = ManifestSection::new("Deployment", SectionType::Deployment)
- .with_steps(1);
- manifest.add_section(deploy_section);
-
manifest.status = ManifestStatus::Running;
manifest.add_terminal_line(&format!("Analyzing: {}", intent), TerminalLineType::Info);
manifest.add_terminal_line("Sending request to AI...", TerminalLineType::Progress);
diff --git a/src/designer/mod.rs b/src/designer/mod.rs
index 456529421..13b2cabad 100644
--- a/src/designer/mod.rs
+++ b/src/designer/mod.rs
@@ -1143,6 +1143,9 @@ Guidelines:
- API endpoints follow pattern: /api/db/{{table_name}}
- Forms should use hx-post for submissions
- Lists should use hx-get with pagination
+- IMPORTANT: Use RELATIVE paths for app assets (styles.css, app.js, NOT /static/styles.css)
+- For HTMX, use CDN:
+- CSS link should be:
Respond with valid JSON only."#,
request.app_name,
@@ -1277,36 +1280,35 @@ async fn apply_file_change(
app_name: &str,
file_name: &str,
content: &str,
- session: &crate::shared::models::UserSession,
+ _session: &crate::shared::models::UserSession,
) -> Result<(), Box> {
- use crate::shared::models::schema::bots::dsl::*;
-
- let mut conn = state.conn.get()?;
- let bot_name_val: String = bots
- .filter(id.eq(session.bot_id))
- .select(name)
- .first(&mut conn)?;
+ // Use bucket_name from state (like app_generator) - e.g., "default.gbai"
+ let bucket_name = state.bucket_name.clone();
+ let sanitized_name = bucket_name.trim_end_matches(".gbai").to_string();
+ // Always write to local disk first (primary storage, like import templates)
+ // Match app_server filesystem fallback path: {site_path}/{bot}.gbai/{bot}.gbapp/{app_name}/{file}
let site_path = state
.config
.as_ref()
.map(|c| c.site_path.clone())
.unwrap_or_else(|| "./botserver-stack/sites".to_string());
- let local_path = format!("{site_path}/{app_name}/{file_name}");
+ let local_path = format!("{site_path}/{}.gbai/{}.gbapp/{app_name}/{file_name}", sanitized_name, sanitized_name);
if let Some(parent) = std::path::Path::new(&local_path).parent() {
std::fs::create_dir_all(parent)?;
}
std::fs::write(&local_path, content)?;
log::info!("Designer updated local file: {local_path}");
+ // Also sync to S3/MinIO if available (with bucket creation retry like app_generator)
if let Some(ref s3_client) = state.drive {
use aws_sdk_s3::primitives::ByteStream;
- let bucket_name = format!("{}.gbai", bot_name_val.to_lowercase());
- // Use same path pattern as app_generator: bucket.gbapp/app_name/file
- let sanitized_bucket = bucket_name.trim_end_matches(".gbai");
- let file_path = format!("{}.gbapp/{app_name}/{file_name}", sanitized_bucket);
+ // Use same path pattern as app_server/app_generator: {sanitized_name}.gbapp/{app_name}/{file}
+ let file_path = format!("{}.gbapp/{}/{}", sanitized_name, app_name, file_name);
+
+ log::info!("Designer syncing to S3: bucket={}, key={}", bucket_name, file_path);
match s3_client
.put_object()
@@ -1321,7 +1323,47 @@ async fn apply_file_change(
log::info!("Designer synced to S3: s3://{bucket_name}/{file_path}");
}
Err(e) => {
- log::warn!("Designer failed to sync to S3 (local write succeeded): {e}");
+ // Check if bucket doesn't exist and try to create it (like app_generator)
+ let err_str = format!("{:?}", e);
+ if err_str.contains("NoSuchBucket") || err_str.contains("NotFound") {
+ log::warn!("Bucket {} not found, attempting to create...", bucket_name);
+
+ // Try to create the bucket
+ match s3_client.create_bucket().bucket(&bucket_name).send().await {
+ Ok(_) => {
+ log::info!("Created bucket: {}", bucket_name);
+ }
+ Err(create_err) => {
+ let create_err_str = format!("{:?}", create_err);
+ // Ignore if bucket already exists (race condition)
+ if !create_err_str.contains("BucketAlreadyExists")
+ && !create_err_str.contains("BucketAlreadyOwnedByYou") {
+ log::warn!("Failed to create bucket {}: {}", bucket_name, create_err);
+ }
+ }
+ }
+
+ // Retry the write after bucket creation
+ match s3_client
+ .put_object()
+ .bucket(&bucket_name)
+ .key(&file_path)
+ .body(ByteStream::from(content.as_bytes().to_vec()))
+ .content_type(get_content_type(file_name))
+ .send()
+ .await
+ {
+ Ok(_) => {
+ log::info!("Designer synced to S3 after bucket creation: s3://{bucket_name}/{file_path}");
+ }
+ Err(retry_err) => {
+ log::warn!("Designer S3 retry failed (local write succeeded): {retry_err}");
+ }
+ }
+ } else {
+ // S3 sync is optional - local write already succeeded
+ log::warn!("Designer S3 sync failed (local write succeeded): {e}");
+ }
}
}
}
diff --git a/src/security/headers.rs b/src/security/headers.rs
index 8a9512cc1..810fd5c96 100644
--- a/src/security/headers.rs
+++ b/src/security/headers.rs
@@ -24,10 +24,10 @@ impl Default for SecurityHeadersConfig {
Self {
content_security_policy: Some(
"default-src 'self'; \
- script-src 'self' 'unsafe-inline' 'unsafe-eval'; \
- style-src 'self' 'unsafe-inline'; \
+ script-src 'self' 'unsafe-inline' 'unsafe-eval' https://unpkg.com https://cdnjs.cloudflare.com https://cdn.jsdelivr.net; \
+ style-src 'self' 'unsafe-inline' https://unpkg.com https://cdnjs.cloudflare.com https://cdn.jsdelivr.net https://fonts.googleapis.com; \
img-src 'self' data: https:; \
- font-src 'self' data:; \
+ font-src 'self' data: https://fonts.gstatic.com; \
connect-src 'self' wss: https:; \
frame-ancestors 'self'; \
base-uri 'self'; \
diff --git a/src/tasks/mod.rs b/src/tasks/mod.rs
index 72ab8609f..4cce8a8c2 100644
--- a/src/tasks/mod.rs
+++ b/src/tasks/mod.rs
@@ -655,12 +655,31 @@ fn extract_app_url_from_results(step_results: &Option, title:
// Helper functions to get real manifest stats
fn get_manifest_processed_count(state: &Arc, task_id: &str) -> String {
+ // First check in-memory manifest
if let Ok(manifests) = state.task_manifests.read() {
if let Some(manifest) = manifests.get(task_id) {
- return manifest.processing_stats.data_points_processed.to_string();
+ let count = manifest.processing_stats.data_points_processed;
+ if count > 0 {
+ return count.to_string();
+ }
+ // Fallback: count completed items from manifest sections
+ let completed_items: u64 = manifest.sections.iter()
+ .map(|s| {
+ let section_items = s.items.iter().filter(|i| i.status == crate::auto_task::ItemStatus::Completed).count() as u64;
+ let section_groups = s.item_groups.iter().filter(|g| g.status == crate::auto_task::ItemStatus::Completed).count() as u64;
+ let child_items: u64 = s.children.iter().map(|c| {
+ c.items.iter().filter(|i| i.status == crate::auto_task::ItemStatus::Completed).count() as u64 +
+ c.item_groups.iter().filter(|g| g.status == crate::auto_task::ItemStatus::Completed).count() as u64
+ }).sum();
+ section_items + section_groups + child_items
+ })
+ .sum();
+ if completed_items > 0 {
+ return completed_items.to_string();
+ }
}
}
- "0".to_string()
+ "-".to_string()
}
fn get_manifest_speed(state: &Arc, task_id: &str) -> String {
@@ -670,14 +689,22 @@ fn get_manifest_speed(state: &Arc, task_id: &str) -> String {
if speed > 0.0 {
return format!("{:.1}/min", speed);
}
+ // For completed tasks, show "-" instead of "calculating..."
+ if manifest.status == crate::auto_task::ManifestStatus::Completed {
+ return "-".to_string();
+ }
}
}
- "calculating...".to_string()
+ "-".to_string()
}
fn get_manifest_eta(state: &Arc, task_id: &str) -> String {
if let Ok(manifests) = state.task_manifests.read() {
if let Some(manifest) = manifests.get(task_id) {
+ // Check if completed first
+ if manifest.status == crate::auto_task::ManifestStatus::Completed {
+ return "Done".to_string();
+ }
let eta_secs = manifest.processing_stats.estimated_remaining_seconds;
if eta_secs > 0 {
if eta_secs >= 60 {
@@ -685,12 +712,10 @@ fn get_manifest_eta(state: &Arc, task_id: &str) -> String {
} else {
return format!("~{} sec", eta_secs);
}
- } else if manifest.status == crate::auto_task::ManifestStatus::Completed {
- return "Done".to_string();
}
}
}
- "calculating...".to_string()
+ "-".to_string()
}
fn build_taskmd_html(state: &Arc, task_id: &str, title: &str, runtime: &str, db_manifest: Option<&serde_json::Value>) -> (String, String) {