Fix panic: check if item_groups is empty before iterating
Prevents 'index out of bounds: the len is 0 but the index is 0' error in complete_item_group_range when item_groups is empty.
This commit is contained in:
parent
938e154c8e
commit
86ac5ca8f5
10 changed files with 1296 additions and 415 deletions
|
|
@ -3325,6 +3325,7 @@ CREATE TABLE IF NOT EXISTS auto_tasks (
|
|||
total_steps INTEGER DEFAULT 0,
|
||||
progress FLOAT DEFAULT 0.0,
|
||||
step_results JSONB DEFAULT '[]'::jsonb,
|
||||
manifest_json JSONB,
|
||||
error TEXT,
|
||||
started_at TIMESTAMPTZ,
|
||||
completed_at TIMESTAMPTZ,
|
||||
|
|
|
|||
3
migrations/20250710000001_add_manifest_json/down.sql
Normal file
3
migrations/20250710000001_add_manifest_json/down.sql
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
-- Remove manifest_json column from auto_tasks table
|
||||
DROP INDEX IF EXISTS idx_auto_tasks_manifest_json;
|
||||
ALTER TABLE auto_tasks DROP COLUMN IF EXISTS manifest_json;
|
||||
5
migrations/20250710000001_add_manifest_json/up.sql
Normal file
5
migrations/20250710000001_add_manifest_json/up.sql
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
-- Add manifest_json column to store the full task manifest for historical viewing
|
||||
ALTER TABLE auto_tasks ADD COLUMN IF NOT EXISTS manifest_json JSONB;
|
||||
|
||||
-- Add an index for faster lookups when manifest exists
|
||||
CREATE INDEX IF NOT EXISTS idx_auto_tasks_manifest_json ON auto_tasks USING gin (manifest_json) WHERE manifest_json IS NOT NULL;
|
||||
|
|
@ -504,51 +504,73 @@ Every HTML page MUST include proper SEO meta tags:
|
|||
|
||||
---
|
||||
|
||||
## RESPONSE FORMAT
|
||||
## RESPONSE FORMAT (STREAMING DELIMITERS)
|
||||
|
||||
When generating an app, respond with JSON:
|
||||
Use this EXACT format with delimiters (NOT JSON) so content can stream safely:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "app-name-lowercase-dashes",
|
||||
"description": "What this app does",
|
||||
"domain": "custom|healthcare|sales|inventory|booking|etc",
|
||||
"tables": [
|
||||
{
|
||||
"name": "table_name",
|
||||
"fields": [
|
||||
{"name": "id", "type": "guid", "nullable": false},
|
||||
{"name": "created_at", "type": "datetime", "nullable": false},
|
||||
{"name": "updated_at", "type": "datetime", "nullable": false},
|
||||
{"name": "field_name", "type": "string", "nullable": true}
|
||||
]
|
||||
}
|
||||
],
|
||||
"pages": [
|
||||
{
|
||||
"filename": "index.html",
|
||||
"title": "Dashboard",
|
||||
"html": "complete HTML document"
|
||||
}
|
||||
],
|
||||
"tools": [
|
||||
{
|
||||
"name": "tool_name",
|
||||
"triggers": ["phrase1", "phrase2"],
|
||||
"basic_code": "BASIC code"
|
||||
}
|
||||
],
|
||||
"schedulers": [
|
||||
{
|
||||
"name": "scheduler_name",
|
||||
"schedule": "0 9 * * *",
|
||||
"basic_code": "BASIC code"
|
||||
}
|
||||
],
|
||||
"css": "complete CSS styles",
|
||||
"custom_js": "optional JavaScript"
|
||||
}
|
||||
```
|
||||
<<<APP_START>>>
|
||||
name: app-name-lowercase-dashes
|
||||
description: What this app does
|
||||
domain: healthcare|sales|inventory|booking|utility|etc
|
||||
<<<TABLES_START>>>
|
||||
<<<TABLE:table_name>>>
|
||||
id:guid:false
|
||||
created_at:datetime:false:now()
|
||||
updated_at:datetime:false:now()
|
||||
field_name:string:true
|
||||
foreign_key:guid:false:ref:other_table
|
||||
<<<TABLE:another_table>>>
|
||||
id:guid:false
|
||||
name:string:true
|
||||
<<<TABLES_END>>>
|
||||
<<<FILE:index.html>>>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>App Title</title>
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
<script src="/js/vendor/htmx.min.js"></script>
|
||||
<script src="designer.js" defer></script>
|
||||
</head>
|
||||
<body data-app-name="app-name-here">
|
||||
<!-- Complete HTML content here -->
|
||||
</body>
|
||||
</html>
|
||||
<<<FILE:styles.css>>>
|
||||
:root { --primary: #3b82f6; --bg: #0f172a; --text: #f8fafc; }
|
||||
body { margin: 0; font-family: system-ui; background: var(--bg); color: var(--text); }
|
||||
/* Complete CSS content here */
|
||||
<<<FILE:app.js>>>
|
||||
// Complete JavaScript content here
|
||||
<<<FILE:table_name.html>>>
|
||||
<!DOCTYPE html>
|
||||
<!-- Complete list page for table_name -->
|
||||
<<<FILE:table_name_form.html>>>
|
||||
<!DOCTYPE html>
|
||||
<!-- Complete form page for table_name -->
|
||||
<<<TOOL:app_helper.bas>>>
|
||||
HEAR "help", "assist"
|
||||
TALK "I can help you with..."
|
||||
END HEAR
|
||||
<<<SCHEDULER:daily_report.bas>>>
|
||||
SET SCHEDULE "0 9 * * *"
|
||||
data = GET FROM "table"
|
||||
SEND MAIL TO "admin@example.com" WITH SUBJECT "Daily Report" BODY data
|
||||
END SCHEDULE
|
||||
<<<APP_END>>>
|
||||
```
|
||||
|
||||
### Table Field Format
|
||||
|
||||
Each field on its own line: `name:type:nullable[:default][:ref:table]`
|
||||
|
||||
- **Types**: guid, string, text, integer, decimal, boolean, date, datetime, json
|
||||
- **nullable**: true or false
|
||||
- **default**: optional (e.g., now(), 0, '', uuid())
|
||||
- **ref:table**: optional foreign key reference
|
||||
|
||||
### Field Types
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -345,16 +345,21 @@ pub async fn create_and_execute_handler(
|
|||
let task_id_str = task_id.to_string();
|
||||
|
||||
// Spawn background task to do the actual work
|
||||
tokio::spawn(async move {
|
||||
info!("[AUTOTASK] Background task started for task_id={}", task_id_str);
|
||||
let spawn_result = tokio::spawn(async move {
|
||||
info!("[AUTOTASK] *** Background task STARTED for task_id={} ***", task_id_str);
|
||||
|
||||
// Use IntentClassifier to classify and process with task tracking
|
||||
let classifier = IntentClassifier::new(state_clone.clone());
|
||||
|
||||
match classifier
|
||||
info!("[AUTOTASK] Calling classify_and_process_with_task_id for task_id={}", task_id_str);
|
||||
|
||||
let result = classifier
|
||||
.classify_and_process_with_task_id(&intent, &session_clone, Some(task_id_str.clone()))
|
||||
.await
|
||||
{
|
||||
.await;
|
||||
|
||||
info!("[AUTOTASK] classify_and_process_with_task_id returned for task_id={}", task_id_str);
|
||||
|
||||
match result {
|
||||
Ok(result) => {
|
||||
let status = if result.success {
|
||||
"completed"
|
||||
|
|
@ -363,20 +368,22 @@ pub async fn create_and_execute_handler(
|
|||
};
|
||||
let _ = update_task_status_db(&state_clone, task_id, status, result.error.as_deref());
|
||||
info!(
|
||||
"[AUTOTASK] Background task completed: task_id={}, status={}, message={}",
|
||||
"[AUTOTASK] *** Background task COMPLETED: task_id={}, status={}, message={} ***",
|
||||
task_id_str, status, result.message
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
let _ = update_task_status_db(&state_clone, task_id, "failed", Some(&e.to_string()));
|
||||
error!(
|
||||
"[AUTOTASK] Background task failed: task_id={}, error={}",
|
||||
"[AUTOTASK] *** Background task FAILED: task_id={}, error={} ***",
|
||||
task_id_str, e
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
info!("[AUTOTASK] Spawn result: {:?}", spawn_result);
|
||||
|
||||
// Return immediately with task_id - client will poll for status
|
||||
info!("[AUTOTASK] Returning immediately with task_id={}", task_id);
|
||||
(
|
||||
|
|
|
|||
|
|
@ -196,18 +196,45 @@ async fn handle_task_progress_websocket(
|
|||
loop {
|
||||
match broadcast_rx.recv().await {
|
||||
Ok(event) => {
|
||||
let is_manifest = event.step == "manifest_update" || event.event_type == "manifest_update";
|
||||
let should_send = task_filter_clone.is_none()
|
||||
|| task_filter_clone.as_ref() == Some(&event.task_id);
|
||||
|
||||
if is_manifest {
|
||||
info!(
|
||||
"[WS_HANDLER] Received manifest_update event: task={}, should_send={}, filter={:?}",
|
||||
event.task_id, should_send, task_filter_clone
|
||||
);
|
||||
}
|
||||
|
||||
if should_send {
|
||||
if let Ok(json_str) = serde_json::to_string(&event) {
|
||||
debug!(
|
||||
"Sending task progress to WebSocket: {} - {}",
|
||||
event.task_id, event.step
|
||||
);
|
||||
if sender.send(Message::Text(json_str)).await.is_err() {
|
||||
error!("Failed to send task progress to WebSocket");
|
||||
break;
|
||||
match serde_json::to_string(&event) {
|
||||
Ok(json_str) => {
|
||||
if is_manifest {
|
||||
info!(
|
||||
"[WS_HANDLER] Sending manifest_update to WebSocket: {} bytes, task={}",
|
||||
json_str.len(), event.task_id
|
||||
);
|
||||
} else {
|
||||
debug!(
|
||||
"Sending task progress to WebSocket: {} - {}",
|
||||
event.task_id, event.step
|
||||
);
|
||||
}
|
||||
match sender.send(Message::Text(json_str)).await {
|
||||
Ok(()) => {
|
||||
if is_manifest {
|
||||
info!("[WS_HANDLER] manifest_update SENT successfully to WebSocket");
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
error!("[WS_HANDLER] Failed to send to WebSocket: {:?}", e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
error!("[WS_HANDLER] Failed to serialize event: {:?}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -826,7 +826,7 @@ impl ManifestBuilder {
|
|||
self
|
||||
}
|
||||
|
||||
pub fn with_pages(mut self, _pages: Vec<PageDefinition>) -> Self {
|
||||
pub fn with_pages(self, _pages: Vec<PageDefinition>) -> Self {
|
||||
// Pages are now included in Files section as HTML Pages child
|
||||
self
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
use crate::config::ConfigManager;
|
||||
#[cfg(feature = "nvidia")]
|
||||
use crate::nvidia;
|
||||
#[cfg(feature = "nvidia")]
|
||||
use crate::nvidia::get_system_metrics;
|
||||
use crate::shared::models::schema::bots::dsl::*;
|
||||
use crate::shared::state::AppState;
|
||||
|
|
|
|||
256
src/tasks/mod.rs
256
src/tasks/mod.rs
|
|
@ -397,6 +397,8 @@ pub async fn handle_task_get(
|
|||
pub total_steps: i32,
|
||||
#[diesel(sql_type = diesel::sql_types::Nullable<diesel::sql_types::Jsonb>)]
|
||||
pub step_results: Option<serde_json::Value>,
|
||||
#[diesel(sql_type = diesel::sql_types::Nullable<diesel::sql_types::Jsonb>)]
|
||||
pub manifest_json: Option<serde_json::Value>,
|
||||
#[diesel(sql_type = diesel::sql_types::Timestamptz)]
|
||||
pub created_at: chrono::DateTime<chrono::Utc>,
|
||||
#[diesel(sql_type = diesel::sql_types::Nullable<diesel::sql_types::Timestamptz>)]
|
||||
|
|
@ -417,7 +419,7 @@ pub async fn handle_task_get(
|
|||
};
|
||||
|
||||
let task: Option<AutoTaskRow> = diesel::sql_query(
|
||||
"SELECT id, title, status, priority, intent, error, progress, current_step, total_steps, step_results, created_at, started_at, completed_at
|
||||
"SELECT id, title, status, priority, intent, error, progress, current_step, total_steps, step_results, manifest_json, created_at, started_at, completed_at
|
||||
FROM auto_tasks WHERE id = $1 LIMIT 1"
|
||||
)
|
||||
.bind::<diesel::sql_types::Uuid, _>(parsed_uuid)
|
||||
|
|
@ -470,9 +472,7 @@ pub async fn handle_task_get(
|
|||
"failed" | "error" => "error",
|
||||
_ => "pending"
|
||||
};
|
||||
let progress_percent = (task.progress * 100.0) as u8;
|
||||
|
||||
// Calculate runtime
|
||||
let runtime = if let Some(started) = task.started_at {
|
||||
let end_time = task.completed_at.unwrap_or_else(chrono::Utc::now);
|
||||
let duration = end_time.signed_duration_since(started);
|
||||
|
|
@ -488,7 +488,6 @@ pub async fn handle_task_get(
|
|||
};
|
||||
|
||||
let task_id = task.id.to_string();
|
||||
let intent_text = task.intent.clone().unwrap_or_else(|| task.title.clone());
|
||||
let error_html = task.error.clone().map(|e| format!(
|
||||
r#"<div class="error-alert">
|
||||
<span class="error-icon">⚠</span>
|
||||
|
|
@ -535,7 +534,7 @@ pub async fn handle_task_get(
|
|||
),
|
||||
};
|
||||
|
||||
let (status_html, progress_log_html) = build_taskmd_html(&state, &task_id, &task.title, &runtime);
|
||||
let (status_html, progress_log_html) = build_taskmd_html(&state, &task_id, &task.title, &runtime, task.manifest_json.as_ref());
|
||||
|
||||
let html = format!(r#"
|
||||
<div class="task-detail-rich" data-task-id="{task_id}">
|
||||
|
|
@ -552,7 +551,7 @@ pub async fn handle_task_get(
|
|||
{error_html}
|
||||
|
||||
<!-- STATUS Section -->
|
||||
<div class="taskmd-section">
|
||||
<div class="taskmd-section taskmd-section-status">
|
||||
<div class="taskmd-section-header">STATUS</div>
|
||||
<div class="taskmd-status-content">
|
||||
{status_html}
|
||||
|
|
@ -560,7 +559,7 @@ pub async fn handle_task_get(
|
|||
</div>
|
||||
|
||||
<!-- PROGRESS LOG Section -->
|
||||
<div class="taskmd-section">
|
||||
<div class="taskmd-section taskmd-section-progress">
|
||||
<div class="taskmd-section-header">PROGRESS LOG</div>
|
||||
<div class="taskmd-progress-content" id="progress-log-{task_id}">
|
||||
{progress_log_html}
|
||||
|
|
@ -568,7 +567,7 @@ pub async fn handle_task_get(
|
|||
</div>
|
||||
|
||||
<!-- TERMINAL Section -->
|
||||
<div class="taskmd-section taskmd-terminal">
|
||||
<div class="taskmd-section taskmd-section-terminal taskmd-terminal">
|
||||
<div class="taskmd-terminal-header">
|
||||
<div class="taskmd-terminal-title">
|
||||
<span class="terminal-dot {terminal_active}"></span>
|
||||
|
|
@ -702,18 +701,41 @@ fn get_manifest_eta(state: &Arc<AppState>, task_id: &str) -> String {
|
|||
"calculating...".to_string()
|
||||
}
|
||||
|
||||
fn build_taskmd_html(state: &Arc<AppState>, task_id: &str, title: &str, runtime: &str) -> (String, String) {
|
||||
fn build_taskmd_html(state: &Arc<AppState>, task_id: &str, title: &str, runtime: &str, db_manifest: Option<&serde_json::Value>) -> (String, String) {
|
||||
log::info!("[TASKMD_HTML] Building TASK.md view for task_id: {}", task_id);
|
||||
|
||||
// First, try to get manifest from in-memory cache (for active/running tasks)
|
||||
if let Ok(manifests) = state.task_manifests.read() {
|
||||
if let Some(manifest) = manifests.get(task_id) {
|
||||
log::info!("[TASKMD_HTML] Found manifest for task: {} with {} sections", manifest.app_name, manifest.sections.len());
|
||||
log::info!("[TASKMD_HTML] Found manifest in memory for task: {} with {} sections", manifest.app_name, manifest.sections.len());
|
||||
let status_html = build_status_section_html(manifest, title, runtime);
|
||||
let progress_html = build_progress_log_html(manifest);
|
||||
return (status_html, progress_html);
|
||||
}
|
||||
}
|
||||
|
||||
// If not in memory, try to load from database (for completed/historical tasks)
|
||||
if let Some(manifest_json) = db_manifest {
|
||||
log::info!("[TASKMD_HTML] Found manifest in database for task: {}", task_id);
|
||||
if let Ok(manifest) = serde_json::from_value::<TaskManifest>(manifest_json.clone()) {
|
||||
log::info!("[TASKMD_HTML] Parsed DB manifest for task: {} with {} sections", manifest.app_name, manifest.sections.len());
|
||||
let status_html = build_status_section_html(&manifest, title, runtime);
|
||||
let progress_html = build_progress_log_html(&manifest);
|
||||
return (status_html, progress_html);
|
||||
} else {
|
||||
// Try parsing as web JSON format (the format we store)
|
||||
if let Ok(web_manifest) = parse_web_manifest_json(manifest_json) {
|
||||
log::info!("[TASKMD_HTML] Parsed web manifest from DB for task: {}", task_id);
|
||||
let status_html = build_status_section_from_web_json(&web_manifest, title, runtime);
|
||||
let progress_html = build_progress_log_from_web_json(&web_manifest);
|
||||
return (status_html, progress_html);
|
||||
}
|
||||
log::warn!("[TASKMD_HTML] Failed to parse manifest JSON for task: {}", task_id);
|
||||
}
|
||||
}
|
||||
|
||||
log::info!("[TASKMD_HTML] No manifest found for task: {}", task_id);
|
||||
|
||||
let default_status = format!(r#"
|
||||
<div class="status-row">
|
||||
<span class="status-title">{}</span>
|
||||
|
|
@ -724,6 +746,190 @@ fn build_taskmd_html(state: &Arc<AppState>, task_id: &str, title: &str, runtime:
|
|||
(default_status, r#"<div class="progress-empty">No steps executed yet</div>"#.to_string())
|
||||
}
|
||||
|
||||
// Parse the web JSON format that we store in the database
|
||||
fn parse_web_manifest_json(json: &serde_json::Value) -> Result<serde_json::Value, ()> {
|
||||
// The web format has sections with status as strings, etc.
|
||||
if json.get("sections").is_some() {
|
||||
Ok(json.clone())
|
||||
} else {
|
||||
Err(())
|
||||
}
|
||||
}
|
||||
|
||||
fn build_status_section_from_web_json(manifest: &serde_json::Value, title: &str, runtime: &str) -> String {
|
||||
let mut html = String::new();
|
||||
|
||||
let current_action = manifest
|
||||
.get("current_status")
|
||||
.and_then(|s| s.get("current_action"))
|
||||
.and_then(|a| a.as_str())
|
||||
.unwrap_or("Processing...");
|
||||
|
||||
let estimated_seconds = manifest
|
||||
.get("estimated_seconds")
|
||||
.and_then(|e| e.as_u64())
|
||||
.unwrap_or(0);
|
||||
|
||||
let estimated = if estimated_seconds >= 60 {
|
||||
format!("{} min", estimated_seconds / 60)
|
||||
} else {
|
||||
format!("{} sec", estimated_seconds)
|
||||
};
|
||||
|
||||
let runtime_display = if runtime == "0s" || runtime == "calculating..." {
|
||||
"Not started".to_string()
|
||||
} else {
|
||||
runtime.to_string()
|
||||
};
|
||||
|
||||
html.push_str(&format!(r#"
|
||||
<div class="status-row status-main">
|
||||
<span class="status-title">{}</span>
|
||||
<span class="status-time">Runtime: {} <span class="status-indicator"></span></span>
|
||||
</div>
|
||||
<div class="status-row status-current">
|
||||
<span class="status-dot active"></span>
|
||||
<span class="status-text">{}</span>
|
||||
<span class="status-time">Estimated: {} <span class="status-gear">⚙</span></span>
|
||||
</div>
|
||||
"#, title, runtime_display, current_action, estimated));
|
||||
|
||||
html
|
||||
}
|
||||
|
||||
fn build_progress_log_from_web_json(manifest: &serde_json::Value) -> String {
|
||||
let mut html = String::new();
|
||||
html.push_str(r#"<div class="taskmd-tree">"#);
|
||||
|
||||
let total_steps = manifest
|
||||
.get("total_steps")
|
||||
.and_then(|t| t.as_u64())
|
||||
.unwrap_or(60) as u32;
|
||||
|
||||
let sections = match manifest.get("sections").and_then(|s| s.as_array()) {
|
||||
Some(s) => s,
|
||||
None => {
|
||||
html.push_str("</div>");
|
||||
return html;
|
||||
}
|
||||
};
|
||||
|
||||
for section in sections {
|
||||
let section_id = section.get("id").and_then(|i| i.as_str()).unwrap_or("unknown");
|
||||
let section_name = section.get("name").and_then(|n| n.as_str()).unwrap_or("Unknown");
|
||||
let section_status = section.get("status").and_then(|s| s.as_str()).unwrap_or("Pending");
|
||||
|
||||
// Progress fields are nested inside a "progress" object in the web JSON format
|
||||
let progress = section.get("progress");
|
||||
let current_step = progress
|
||||
.and_then(|p| p.get("current"))
|
||||
.and_then(|c| c.as_u64())
|
||||
.unwrap_or(0) as u32;
|
||||
let global_step_start = progress
|
||||
.and_then(|p| p.get("global_start"))
|
||||
.and_then(|g| g.as_u64())
|
||||
.unwrap_or(0) as u32;
|
||||
|
||||
let section_class = match section_status.to_lowercase().as_str() {
|
||||
"completed" => "completed expanded",
|
||||
"running" => "running expanded",
|
||||
"failed" => "failed",
|
||||
"skipped" => "skipped",
|
||||
_ => "pending",
|
||||
};
|
||||
|
||||
let global_current = global_step_start + current_step;
|
||||
|
||||
html.push_str(&format!(r#"
|
||||
<div class="tree-section {}" data-section-id="{}">
|
||||
<div class="tree-row tree-level-0" onclick="this.parentElement.classList.toggle('expanded')">
|
||||
<span class="tree-name">{}</span>
|
||||
<span class="tree-step-badge">Step {}/{}</span>
|
||||
<span class="tree-status {}">{}</span>
|
||||
<span class="tree-section-dot {}"></span>
|
||||
</div>
|
||||
<div class="tree-children">
|
||||
"#, section_class, section_id, section_name, global_current, total_steps, section_class, section_status, section_class));
|
||||
|
||||
// Render children
|
||||
if let Some(children) = section.get("children").and_then(|c| c.as_array()) {
|
||||
for child in children {
|
||||
let child_id = child.get("id").and_then(|i| i.as_str()).unwrap_or("unknown");
|
||||
let child_name = child.get("name").and_then(|n| n.as_str()).unwrap_or("Unknown");
|
||||
let child_status = child.get("status").and_then(|s| s.as_str()).unwrap_or("Pending");
|
||||
|
||||
// Progress fields are nested inside a "progress" object in the web JSON format
|
||||
let child_progress = child.get("progress");
|
||||
let child_current = child_progress
|
||||
.and_then(|p| p.get("current"))
|
||||
.and_then(|c| c.as_u64())
|
||||
.unwrap_or(0) as u32;
|
||||
let child_total = child_progress
|
||||
.and_then(|p| p.get("total"))
|
||||
.and_then(|t| t.as_u64())
|
||||
.unwrap_or(0) as u32;
|
||||
|
||||
let child_class = match child_status.to_lowercase().as_str() {
|
||||
"completed" => "completed expanded",
|
||||
"running" => "running expanded",
|
||||
"failed" => "failed",
|
||||
"skipped" => "skipped",
|
||||
_ => "pending",
|
||||
};
|
||||
|
||||
html.push_str(&format!(r#"
|
||||
<div class="tree-child {}" data-child-id="{}">
|
||||
<div class="tree-row tree-level-1" onclick="this.parentElement.classList.toggle('expanded')">
|
||||
<span class="tree-indent"></span>
|
||||
<span class="tree-name">{}</span>
|
||||
<span class="tree-step-badge">Step {}/{}</span>
|
||||
<span class="tree-status {}">{}</span>
|
||||
</div>
|
||||
<div class="tree-items">
|
||||
"#, child_class, child_id, child_name, child_current, child_total, child_class, child_status));
|
||||
|
||||
// Render items
|
||||
if let Some(items) = child.get("items").and_then(|i| i.as_array()) {
|
||||
for item in items {
|
||||
let item_name = item.get("name").and_then(|n| n.as_str()).unwrap_or("Unknown");
|
||||
let item_status = item.get("status").and_then(|s| s.as_str()).unwrap_or("Pending");
|
||||
let duration = item.get("duration_seconds").and_then(|d| d.as_u64());
|
||||
|
||||
let item_class = match item_status.to_lowercase().as_str() {
|
||||
"completed" => "completed",
|
||||
"running" => "running",
|
||||
_ => "pending",
|
||||
};
|
||||
|
||||
let check_mark = if item_status.to_lowercase() == "completed" { "✓" } else { "" };
|
||||
let duration_str = duration
|
||||
.map(|s| if s >= 60 { format!("Duration: {} min", s / 60) } else { format!("Duration: {} sec", s) })
|
||||
.unwrap_or_default();
|
||||
|
||||
html.push_str(&format!(r#"
|
||||
<div class="tree-item {}">
|
||||
<span class="item-dot {}"></span>
|
||||
<span class="item-name">{}</span>
|
||||
<div class="item-info">
|
||||
<span class="item-duration">{}</span>
|
||||
<span class="item-check {}">{}</span>
|
||||
</div>
|
||||
</div>
|
||||
"#, item_class, item_class, item_name, duration_str, item_class, check_mark));
|
||||
}
|
||||
}
|
||||
|
||||
html.push_str("</div></div>"); // Close tree-items and tree-child
|
||||
}
|
||||
}
|
||||
|
||||
html.push_str("</div></div>"); // Close tree-children and tree-section
|
||||
}
|
||||
|
||||
html.push_str("</div>"); // Close taskmd-tree
|
||||
html
|
||||
}
|
||||
|
||||
fn build_status_section_html(manifest: &TaskManifest, title: &str, runtime: &str) -> String {
|
||||
let mut html = String::new();
|
||||
|
||||
|
|
@ -774,9 +980,13 @@ fn build_progress_log_html(manifest: &TaskManifest) -> String {
|
|||
|
||||
let total_steps = manifest.total_steps;
|
||||
|
||||
log::info!("[PROGRESS_HTML] Building progress log, {} sections, total_steps={}", manifest.sections.len(), total_steps);
|
||||
|
||||
for section in &manifest.sections {
|
||||
log::info!("[PROGRESS_HTML] Section '{}': children={}, items={}, item_groups={}",
|
||||
section.name, section.children.len(), section.items.len(), section.item_groups.len());
|
||||
let section_class = match section.status {
|
||||
crate::auto_task::SectionStatus::Completed => "completed",
|
||||
crate::auto_task::SectionStatus::Completed => "completed expanded",
|
||||
crate::auto_task::SectionStatus::Running => "running expanded",
|
||||
crate::auto_task::SectionStatus::Failed => "failed",
|
||||
crate::auto_task::SectionStatus::Skipped => "skipped",
|
||||
|
|
@ -806,8 +1016,10 @@ fn build_progress_log_html(manifest: &TaskManifest) -> String {
|
|||
"#, section_class, section.id, section.name, global_current, total_steps, section_class, status_text, section_class));
|
||||
|
||||
for child in §ion.children {
|
||||
log::info!("[PROGRESS_HTML] Child '{}': items={}, item_groups={}",
|
||||
child.name, child.items.len(), child.item_groups.len());
|
||||
let child_class = match child.status {
|
||||
crate::auto_task::SectionStatus::Completed => "completed",
|
||||
crate::auto_task::SectionStatus::Completed => "completed expanded",
|
||||
crate::auto_task::SectionStatus::Running => "running expanded",
|
||||
crate::auto_task::SectionStatus::Failed => "failed",
|
||||
crate::auto_task::SectionStatus::Skipped => "skipped",
|
||||
|
|
@ -823,7 +1035,7 @@ fn build_progress_log_html(manifest: &TaskManifest) -> String {
|
|||
};
|
||||
|
||||
html.push_str(&format!(r#"
|
||||
<div class="tree-child {}" onclick="this.classList.toggle('expanded')">
|
||||
<div class="tree-child {}" data-child-id="{}" onclick="this.classList.toggle('expanded')">
|
||||
<div class="tree-row tree-level-1">
|
||||
<span class="tree-indent"></span>
|
||||
<span class="tree-name">{}</span>
|
||||
|
|
@ -831,7 +1043,7 @@ fn build_progress_log_html(manifest: &TaskManifest) -> String {
|
|||
<span class="tree-status {}">{}</span>
|
||||
</div>
|
||||
<div class="tree-items">
|
||||
"#, child_class, child.name, child.current_step, child.total_steps, child_class, child_status));
|
||||
"#, child_class, child.id, child.name, child.current_step, child.total_steps, child_class, child_status));
|
||||
|
||||
// Render item groups first (grouped fields like "email, password_hash, email_verified")
|
||||
for group in &child.item_groups {
|
||||
|
|
@ -849,13 +1061,13 @@ fn build_progress_log_html(manifest: &TaskManifest) -> String {
|
|||
let group_name = group.display_name();
|
||||
|
||||
html.push_str(&format!(r#"
|
||||
<div class="tree-item {}">
|
||||
<div class="tree-item {}" data-item-id="{}">
|
||||
<span class="tree-item-dot {}"></span>
|
||||
<span class="tree-item-name">{}</span>
|
||||
<span class="tree-item-duration">{}</span>
|
||||
<span class="tree-item-check {}">{}</span>
|
||||
</div>
|
||||
"#, group_class, group_class, group_name, group_duration, group_class, check_mark));
|
||||
"#, group_class, group.id, group_class, group_name, group_duration, group_class, check_mark));
|
||||
}
|
||||
|
||||
// Then individual items
|
||||
|
|
@ -872,13 +1084,13 @@ fn build_progress_log_html(manifest: &TaskManifest) -> String {
|
|||
.unwrap_or_default();
|
||||
|
||||
html.push_str(&format!(r#"
|
||||
<div class="tree-item {}">
|
||||
<div class="tree-item {}" data-item-id="{}">
|
||||
<span class="tree-item-dot {}"></span>
|
||||
<span class="tree-item-name">{}</span>
|
||||
<span class="tree-item-duration">{}</span>
|
||||
<span class="tree-item-check {}">{}</span>
|
||||
</div>
|
||||
"#, item_class, item_class, item.name, item_duration, item_class, check_mark));
|
||||
"#, item_class, item.id, item_class, item.name, item_duration, item_class, check_mark));
|
||||
}
|
||||
|
||||
html.push_str("</div></div>");
|
||||
|
|
@ -900,13 +1112,13 @@ fn build_progress_log_html(manifest: &TaskManifest) -> String {
|
|||
let group_name = group.display_name();
|
||||
|
||||
html.push_str(&format!(r#"
|
||||
<div class="tree-item {}">
|
||||
<div class="tree-item {}" data-item-id="{}">
|
||||
<span class="tree-item-dot {}"></span>
|
||||
<span class="tree-item-name">{}</span>
|
||||
<span class="tree-item-duration">{}</span>
|
||||
<span class="tree-item-check {}">{}</span>
|
||||
</div>
|
||||
"#, group_class, group_class, group_name, group_duration, group_class, check_mark));
|
||||
"#, group_class, group.id, group_class, group_name, group_duration, group_class, check_mark));
|
||||
}
|
||||
|
||||
// Render section-level items
|
||||
|
|
@ -923,13 +1135,13 @@ fn build_progress_log_html(manifest: &TaskManifest) -> String {
|
|||
.unwrap_or_default();
|
||||
|
||||
html.push_str(&format!(r#"
|
||||
<div class="tree-item {}">
|
||||
<div class="tree-item {}" data-item-id="{}">
|
||||
<span class="tree-item-dot {}"></span>
|
||||
<span class="tree-item-name">{}</span>
|
||||
<span class="tree-item-duration">{}</span>
|
||||
<span class="tree-item-check {}">{}</span>
|
||||
</div>
|
||||
"#, item_class, item_class, item.name, item_duration, item_class, check_mark));
|
||||
"#, item_class, item.id, item_class, item.name, item_duration, item_class, check_mark));
|
||||
}
|
||||
|
||||
html.push_str("</div></div>");
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue