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,
|
total_steps INTEGER DEFAULT 0,
|
||||||
progress FLOAT DEFAULT 0.0,
|
progress FLOAT DEFAULT 0.0,
|
||||||
step_results JSONB DEFAULT '[]'::jsonb,
|
step_results JSONB DEFAULT '[]'::jsonb,
|
||||||
|
manifest_json JSONB,
|
||||||
error TEXT,
|
error TEXT,
|
||||||
started_at TIMESTAMPTZ,
|
started_at TIMESTAMPTZ,
|
||||||
completed_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
|
### 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();
|
let task_id_str = task_id.to_string();
|
||||||
|
|
||||||
// Spawn background task to do the actual work
|
// Spawn background task to do the actual work
|
||||||
tokio::spawn(async move {
|
let spawn_result = tokio::spawn(async move {
|
||||||
info!("[AUTOTASK] Background task started for task_id={}", task_id_str);
|
info!("[AUTOTASK] *** Background task STARTED for task_id={} ***", task_id_str);
|
||||||
|
|
||||||
// Use IntentClassifier to classify and process with task tracking
|
// Use IntentClassifier to classify and process with task tracking
|
||||||
let classifier = IntentClassifier::new(state_clone.clone());
|
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()))
|
.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) => {
|
Ok(result) => {
|
||||||
let status = if result.success {
|
let status = if result.success {
|
||||||
"completed"
|
"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());
|
let _ = update_task_status_db(&state_clone, task_id, status, result.error.as_deref());
|
||||||
info!(
|
info!(
|
||||||
"[AUTOTASK] Background task completed: task_id={}, status={}, message={}",
|
"[AUTOTASK] *** Background task COMPLETED: task_id={}, status={}, message={} ***",
|
||||||
task_id_str, status, result.message
|
task_id_str, status, result.message
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
let _ = update_task_status_db(&state_clone, task_id, "failed", Some(&e.to_string()));
|
let _ = update_task_status_db(&state_clone, task_id, "failed", Some(&e.to_string()));
|
||||||
error!(
|
error!(
|
||||||
"[AUTOTASK] Background task failed: task_id={}, error={}",
|
"[AUTOTASK] *** Background task FAILED: task_id={}, error={} ***",
|
||||||
task_id_str, e
|
task_id_str, e
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
info!("[AUTOTASK] Spawn result: {:?}", spawn_result);
|
||||||
|
|
||||||
// Return immediately with task_id - client will poll for status
|
// Return immediately with task_id - client will poll for status
|
||||||
info!("[AUTOTASK] Returning immediately with task_id={}", task_id);
|
info!("[AUTOTASK] Returning immediately with task_id={}", task_id);
|
||||||
(
|
(
|
||||||
|
|
|
||||||
|
|
@ -196,21 +196,48 @@ async fn handle_task_progress_websocket(
|
||||||
loop {
|
loop {
|
||||||
match broadcast_rx.recv().await {
|
match broadcast_rx.recv().await {
|
||||||
Ok(event) => {
|
Ok(event) => {
|
||||||
|
let is_manifest = event.step == "manifest_update" || event.event_type == "manifest_update";
|
||||||
let should_send = task_filter_clone.is_none()
|
let should_send = task_filter_clone.is_none()
|
||||||
|| task_filter_clone.as_ref() == Some(&event.task_id);
|
|| 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 should_send {
|
||||||
if let Ok(json_str) = serde_json::to_string(&event) {
|
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!(
|
debug!(
|
||||||
"Sending task progress to WebSocket: {} - {}",
|
"Sending task progress to WebSocket: {} - {}",
|
||||||
event.task_id, event.step
|
event.task_id, event.step
|
||||||
);
|
);
|
||||||
if sender.send(Message::Text(json_str)).await.is_err() {
|
}
|
||||||
error!("Failed to send task progress to WebSocket");
|
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;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Err(e) => {
|
||||||
|
error!("[WS_HANDLER] Failed to serialize event: {:?}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Err(tokio::sync::broadcast::error::RecvError::Lagged(n)) => {
|
Err(tokio::sync::broadcast::error::RecvError::Lagged(n)) => {
|
||||||
warn!("Task progress WebSocket lagged by {} messages", n);
|
warn!("Task progress WebSocket lagged by {} messages", n);
|
||||||
|
|
|
||||||
|
|
@ -826,7 +826,7 @@ impl ManifestBuilder {
|
||||||
self
|
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
|
// Pages are now included in Files section as HTML Pages child
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,5 @@
|
||||||
use crate::config::ConfigManager;
|
use crate::config::ConfigManager;
|
||||||
#[cfg(feature = "nvidia")]
|
#[cfg(feature = "nvidia")]
|
||||||
use crate::nvidia;
|
|
||||||
#[cfg(feature = "nvidia")]
|
|
||||||
use crate::nvidia::get_system_metrics;
|
use crate::nvidia::get_system_metrics;
|
||||||
use crate::shared::models::schema::bots::dsl::*;
|
use crate::shared::models::schema::bots::dsl::*;
|
||||||
use crate::shared::state::AppState;
|
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,
|
pub total_steps: i32,
|
||||||
#[diesel(sql_type = diesel::sql_types::Nullable<diesel::sql_types::Jsonb>)]
|
#[diesel(sql_type = diesel::sql_types::Nullable<diesel::sql_types::Jsonb>)]
|
||||||
pub step_results: Option<serde_json::Value>,
|
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)]
|
#[diesel(sql_type = diesel::sql_types::Timestamptz)]
|
||||||
pub created_at: chrono::DateTime<chrono::Utc>,
|
pub created_at: chrono::DateTime<chrono::Utc>,
|
||||||
#[diesel(sql_type = diesel::sql_types::Nullable<diesel::sql_types::Timestamptz>)]
|
#[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(
|
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"
|
FROM auto_tasks WHERE id = $1 LIMIT 1"
|
||||||
)
|
)
|
||||||
.bind::<diesel::sql_types::Uuid, _>(parsed_uuid)
|
.bind::<diesel::sql_types::Uuid, _>(parsed_uuid)
|
||||||
|
|
@ -470,9 +472,7 @@ pub async fn handle_task_get(
|
||||||
"failed" | "error" => "error",
|
"failed" | "error" => "error",
|
||||||
_ => "pending"
|
_ => "pending"
|
||||||
};
|
};
|
||||||
let progress_percent = (task.progress * 100.0) as u8;
|
|
||||||
|
|
||||||
// Calculate runtime
|
|
||||||
let runtime = if let Some(started) = task.started_at {
|
let runtime = if let Some(started) = task.started_at {
|
||||||
let end_time = task.completed_at.unwrap_or_else(chrono::Utc::now);
|
let end_time = task.completed_at.unwrap_or_else(chrono::Utc::now);
|
||||||
let duration = end_time.signed_duration_since(started);
|
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 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!(
|
let error_html = task.error.clone().map(|e| format!(
|
||||||
r#"<div class="error-alert">
|
r#"<div class="error-alert">
|
||||||
<span class="error-icon">⚠</span>
|
<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#"
|
let html = format!(r#"
|
||||||
<div class="task-detail-rich" data-task-id="{task_id}">
|
<div class="task-detail-rich" data-task-id="{task_id}">
|
||||||
|
|
@ -552,7 +551,7 @@ pub async fn handle_task_get(
|
||||||
{error_html}
|
{error_html}
|
||||||
|
|
||||||
<!-- STATUS Section -->
|
<!-- STATUS Section -->
|
||||||
<div class="taskmd-section">
|
<div class="taskmd-section taskmd-section-status">
|
||||||
<div class="taskmd-section-header">STATUS</div>
|
<div class="taskmd-section-header">STATUS</div>
|
||||||
<div class="taskmd-status-content">
|
<div class="taskmd-status-content">
|
||||||
{status_html}
|
{status_html}
|
||||||
|
|
@ -560,7 +559,7 @@ pub async fn handle_task_get(
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- PROGRESS LOG Section -->
|
<!-- 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-section-header">PROGRESS LOG</div>
|
||||||
<div class="taskmd-progress-content" id="progress-log-{task_id}">
|
<div class="taskmd-progress-content" id="progress-log-{task_id}">
|
||||||
{progress_log_html}
|
{progress_log_html}
|
||||||
|
|
@ -568,7 +567,7 @@ pub async fn handle_task_get(
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- TERMINAL Section -->
|
<!-- 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-header">
|
||||||
<div class="taskmd-terminal-title">
|
<div class="taskmd-terminal-title">
|
||||||
<span class="terminal-dot {terminal_active}"></span>
|
<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()
|
"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);
|
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 Ok(manifests) = state.task_manifests.read() {
|
||||||
if let Some(manifest) = manifests.get(task_id) {
|
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 status_html = build_status_section_html(manifest, title, runtime);
|
||||||
let progress_html = build_progress_log_html(manifest);
|
let progress_html = build_progress_log_html(manifest);
|
||||||
return (status_html, progress_html);
|
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#"
|
let default_status = format!(r#"
|
||||||
<div class="status-row">
|
<div class="status-row">
|
||||||
<span class="status-title">{}</span>
|
<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())
|
(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 {
|
fn build_status_section_html(manifest: &TaskManifest, title: &str, runtime: &str) -> String {
|
||||||
let mut html = String::new();
|
let mut html = String::new();
|
||||||
|
|
||||||
|
|
@ -774,9 +980,13 @@ fn build_progress_log_html(manifest: &TaskManifest) -> String {
|
||||||
|
|
||||||
let total_steps = manifest.total_steps;
|
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 {
|
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 {
|
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::Running => "running expanded",
|
||||||
crate::auto_task::SectionStatus::Failed => "failed",
|
crate::auto_task::SectionStatus::Failed => "failed",
|
||||||
crate::auto_task::SectionStatus::Skipped => "skipped",
|
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));
|
"#, section_class, section.id, section.name, global_current, total_steps, section_class, status_text, section_class));
|
||||||
|
|
||||||
for child in §ion.children {
|
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 {
|
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::Running => "running expanded",
|
||||||
crate::auto_task::SectionStatus::Failed => "failed",
|
crate::auto_task::SectionStatus::Failed => "failed",
|
||||||
crate::auto_task::SectionStatus::Skipped => "skipped",
|
crate::auto_task::SectionStatus::Skipped => "skipped",
|
||||||
|
|
@ -823,7 +1035,7 @@ fn build_progress_log_html(manifest: &TaskManifest) -> String {
|
||||||
};
|
};
|
||||||
|
|
||||||
html.push_str(&format!(r#"
|
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">
|
<div class="tree-row tree-level-1">
|
||||||
<span class="tree-indent"></span>
|
<span class="tree-indent"></span>
|
||||||
<span class="tree-name">{}</span>
|
<span class="tree-name">{}</span>
|
||||||
|
|
@ -831,7 +1043,7 @@ fn build_progress_log_html(manifest: &TaskManifest) -> String {
|
||||||
<span class="tree-status {}">{}</span>
|
<span class="tree-status {}">{}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="tree-items">
|
<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")
|
// Render item groups first (grouped fields like "email, password_hash, email_verified")
|
||||||
for group in &child.item_groups {
|
for group in &child.item_groups {
|
||||||
|
|
@ -849,13 +1061,13 @@ fn build_progress_log_html(manifest: &TaskManifest) -> String {
|
||||||
let group_name = group.display_name();
|
let group_name = group.display_name();
|
||||||
|
|
||||||
html.push_str(&format!(r#"
|
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-dot {}"></span>
|
||||||
<span class="tree-item-name">{}</span>
|
<span class="tree-item-name">{}</span>
|
||||||
<span class="tree-item-duration">{}</span>
|
<span class="tree-item-duration">{}</span>
|
||||||
<span class="tree-item-check {}">{}</span>
|
<span class="tree-item-check {}">{}</span>
|
||||||
</div>
|
</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
|
// Then individual items
|
||||||
|
|
@ -872,13 +1084,13 @@ fn build_progress_log_html(manifest: &TaskManifest) -> String {
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
html.push_str(&format!(r#"
|
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-dot {}"></span>
|
||||||
<span class="tree-item-name">{}</span>
|
<span class="tree-item-name">{}</span>
|
||||||
<span class="tree-item-duration">{}</span>
|
<span class="tree-item-duration">{}</span>
|
||||||
<span class="tree-item-check {}">{}</span>
|
<span class="tree-item-check {}">{}</span>
|
||||||
</div>
|
</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>");
|
html.push_str("</div></div>");
|
||||||
|
|
@ -900,13 +1112,13 @@ fn build_progress_log_html(manifest: &TaskManifest) -> String {
|
||||||
let group_name = group.display_name();
|
let group_name = group.display_name();
|
||||||
|
|
||||||
html.push_str(&format!(r#"
|
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-dot {}"></span>
|
||||||
<span class="tree-item-name">{}</span>
|
<span class="tree-item-name">{}</span>
|
||||||
<span class="tree-item-duration">{}</span>
|
<span class="tree-item-duration">{}</span>
|
||||||
<span class="tree-item-check {}">{}</span>
|
<span class="tree-item-check {}">{}</span>
|
||||||
</div>
|
</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
|
// Render section-level items
|
||||||
|
|
@ -923,13 +1135,13 @@ fn build_progress_log_html(manifest: &TaskManifest) -> String {
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
html.push_str(&format!(r#"
|
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-dot {}"></span>
|
||||||
<span class="tree-item-name">{}</span>
|
<span class="tree-item-name">{}</span>
|
||||||
<span class="tree-item-duration">{}</span>
|
<span class="tree-item-duration">{}</span>
|
||||||
<span class="tree-item-check {}">{}</span>
|
<span class="tree-item-check {}">{}</span>
|
||||||
</div>
|
</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>");
|
html.push_str("</div></div>");
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue