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
This commit is contained in:
parent
021080d763
commit
7bad8d50f7
5 changed files with 115 additions and 34 deletions
|
|
@ -490,8 +490,10 @@ Every HTML page MUST include proper SEO meta tags:
|
|||
<meta property="og:type" content="website">
|
||||
<link rel="icon" href="/assets/icons/gb-logo.svg" type="image/svg+xml">
|
||||
<title>{Page Title} - {App Name}</title>
|
||||
<!-- IMPORTANT: Use relative paths for app assets -->
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
<script src="/js/vendor/htmx.min.js"></script>
|
||||
<!-- HTMX from CDN - allowed by CSP -->
|
||||
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
|
||||
<script src="designer.js" defer></script>
|
||||
</head>
|
||||
```
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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: <script src="https://unpkg.com/htmx.org@1.9.10"></script>
|
||||
- CSS link should be: <link rel="stylesheet" href="styles.css">
|
||||
|
||||
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<dyn std::error::Error + Send + Sync>> {
|
||||
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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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'; \
|
||||
|
|
|
|||
|
|
@ -655,12 +655,31 @@ fn extract_app_url_from_results(step_results: &Option<serde_json::Value>, title:
|
|||
|
||||
// Helper functions to get real manifest stats
|
||||
fn get_manifest_processed_count(state: &Arc<AppState>, 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<AppState>, task_id: &str) -> String {
|
||||
|
|
@ -670,14 +689,22 @@ fn get_manifest_speed(state: &Arc<AppState>, 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<AppState>, 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<AppState>, 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<AppState>, task_id: &str, title: &str, runtime: &str, db_manifest: Option<&serde_json::Value>) -> (String, String) {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue