2026-01-22 19:45:18 -03:00
|
|
|
#[cfg(feature = "llm")]
|
2025-12-17 17:41:37 -03:00
|
|
|
use crate::llm::LLMProvider;
|
2025-11-30 22:33:54 -03:00
|
|
|
use crate::shared::models::UserSession;
|
|
|
|
|
use crate::shared::state::AppState;
|
2026-01-23 13:14:20 -03:00
|
|
|
use log::{debug, info};
|
2026-01-24 22:04:47 -03:00
|
|
|
#[cfg(feature = "llm")]
|
|
|
|
|
use log::warn;
|
2025-10-06 10:30:17 -03:00
|
|
|
use rhai::Dynamic;
|
|
|
|
|
use rhai::Engine;
|
2026-01-23 13:14:20 -03:00
|
|
|
#[cfg(feature = "llm")]
|
2025-12-17 17:41:37 -03:00
|
|
|
use serde_json::json;
|
2025-10-06 10:30:17 -03:00
|
|
|
use std::error::Error;
|
|
|
|
|
use std::fs;
|
|
|
|
|
use std::io::Read;
|
|
|
|
|
use std::path::PathBuf;
|
2026-01-23 13:14:20 -03:00
|
|
|
#[cfg(feature = "llm")]
|
2025-12-17 17:41:37 -03:00
|
|
|
use std::sync::Arc;
|
2025-11-30 22:33:54 -03:00
|
|
|
|
2026-01-22 19:45:18 -03:00
|
|
|
// When llm feature is disabled, create a dummy trait for type compatibility
|
|
|
|
|
#[cfg(not(feature = "llm"))]
|
2026-01-23 13:14:20 -03:00
|
|
|
#[allow(dead_code)]
|
2026-01-22 19:45:18 -03:00
|
|
|
trait LLMProvider: Send + Sync {}
|
|
|
|
|
|
2025-12-17 17:41:37 -03:00
|
|
|
pub fn create_site_keyword(state: &AppState, user: UserSession, engine: &mut Engine) {
|
2025-11-30 22:33:54 -03:00
|
|
|
let state_clone = state.clone();
|
2025-12-26 08:59:25 -03:00
|
|
|
let user_clone = user;
|
2025-12-17 17:41:37 -03:00
|
|
|
|
2025-11-30 22:33:54 -03:00
|
|
|
engine
|
|
|
|
|
.register_custom_syntax(
|
2025-12-26 08:59:25 -03:00
|
|
|
["CREATE", "SITE", "$expr$", ",", "$expr$", ",", "$expr$"],
|
2025-11-30 22:33:54 -03:00
|
|
|
true,
|
|
|
|
|
move |context, inputs| {
|
|
|
|
|
if inputs.len() < 3 {
|
|
|
|
|
return Err("Not enough arguments for CREATE SITE".into());
|
|
|
|
|
}
|
|
|
|
|
let alias = context.eval_expression_tree(&inputs[0])?;
|
|
|
|
|
let template_dir = context.eval_expression_tree(&inputs[1])?;
|
|
|
|
|
let prompt = context.eval_expression_tree(&inputs[2])?;
|
2025-12-17 17:41:37 -03:00
|
|
|
|
2025-12-28 21:26:08 -03:00
|
|
|
let config = match state_clone.config.as_ref() {
|
|
|
|
|
Some(c) => c.clone(),
|
|
|
|
|
None => {
|
|
|
|
|
return Err(Box::new(rhai::EvalAltResult::ErrorRuntime(
|
|
|
|
|
"Config must be initialized".into(),
|
|
|
|
|
rhai::Position::NONE,
|
|
|
|
|
)));
|
|
|
|
|
}
|
|
|
|
|
};
|
2025-12-17 17:41:37 -03:00
|
|
|
|
2026-02-04 13:29:29 -03:00
|
|
|
let s3 = state_clone.drive.clone().map(std::sync::Arc::new);
|
2025-12-17 17:41:37 -03:00
|
|
|
let bucket = state_clone.bucket_name.clone();
|
|
|
|
|
let bot_id = user_clone.bot_id.to_string();
|
|
|
|
|
|
|
|
|
|
#[cfg(feature = "llm")]
|
2026-01-22 19:45:18 -03:00
|
|
|
let llm = Some(state_clone.llm_provider.clone());
|
2025-12-17 17:41:37 -03:00
|
|
|
#[cfg(not(feature = "llm"))]
|
2026-01-22 19:45:18 -03:00
|
|
|
let llm: Option<()> = None;
|
2025-12-17 17:41:37 -03:00
|
|
|
|
|
|
|
|
let fut = create_site(config, s3, bucket, bot_id, llm, alias, template_dir, prompt);
|
2025-11-30 22:33:54 -03:00
|
|
|
let result =
|
|
|
|
|
tokio::task::block_in_place(|| tokio::runtime::Handle::current().block_on(fut))
|
|
|
|
|
.map_err(|e| format!("Site creation failed: {}", e))?;
|
|
|
|
|
Ok(Dynamic::from(result))
|
|
|
|
|
},
|
|
|
|
|
)
|
feat(security): Complete security infrastructure implementation
SECURITY MODULES ADDED:
- security/auth.rs: Full RBAC with roles (Anonymous, User, Moderator, Admin, SuperAdmin, Service, Bot, BotOwner, BotOperator, BotViewer) and permissions
- security/cors.rs: Hardened CORS (no wildcard in production, env-based config)
- security/panic_handler.rs: Panic catching middleware with safe 500 responses
- security/path_guard.rs: Path traversal protection, null byte prevention
- security/request_id.rs: UUID request tracking with correlation IDs
- security/error_sanitizer.rs: Sensitive data redaction from responses
- security/zitadel_auth.rs: Zitadel token introspection and role mapping
- security/sql_guard.rs: SQL injection prevention with table whitelist
- security/command_guard.rs: Command injection prevention
- security/secrets.rs: Zeroizing secret management
- security/validation.rs: Input validation utilities
- security/rate_limiter.rs: Rate limiting with governor crate
- security/headers.rs: Security headers (CSP, HSTS, X-Frame-Options)
MAIN.RS UPDATES:
- Replaced tower_http::cors::Any with hardened create_cors_layer()
- Added panic handler middleware
- Added request ID tracking middleware
- Set global panic hook
SECURITY STATUS:
- 0 unwrap() in production code
- 0 panic! in production code
- 0 unsafe blocks
- cargo audit: PASS (no vulnerabilities)
- Estimated completion: ~98%
Remaining: Wire auth middleware to handlers, audit logs for sensitive data
2025-12-28 19:29:18 -03:00
|
|
|
.expect("valid syntax registration");
|
2025-10-06 10:30:17 -03:00
|
|
|
}
|
2025-11-30 22:33:54 -03:00
|
|
|
|
2026-01-22 19:45:18 -03:00
|
|
|
#[cfg(feature = "llm")]
|
2025-11-30 22:33:54 -03:00
|
|
|
async fn create_site(
|
2026-01-16 11:29:22 -03:00
|
|
|
config: crate::core::config::AppConfig,
|
2025-12-17 17:41:37 -03:00
|
|
|
s3: Option<std::sync::Arc<aws_sdk_s3::Client>>,
|
|
|
|
|
bucket: String,
|
|
|
|
|
bot_id: String,
|
|
|
|
|
llm: Option<Arc<dyn LLMProvider>>,
|
2025-11-30 22:33:54 -03:00
|
|
|
alias: Dynamic,
|
|
|
|
|
template_dir: Dynamic,
|
|
|
|
|
prompt: Dynamic,
|
|
|
|
|
) -> Result<String, Box<dyn Error + Send + Sync>> {
|
2025-12-17 17:41:37 -03:00
|
|
|
let alias_str = alias.to_string();
|
|
|
|
|
let template_dir_str = template_dir.to_string();
|
|
|
|
|
let prompt_str = prompt.to_string();
|
|
|
|
|
|
|
|
|
|
info!(
|
|
|
|
|
"CREATE SITE: {} from template {}",
|
|
|
|
|
alias_str, template_dir_str
|
|
|
|
|
);
|
|
|
|
|
|
2025-11-30 22:33:54 -03:00
|
|
|
let base_path = PathBuf::from(&config.site_path);
|
2025-12-17 17:41:37 -03:00
|
|
|
let template_path = base_path.join(&template_dir_str);
|
|
|
|
|
|
|
|
|
|
let combined_content = load_templates(&template_path)?;
|
|
|
|
|
|
|
|
|
|
let generated_html = generate_html_from_prompt(llm, &combined_content, &prompt_str).await?;
|
|
|
|
|
|
|
|
|
|
let drive_path = format!("apps/{}", alias_str);
|
2025-12-26 08:59:25 -03:00
|
|
|
store_to_drive(s3.as_ref(), &bucket, &bot_id, &drive_path, &generated_html).await?;
|
2025-11-30 22:33:54 -03:00
|
|
|
|
2025-12-17 17:41:37 -03:00
|
|
|
let serve_path = base_path.join(&alias_str);
|
feat(autotask): Implement AutoTask system with intent classification and app generation
- Add IntentClassifier with 7 intent types (APP_CREATE, TODO, MONITOR, ACTION, SCHEDULE, GOAL, TOOL)
- Add AppGenerator with LLM-powered app structure analysis
- Add DesignerAI for modifying apps through conversation
- Add app_server for serving generated apps with clean URLs
- Add db_api for CRUD operations on bot database tables
- Add ask_later keyword for pending info collection
- Add migration 6.1.1 with tables: pending_info, auto_tasks, execution_plans, task_approvals, task_decisions, safety_audit_log, generated_apps, intent_classifications, designer_changes
- Write apps to S3 drive and sync to SITE_ROOT for serving
- Clean URL structure: /apps/{app_name}/
- Integrate with DriveMonitor for file sync
Based on Chapter 17 - Autonomous Tasks specification
2025-12-27 21:10:09 -03:00
|
|
|
sync_to_serve_path(&serve_path, &generated_html, &template_path)?;
|
2025-11-30 22:33:54 -03:00
|
|
|
|
2025-12-17 17:41:37 -03:00
|
|
|
info!(
|
|
|
|
|
"CREATE SITE: {} completed, available at /apps/{}",
|
|
|
|
|
alias_str, alias_str
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
Ok(format!("/apps/{}", alias_str))
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-22 19:45:18 -03:00
|
|
|
#[cfg(not(feature = "llm"))]
|
|
|
|
|
async fn create_site(
|
|
|
|
|
config: crate::core::config::AppConfig,
|
|
|
|
|
s3: Option<std::sync::Arc<aws_sdk_s3::Client>>,
|
|
|
|
|
bucket: String,
|
|
|
|
|
bot_id: String,
|
|
|
|
|
_llm: Option<()>,
|
|
|
|
|
alias: Dynamic,
|
|
|
|
|
template_dir: Dynamic,
|
|
|
|
|
prompt: Dynamic,
|
|
|
|
|
) -> Result<String, Box<dyn Error + Send + Sync>> {
|
|
|
|
|
let alias_str = alias.to_string();
|
|
|
|
|
let template_dir_str = template_dir.to_string();
|
|
|
|
|
let prompt_str = prompt.to_string();
|
|
|
|
|
|
|
|
|
|
info!(
|
|
|
|
|
"CREATE SITE: {} from template {}",
|
|
|
|
|
alias_str, template_dir_str
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let base_path = PathBuf::from(&config.site_path);
|
|
|
|
|
let template_path = base_path.join(&template_dir_str);
|
|
|
|
|
|
|
|
|
|
let combined_content = load_templates(&template_path)?;
|
|
|
|
|
|
|
|
|
|
let generated_html = generate_html_from_prompt(_llm, &combined_content, &prompt_str).await?;
|
|
|
|
|
|
|
|
|
|
let drive_path = format!("apps/{}", alias_str);
|
|
|
|
|
store_to_drive(s3.as_ref(), &bucket, &bot_id, &drive_path, &generated_html).await?;
|
|
|
|
|
|
|
|
|
|
let serve_path = base_path.join(&alias_str);
|
|
|
|
|
sync_to_serve_path(&serve_path, &generated_html, &template_path)?;
|
|
|
|
|
|
|
|
|
|
info!(
|
|
|
|
|
"CREATE SITE: {} completed, available at /apps/{}",
|
|
|
|
|
alias_str, alias_str
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
Ok(format!("/apps/{}", alias_str))
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-26 08:59:25 -03:00
|
|
|
fn load_templates(template_path: &std::path::Path) -> Result<String, Box<dyn Error + Send + Sync>> {
|
2025-11-30 22:33:54 -03:00
|
|
|
let mut combined_content = String::new();
|
2025-12-17 17:41:37 -03:00
|
|
|
|
|
|
|
|
if !template_path.exists() {
|
2025-12-26 08:59:25 -03:00
|
|
|
return Err(format!("Template directory not found: {}", template_path.display()).into());
|
2025-12-17 17:41:37 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for entry in fs::read_dir(template_path).map_err(|e| e.to_string())? {
|
2025-11-30 22:33:54 -03:00
|
|
|
let entry = entry.map_err(|e| e.to_string())?;
|
|
|
|
|
let path = entry.path();
|
2025-12-17 17:41:37 -03:00
|
|
|
|
2025-12-26 08:59:25 -03:00
|
|
|
if path.extension().is_some_and(|ext| ext == "html") {
|
2025-11-30 22:33:54 -03:00
|
|
|
let mut file = fs::File::open(&path).map_err(|e| e.to_string())?;
|
|
|
|
|
let mut contents = String::new();
|
|
|
|
|
file.read_to_string(&mut contents)
|
|
|
|
|
.map_err(|e| e.to_string())?;
|
2025-12-17 17:41:37 -03:00
|
|
|
|
2025-12-26 08:59:25 -03:00
|
|
|
use std::fmt::Write;
|
|
|
|
|
let _ = writeln!(combined_content, "<!-- TEMPLATE: {} -->", path.display());
|
2025-11-30 22:33:54 -03:00
|
|
|
combined_content.push_str(&contents);
|
|
|
|
|
combined_content.push_str("\n\n--- TEMPLATE SEPARATOR ---\n\n");
|
2025-12-17 17:41:37 -03:00
|
|
|
|
2025-12-26 08:59:25 -03:00
|
|
|
debug!("Loaded template: {}", path.display());
|
2025-11-30 22:33:54 -03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-17 17:41:37 -03:00
|
|
|
if combined_content.is_empty() {
|
|
|
|
|
return Err("No HTML templates found in template directory".into());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(combined_content)
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-22 19:45:18 -03:00
|
|
|
#[cfg(feature = "llm")]
|
2025-12-17 17:41:37 -03:00
|
|
|
async fn generate_html_from_prompt(
|
|
|
|
|
llm: Option<Arc<dyn LLMProvider>>,
|
|
|
|
|
templates: &str,
|
|
|
|
|
prompt: &str,
|
|
|
|
|
) -> Result<String, Box<dyn Error + Send + Sync>> {
|
|
|
|
|
let full_prompt = format!(
|
|
|
|
|
r#"You are an expert HTML/HTMX developer. Generate a complete HTML application.
|
|
|
|
|
|
|
|
|
|
TEMPLATE FILES FOR REFERENCE:
|
|
|
|
|
{}
|
|
|
|
|
|
|
|
|
|
USER REQUEST:
|
|
|
|
|
{}
|
|
|
|
|
|
|
|
|
|
REQUIREMENTS:
|
|
|
|
|
1. Clone the template structure and styling
|
|
|
|
|
2. Use ONLY local _assets (htmx.min.js, app.js, styles.css) - NO external CDNs
|
|
|
|
|
3. Use HTMX for all data operations:
|
|
|
|
|
- hx-get="/api/db/TABLE" for lists
|
|
|
|
|
- hx-post="/api/db/TABLE" for create
|
|
|
|
|
- hx-put="/api/db/TABLE/ID" for update
|
|
|
|
|
- hx-delete="/api/db/TABLE/ID" for delete
|
|
|
|
|
4. Include search with hx-trigger="keyup changed delay:300ms"
|
|
|
|
|
5. Generate semantic, accessible HTML
|
|
|
|
|
6. App context is automatic - just use /api/db/* paths
|
|
|
|
|
|
|
|
|
|
OUTPUT: Complete index.html file only, no explanations."#,
|
|
|
|
|
templates, prompt
|
2025-11-30 22:33:54 -03:00
|
|
|
);
|
|
|
|
|
|
2025-12-17 17:41:37 -03:00
|
|
|
let html = match llm {
|
|
|
|
|
Some(provider) => {
|
|
|
|
|
let messages = json!([{
|
|
|
|
|
"role": "user",
|
|
|
|
|
"content": full_prompt
|
|
|
|
|
}]);
|
|
|
|
|
|
|
|
|
|
match provider
|
|
|
|
|
.generate(&full_prompt, &messages, "gpt-4o-mini", "")
|
|
|
|
|
.await
|
|
|
|
|
{
|
|
|
|
|
Ok(response) => {
|
|
|
|
|
let cleaned = extract_html_from_response(&response);
|
|
|
|
|
if cleaned.contains("<html") || cleaned.contains("<!DOCTYPE") {
|
|
|
|
|
info!("LLM generated HTML ({} bytes)", cleaned.len());
|
|
|
|
|
cleaned
|
|
|
|
|
} else {
|
|
|
|
|
warn!("LLM response doesn't contain valid HTML, using placeholder");
|
|
|
|
|
generate_placeholder_html(prompt)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Err(e) => {
|
|
|
|
|
warn!("LLM generation failed: {}, using placeholder", e);
|
|
|
|
|
generate_placeholder_html(prompt)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
None => {
|
|
|
|
|
debug!("No LLM provider configured, using placeholder HTML");
|
|
|
|
|
generate_placeholder_html(prompt)
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
debug!("Generated HTML ({} bytes)", html.len());
|
|
|
|
|
Ok(html)
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-22 19:45:18 -03:00
|
|
|
#[cfg(not(feature = "llm"))]
|
|
|
|
|
async fn generate_html_from_prompt(
|
|
|
|
|
_llm: Option<()>,
|
|
|
|
|
_templates: &str,
|
|
|
|
|
prompt: &str,
|
|
|
|
|
) -> Result<String, Box<dyn Error + Send + Sync>> {
|
|
|
|
|
debug!("LLM feature not enabled, using placeholder HTML");
|
|
|
|
|
Ok(generate_placeholder_html(prompt))
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-23 13:14:20 -03:00
|
|
|
#[cfg(feature = "llm")]
|
2025-12-17 17:41:37 -03:00
|
|
|
fn extract_html_from_response(response: &str) -> String {
|
|
|
|
|
let trimmed = response.trim();
|
|
|
|
|
|
|
|
|
|
if trimmed.starts_with("```html") {
|
|
|
|
|
let without_prefix = trimmed.strip_prefix("```html").unwrap_or(trimmed);
|
|
|
|
|
let without_suffix = without_prefix
|
|
|
|
|
.trim()
|
|
|
|
|
.strip_suffix("```")
|
|
|
|
|
.unwrap_or(without_prefix);
|
|
|
|
|
return without_suffix.trim().to_string();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if trimmed.starts_with("```") {
|
|
|
|
|
let without_prefix = trimmed.strip_prefix("```").unwrap_or(trimmed);
|
|
|
|
|
let without_suffix = without_prefix
|
|
|
|
|
.trim()
|
|
|
|
|
.strip_suffix("```")
|
|
|
|
|
.unwrap_or(without_prefix);
|
|
|
|
|
return without_suffix.trim().to_string();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
trimmed.to_string()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn generate_placeholder_html(prompt: &str) -> String {
|
|
|
|
|
format!(
|
|
|
|
|
r##"<!DOCTYPE html>
|
|
|
|
|
<html lang="en">
|
|
|
|
|
<head>
|
|
|
|
|
<meta charset="UTF-8">
|
|
|
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
|
|
|
<title>App</title>
|
|
|
|
|
<script src="_assets/htmx.min.js"></script>
|
|
|
|
|
<link rel="stylesheet" href="_assets/styles.css">
|
|
|
|
|
</head>
|
|
|
|
|
<body>
|
|
|
|
|
<header>
|
|
|
|
|
<h1>Generated App</h1>
|
|
|
|
|
<p>Prompt: {}</p>
|
|
|
|
|
</header>
|
|
|
|
|
|
|
|
|
|
<main>
|
|
|
|
|
<section>
|
|
|
|
|
<h2>Data</h2>
|
|
|
|
|
<div id="data-list"
|
|
|
|
|
hx-get="/api/db/items"
|
|
|
|
|
hx-trigger="load"
|
|
|
|
|
hx-swap="innerHTML">
|
|
|
|
|
Loading...
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<form hx-post="/api/db/items"
|
|
|
|
|
hx-target="#data-list"
|
|
|
|
|
hx-swap="afterbegin">
|
|
|
|
|
<input name="name" placeholder="Name" required>
|
|
|
|
|
<button type="submit">Add</button>
|
|
|
|
|
</form>
|
|
|
|
|
</section>
|
|
|
|
|
</main>
|
|
|
|
|
|
|
|
|
|
<script src="_assets/app.js"></script>
|
|
|
|
|
</body>
|
|
|
|
|
</html>"##,
|
|
|
|
|
prompt
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn store_to_drive(
|
2025-12-26 08:59:25 -03:00
|
|
|
s3: Option<&std::sync::Arc<aws_sdk_s3::Client>>,
|
2025-12-17 17:41:37 -03:00
|
|
|
bucket: &str,
|
|
|
|
|
bot_id: &str,
|
|
|
|
|
drive_path: &str,
|
|
|
|
|
html_content: &str,
|
|
|
|
|
) -> Result<(), Box<dyn Error + Send + Sync>> {
|
|
|
|
|
let Some(s3_client) = s3 else {
|
|
|
|
|
debug!("S3 not configured, skipping drive storage");
|
|
|
|
|
return Ok(());
|
|
|
|
|
};
|
|
|
|
|
let key = format!("{}.gbdrive/{}/index.html", bot_id, drive_path);
|
|
|
|
|
|
|
|
|
|
info!("Storing to drive: s3://{}/{}", bucket, key);
|
|
|
|
|
|
|
|
|
|
s3_client
|
|
|
|
|
.put_object()
|
|
|
|
|
.bucket(bucket)
|
|
|
|
|
.key(&key)
|
|
|
|
|
.body(html_content.as_bytes().to_vec().into())
|
|
|
|
|
.content_type("text/html")
|
|
|
|
|
.send()
|
|
|
|
|
.await
|
|
|
|
|
.map_err(|e| format!("Failed to store to drive: {}", e))?;
|
|
|
|
|
|
|
|
|
|
let schema_key = format!("{}.gbdrive/{}/schema.json", bot_id, drive_path);
|
|
|
|
|
let schema = r#"{"tables": {}, "version": 1}"#;
|
|
|
|
|
|
|
|
|
|
s3_client
|
|
|
|
|
.put_object()
|
|
|
|
|
.bucket(bucket)
|
|
|
|
|
.key(&schema_key)
|
|
|
|
|
.body(schema.as_bytes().to_vec().into())
|
|
|
|
|
.content_type("application/json")
|
|
|
|
|
.send()
|
|
|
|
|
.await
|
|
|
|
|
.map_err(|e| format!("Failed to store schema: {}", e))?;
|
|
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
feat(autotask): Implement AutoTask system with intent classification and app generation
- Add IntentClassifier with 7 intent types (APP_CREATE, TODO, MONITOR, ACTION, SCHEDULE, GOAL, TOOL)
- Add AppGenerator with LLM-powered app structure analysis
- Add DesignerAI for modifying apps through conversation
- Add app_server for serving generated apps with clean URLs
- Add db_api for CRUD operations on bot database tables
- Add ask_later keyword for pending info collection
- Add migration 6.1.1 with tables: pending_info, auto_tasks, execution_plans, task_approvals, task_decisions, safety_audit_log, generated_apps, intent_classifications, designer_changes
- Write apps to S3 drive and sync to SITE_ROOT for serving
- Clean URL structure: /apps/{app_name}/
- Integrate with DriveMonitor for file sync
Based on Chapter 17 - Autonomous Tasks specification
2025-12-27 21:10:09 -03:00
|
|
|
fn sync_to_serve_path(
|
2025-12-26 08:59:25 -03:00
|
|
|
serve_path: &std::path::Path,
|
2025-12-17 17:41:37 -03:00
|
|
|
html_content: &str,
|
2025-12-26 08:59:25 -03:00
|
|
|
template_path: &std::path::Path,
|
2025-12-17 17:41:37 -03:00
|
|
|
) -> Result<(), Box<dyn Error + Send + Sync>> {
|
|
|
|
|
fs::create_dir_all(serve_path).map_err(|e| format!("Failed to create serve path: {}", e))?;
|
|
|
|
|
|
|
|
|
|
let index_path = serve_path.join("index.html");
|
|
|
|
|
fs::write(&index_path, html_content)
|
|
|
|
|
.map_err(|e| format!("Failed to write index.html: {}", e))?;
|
|
|
|
|
|
2025-12-26 08:59:25 -03:00
|
|
|
info!("Written: {}", index_path.display());
|
2025-12-17 17:41:37 -03:00
|
|
|
|
|
|
|
|
let template_assets = template_path.join("_assets");
|
|
|
|
|
let serve_assets = serve_path.join("_assets");
|
|
|
|
|
|
|
|
|
|
if template_assets.exists() {
|
|
|
|
|
copy_dir_recursive(&template_assets, &serve_assets)?;
|
2025-12-26 08:59:25 -03:00
|
|
|
info!("Copied assets to: {}", serve_assets.display());
|
2025-12-17 17:41:37 -03:00
|
|
|
} else {
|
|
|
|
|
fs::create_dir_all(&serve_assets)
|
|
|
|
|
.map_err(|e| format!("Failed to create assets dir: {}", e))?;
|
|
|
|
|
|
|
|
|
|
let htmx_path = serve_assets.join("htmx.min.js");
|
|
|
|
|
if !htmx_path.exists() {
|
|
|
|
|
fs::write(&htmx_path, "/* HTMX - include from CDN or bundle */")
|
|
|
|
|
.map_err(|e| format!("Failed to write htmx: {}", e))?;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let styles_path = serve_assets.join("styles.css");
|
|
|
|
|
if !styles_path.exists() {
|
|
|
|
|
fs::write(&styles_path, DEFAULT_STYLES)
|
|
|
|
|
.map_err(|e| format!("Failed to write styles: {}", e))?;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let app_js_path = serve_assets.join("app.js");
|
|
|
|
|
if !app_js_path.exists() {
|
|
|
|
|
fs::write(&app_js_path, DEFAULT_APP_JS)
|
|
|
|
|
.map_err(|e| format!("Failed to write app.js: {}", e))?;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let schema_path = serve_path.join("schema.json");
|
|
|
|
|
fs::write(&schema_path, r#"{"tables": {}, "version": 1}"#)
|
|
|
|
|
.map_err(|e| format!("Failed to write schema.json: {}", e))?;
|
|
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn copy_dir_recursive(src: &PathBuf, dst: &PathBuf) -> Result<(), Box<dyn Error + Send + Sync>> {
|
2025-12-26 08:59:25 -03:00
|
|
|
fs::create_dir_all(dst)
|
|
|
|
|
.map_err(|e| format!("Failed to create dir {}: {}", dst.display(), e))?;
|
2025-12-17 17:41:37 -03:00
|
|
|
|
|
|
|
|
for entry in fs::read_dir(src).map_err(|e| e.to_string())? {
|
|
|
|
|
let entry = entry.map_err(|e| e.to_string())?;
|
|
|
|
|
let src_path = entry.path();
|
|
|
|
|
let dst_path = dst.join(entry.file_name());
|
|
|
|
|
|
|
|
|
|
if src_path.is_dir() {
|
|
|
|
|
copy_dir_recursive(&src_path, &dst_path)?;
|
|
|
|
|
} else {
|
|
|
|
|
fs::copy(&src_path, &dst_path)
|
2025-12-26 08:59:25 -03:00
|
|
|
.map_err(|e| format!("Failed to copy file {}: {}", src_path.display(), e))?;
|
2025-12-17 17:41:37 -03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
Update attendance, keywords, calendar, compliance, console, core, drive, email, llm, msteams, security, and tasks modules
2025-12-24 09:29:27 -03:00
|
|
|
const DEFAULT_STYLES: &str = r"
|
2025-12-17 17:41:37 -03:00
|
|
|
:root {
|
|
|
|
|
--primary: #0ea5e9;
|
|
|
|
|
--success: #22c55e;
|
|
|
|
|
--warning: #f59e0b;
|
|
|
|
|
--danger: #ef4444;
|
|
|
|
|
--bg: #ffffff;
|
|
|
|
|
--text: #1e293b;
|
|
|
|
|
--border: #e2e8f0;
|
|
|
|
|
--radius: 8px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@media (prefers-color-scheme: dark) {
|
|
|
|
|
:root {
|
|
|
|
|
--bg: #0f172a;
|
|
|
|
|
--text: #f1f5f9;
|
|
|
|
|
--border: #334155;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
|
|
|
|
|
|
|
|
body {
|
|
|
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
|
|
|
background: var(--bg);
|
|
|
|
|
color: var(--text);
|
|
|
|
|
line-height: 1.5;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
header {
|
|
|
|
|
padding: 1rem 2rem;
|
|
|
|
|
border-bottom: 1px solid var(--border);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
main {
|
|
|
|
|
padding: 2rem;
|
|
|
|
|
max-width: 1200px;
|
|
|
|
|
margin: 0 auto;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
h1, h2, h3 { margin-bottom: 1rem; }
|
|
|
|
|
|
|
|
|
|
input, select, textarea {
|
|
|
|
|
padding: 0.5rem 1rem;
|
|
|
|
|
border: 1px solid var(--border);
|
|
|
|
|
border-radius: var(--radius);
|
|
|
|
|
background: var(--bg);
|
|
|
|
|
color: var(--text);
|
|
|
|
|
font-size: 1rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
input:focus, select:focus, textarea:focus {
|
|
|
|
|
outline: none;
|
|
|
|
|
border-color: var(--primary);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
button {
|
|
|
|
|
padding: 0.5rem 1rem;
|
|
|
|
|
background: var(--primary);
|
|
|
|
|
color: white;
|
|
|
|
|
border: none;
|
|
|
|
|
border-radius: var(--radius);
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
font-size: 1rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
button:hover { opacity: 0.9; }
|
|
|
|
|
|
|
|
|
|
form {
|
|
|
|
|
display: flex;
|
|
|
|
|
gap: 0.5rem;
|
|
|
|
|
margin: 1rem 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
table {
|
|
|
|
|
width: 100%;
|
|
|
|
|
border-collapse: collapse;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
th, td {
|
|
|
|
|
padding: 0.75rem;
|
|
|
|
|
text-align: left;
|
|
|
|
|
border-bottom: 1px solid var(--border);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.htmx-indicator {
|
|
|
|
|
opacity: 0;
|
|
|
|
|
transition: opacity 0.2s;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.htmx-request .htmx-indicator {
|
|
|
|
|
opacity: 1;
|
|
|
|
|
}
|
Update attendance, keywords, calendar, compliance, console, core, drive, email, llm, msteams, security, and tasks modules
2025-12-24 09:29:27 -03:00
|
|
|
";
|
2025-12-23 18:40:58 -03:00
|
|
|
|
Update attendance, keywords, calendar, compliance, console, core, drive, email, llm, msteams, security, and tasks modules
2025-12-24 09:29:27 -03:00
|
|
|
const DEFAULT_APP_JS: &str = r"
|
2025-12-23 18:40:58 -03:00
|
|
|
|
2025-12-17 17:41:37 -03:00
|
|
|
function toast(message, type = 'info') {
|
|
|
|
|
const el = document.createElement('div');
|
|
|
|
|
el.className = 'toast toast-' + type;
|
|
|
|
|
el.textContent = message;
|
|
|
|
|
el.style.cssText = 'position:fixed;bottom:20px;right:20px;padding:1rem;background:#333;color:#fff;border-radius:8px;z-index:9999;';
|
|
|
|
|
document.body.appendChild(el);
|
|
|
|
|
setTimeout(() => el.remove(), 3000);
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-23 18:40:58 -03:00
|
|
|
|
2025-12-17 17:41:37 -03:00
|
|
|
document.body.addEventListener('htmx:afterSwap', function(e) {
|
|
|
|
|
console.log('Data updated:', e.detail.target.id);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
document.body.addEventListener('htmx:responseError', function(e) {
|
|
|
|
|
toast('Error: ' + (e.detail.xhr.responseText || 'Request failed'), 'error');
|
|
|
|
|
});
|
|
|
|
|
|
2025-12-23 18:40:58 -03:00
|
|
|
|
2025-12-17 17:41:37 -03:00
|
|
|
function openModal(id) {
|
|
|
|
|
document.getElementById(id)?.classList.add('active');
|
|
|
|
|
}
|
2025-11-30 22:33:54 -03:00
|
|
|
|
2025-12-17 17:41:37 -03:00
|
|
|
function closeModal(id) {
|
|
|
|
|
document.getElementById(id)?.classList.remove('active');
|
2025-10-06 10:30:17 -03:00
|
|
|
}
|
Update attendance, keywords, calendar, compliance, console, core, drive, email, llm, msteams, security, and tasks modules
2025-12-24 09:29:27 -03:00
|
|
|
";
|