From a7e237e2ff0c676b6fc2bd5e4441507315ff8b1d Mon Sep 17 00:00:00 2001 From: Christoffer Martinsson Date: Sat, 25 Oct 2025 11:06:36 +0200 Subject: [PATCH] Fix rebuild indicator with proper timeout and completion detection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add automatic timeout mechanism (5 minutes for rebuilds, 30 seconds for services) - Implement agent hash change detection for rebuild completion - Add visual feedback states: blue ↻ (in progress), green ✓ (success), red ✗ (failed) - Clear status automatically after timeout or completion - Fix command status lifecycle management --- dashboard/src/ui/mod.rs | 111 ++++++++++++++++++++++++----- dashboard/src/ui/widgets/system.rs | 5 ++ 2 files changed, 99 insertions(+), 17 deletions(-) diff --git a/dashboard/src/ui/mod.rs b/dashboard/src/ui/mod.rs index d56c038..e989aa9 100644 --- a/dashboard/src/ui/mod.rs +++ b/dashboard/src/ui/mod.rs @@ -7,7 +7,7 @@ use ratatui::{ Frame, }; use std::collections::HashMap; -use std::time::Instant; +use std::time::{Duration, Instant}; use tracing::info; pub mod theme; @@ -34,9 +34,9 @@ pub enum CommandStatus { /// Command is executing InProgress { command_type: CommandType, target: String, start_time: std::time::Instant }, /// Command completed successfully - Success { command_type: CommandType, target: String, duration: std::time::Duration }, + Success { command_type: CommandType, target: String, duration: std::time::Duration, completed_at: std::time::Instant }, /// Command failed - Failed { command_type: CommandType, target: String, error: String }, + Failed { command_type: CommandType, target: String, error: String, failed_at: std::time::Instant }, } /// Types of commands for status tracking @@ -156,6 +156,12 @@ impl TuiApp { /// Update widgets with metrics from store (only for current host) pub fn update_metrics(&mut self, metric_store: &MetricStore) { + // Check for command timeouts first + self.check_command_timeouts(); + + // Check for rebuild completion by agent hash change + self.check_rebuild_completion(metric_store); + if let Some(hostname) = self.current_host.clone() { // Only update widgets if we have metrics for this host let all_metrics = metric_store.get_metrics_for_host(&hostname); @@ -491,10 +497,8 @@ impl TuiApp { command_type: command_type.clone(), target: target.clone(), duration, + completed_at: Instant::now(), }); - - // Clear success status after 3 seconds - // TODO: Implement timer to clear this } } } @@ -507,14 +511,76 @@ impl TuiApp { command_type: command_type.clone(), target: target.clone(), error, + failed_at: Instant::now(), }); - - // Clear error status after 5 seconds - // TODO: Implement timer to clear this } } } + /// Check for command timeouts and automatically clear them + pub fn check_command_timeouts(&mut self) { + let now = Instant::now(); + let mut hosts_to_clear = Vec::new(); + + for (hostname, host_widgets) in &self.host_widgets { + if let Some(CommandStatus::InProgress { command_type, start_time, .. }) = &host_widgets.command_status { + let timeout_duration = match command_type { + CommandType::SystemRebuild => Duration::from_secs(300), // 5 minutes for rebuilds + _ => Duration::from_secs(30), // 30 seconds for service commands + }; + + if now.duration_since(*start_time) > timeout_duration { + hosts_to_clear.push(hostname.clone()); + } + } + // Also clear success/failed status after display time + else if let Some(CommandStatus::Success { completed_at, .. }) = &host_widgets.command_status { + if now.duration_since(*completed_at) > Duration::from_secs(3) { + hosts_to_clear.push(hostname.clone()); + } + } + else if let Some(CommandStatus::Failed { failed_at, .. }) = &host_widgets.command_status { + if now.duration_since(*failed_at) > Duration::from_secs(5) { + hosts_to_clear.push(hostname.clone()); + } + } + } + + // Clear timed out commands + for hostname in hosts_to_clear { + if let Some(host_widgets) = self.host_widgets.get_mut(&hostname) { + host_widgets.command_status = None; + } + } + } + + /// Check for rebuild completion by detecting agent hash changes + pub fn check_rebuild_completion(&mut self, metric_store: &MetricStore) { + let mut hosts_to_complete = Vec::new(); + + for (hostname, host_widgets) in &self.host_widgets { + if let Some(CommandStatus::InProgress { command_type: CommandType::SystemRebuild, .. }) = &host_widgets.command_status { + // Check if agent hash has changed (indicating successful rebuild) + if let Some(agent_hash_metric) = metric_store.get_metric(hostname, "system_agent_hash") { + if let cm_dashboard_shared::MetricValue::String(current_hash) = &agent_hash_metric.value { + // Compare with stored hash (if we have one) + if let Some(stored_hash) = host_widgets.system_widget.get_agent_hash() { + if current_hash != stored_hash { + // Agent hash changed - rebuild completed successfully + hosts_to_complete.push(hostname.clone()); + } + } + } + } + } + } + + // Mark rebuilds as completed + for hostname in hosts_to_complete { + self.complete_command(&hostname); + } + } + /// Scroll the focused panel up or down pub fn scroll_focused_panel(&mut self, direction: i32) { if let Some(hostname) = self.current_host.clone() { @@ -682,15 +748,26 @@ impl TuiApp { spans.push(Span::styled(" ", Typography::title())); } - // Check if this host has a SystemRebuild command in progress + // Check if this host has a command status that affects the icon let (status_icon, status_color) = if let Some(host_widgets) = self.host_widgets.get(host) { - if let Some(CommandStatus::InProgress { command_type: CommandType::SystemRebuild, .. }) = &host_widgets.command_status { - // Show blue circular arrow during rebuild - ("↻", Theme::highlight()) - } else { - // Normal status icon based on metrics - let host_status = self.calculate_host_status(host, metric_store); - (StatusIcons::get_icon(host_status), Theme::status_color(host_status)) + match &host_widgets.command_status { + Some(CommandStatus::InProgress { command_type: CommandType::SystemRebuild, .. }) => { + // Show blue circular arrow during rebuild + ("↻", Theme::highlight()) + } + Some(CommandStatus::Success { command_type: CommandType::SystemRebuild, .. }) => { + // Show green checkmark for successful rebuild + ("✓", Theme::success()) + } + Some(CommandStatus::Failed { command_type: CommandType::SystemRebuild, .. }) => { + // Show red X for failed rebuild + ("✗", Theme::error()) + } + _ => { + // Normal status icon based on metrics + let host_status = self.calculate_host_status(host, metric_store); + (StatusIcons::get_icon(host_status), Theme::status_color(host_status)) + } } } else { // No host widgets yet, use normal status diff --git a/dashboard/src/ui/widgets/system.rs b/dashboard/src/ui/widgets/system.rs index 0216998..4551cde 100644 --- a/dashboard/src/ui/widgets/system.rs +++ b/dashboard/src/ui/widgets/system.rs @@ -128,6 +128,11 @@ impl SystemWidget { } } + /// Get the current agent hash for rebuild completion detection + pub fn get_agent_hash(&self) -> Option<&String> { + self.agent_hash.as_ref() + } + /// Get mount point for a pool name fn get_mount_point_for_pool(&self, pool_name: &str) -> String { match pool_name {