From 7bad8d50f7233f0fd106ae440ba27cab9a1fc267 Mon Sep 17 00:00:00 2001 From: "Rodrigo Rodriguez (Pragmatismo)" Date: Fri, 2 Jan 2026 17:48:51 -0300 Subject: [PATCH] Fix designer file writing and CSP for CDN assets - Designer now uses state.bucket_name (like app_generator) instead of DB lookup - Fixed local file path to match app_server fallback: {site_path}/{bot}.gbai/{bot}.gbapp/{app}/{file} - Fixed S3 path to match app_server: {bot}.gbapp/{app}/{file} in bucket {bot}.gbai - Added S3 bucket creation retry logic (like app_generator) - Updated CSP to allow unpkg.com, cdnjs.cloudflare.com, cdn.jsdelivr.net for scripts/styles - Added fonts.googleapis.com and fonts.gstatic.com for web fonts - Updated APP_GENERATOR_PROMPT to use HTMX CDN instead of non-existent /js/vendor path - Added designer prompt guidelines for relative asset paths --- src/auto_task/APP_GENERATOR_PROMPT.md | 4 +- src/auto_task/app_generator.rs | 32 ++++++++---- src/designer/mod.rs | 70 +++++++++++++++++++++------ src/security/headers.rs | 6 +-- src/tasks/mod.rs | 37 +++++++++++--- 5 files changed, 115 insertions(+), 34 deletions(-) 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) {