diff --git a/Cargo.lock b/Cargo.lock index bd3f6ae..64d7d1a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -279,7 +279,7 @@ checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" [[package]] name = "cm-dashboard" -version = "0.1.95" +version = "0.1.96" dependencies = [ "anyhow", "chrono", @@ -301,7 +301,7 @@ dependencies = [ [[package]] name = "cm-dashboard-agent" -version = "0.1.95" +version = "0.1.96" dependencies = [ "anyhow", "async-trait", @@ -324,7 +324,7 @@ dependencies = [ [[package]] name = "cm-dashboard-shared" -version = "0.1.95" +version = "0.1.96" dependencies = [ "chrono", "serde", diff --git a/agent/Cargo.toml b/agent/Cargo.toml index 001c698..33128d5 100644 --- a/agent/Cargo.toml +++ b/agent/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cm-dashboard-agent" -version = "0.1.96" +version = "0.1.97" edition = "2021" [dependencies] diff --git a/agent/src/collectors/backup.rs b/agent/src/collectors/backup.rs index babea7a..5283f47 100644 --- a/agent/src/collectors/backup.rs +++ b/agent/src/collectors/backup.rs @@ -25,6 +25,25 @@ impl BackupCollector { } async fn read_backup_status(&self) -> Result, CollectorError> { + // Check if we're in maintenance mode + if std::fs::metadata("/tmp/cm-maintenance").is_ok() { + // Return special maintenance mode status + let maintenance_status = BackupStatusToml { + backup_name: "maintenance".to_string(), + start_time: chrono::Utc::now().format("%Y-%m-%d %H:%M:%S UTC").to_string(), + current_time: chrono::Utc::now().format("%Y-%m-%d %H:%M:%S UTC").to_string(), + duration_seconds: 0, + status: "pending".to_string(), + last_updated: chrono::Utc::now().format("%Y-%m-%d %H:%M:%S UTC").to_string(), + disk_space: None, + disk_product_name: None, + disk_serial_number: None, + disk_wear_percent: None, + services: HashMap::new(), + }; + return Ok(Some(maintenance_status)); + } + // Check if backup status file exists if !std::path::Path::new(&self.backup_status_file).exists() { return Ok(None); // File doesn't exist, but this is not an error @@ -79,7 +98,9 @@ impl BackupCollector { } } "failed" => Status::Critical, + "warning" => Status::Warning, // Backup completed with warnings "running" => Status::Ok, // Currently running is OK + "pending" => Status::Pending, // Maintenance mode or backup starting _ => Status::Unknown, } } @@ -379,6 +400,25 @@ impl Collector for BackupCollector { }); } + if let Some(wear_percent) = backup_status.disk_wear_percent { + let wear_status = if wear_percent >= 90.0 { + Status::Critical + } else if wear_percent >= 75.0 { + Status::Warning + } else { + Status::Ok + }; + + metrics.push(Metric { + name: "backup_disk_wear_percent".to_string(), + value: MetricValue::Float(wear_percent), + status: wear_status, + timestamp, + description: Some("Backup disk wear percentage from SMART data".to_string()), + unit: Some("percent".to_string()), + }); + } + // Count services by status let mut status_counts = HashMap::new(); for service in backup_status.services.values() { @@ -412,6 +452,7 @@ pub struct BackupStatusToml { pub disk_space: Option, pub disk_product_name: Option, pub disk_serial_number: Option, + pub disk_wear_percent: Option, pub services: HashMap, } diff --git a/dashboard/Cargo.toml b/dashboard/Cargo.toml index 880ce20..689f51b 100644 --- a/dashboard/Cargo.toml +++ b/dashboard/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cm-dashboard" -version = "0.1.96" +version = "0.1.97" edition = "2021" [dependencies] diff --git a/dashboard/src/ui/mod.rs b/dashboard/src/ui/mod.rs index df2bd68..c7b3b21 100644 --- a/dashboard/src/ui/mod.rs +++ b/dashboard/src/ui/mod.rs @@ -678,7 +678,7 @@ impl TuiApp { Status::Pending => ok_count += 1, // Treat pending as OK for aggregation Status::Ok => ok_count += 1, Status::Inactive => ok_count += 1, // Treat inactive as OK for aggregation - Status::Unknown => {}, // Ignore unknown for aggregation + Status::Unknown => ok_count += 1, // Treat unknown as OK for aggregation Status::Offline => {}, // Ignore offline for aggregation } } diff --git a/dashboard/src/ui/widgets/backup.rs b/dashboard/src/ui/widgets/backup.rs index e50fa06..f3a8dfb 100644 --- a/dashboard/src/ui/widgets/backup.rs +++ b/dashboard/src/ui/widgets/backup.rs @@ -30,6 +30,8 @@ pub struct BackupWidget { backup_disk_product_name: Option, /// Backup disk serial number from SMART data backup_disk_serial_number: Option, + /// Backup disk wear percentage from SMART data + backup_disk_wear_percent: Option, /// Backup disk filesystem label backup_disk_filesystem_label: Option, /// Number of completed services @@ -65,6 +67,7 @@ impl BackupWidget { backup_disk_used_gb: None, backup_disk_product_name: None, backup_disk_serial_number: None, + backup_disk_wear_percent: None, backup_disk_filesystem_label: None, services_completed_count: None, services_failed_count: None, @@ -197,6 +200,9 @@ impl Widget for BackupWidget { "backup_disk_serial_number" => { self.backup_disk_serial_number = Some(metric.value.as_string()); } + "backup_disk_wear_percent" => { + self.backup_disk_wear_percent = metric.value.as_f32(); + } "backup_disk_filesystem_label" => { self.backup_disk_filesystem_label = Some(metric.value.as_string()); } @@ -328,21 +334,31 @@ impl BackupWidget { ); lines.push(ratatui::text::Line::from(disk_spans)); - // Serial number as sub-item + // Collect sub-items to determine tree structure + let mut sub_items = Vec::new(); + if let Some(serial) = &self.backup_disk_serial_number { - lines.push(ratatui::text::Line::from(vec![ - ratatui::text::Span::styled(" ├─ ", Typography::tree()), - ratatui::text::Span::styled(format!("S/N: {}", serial), Typography::secondary()) - ])); + sub_items.push(format!("S/N: {}", serial)); } - - // Usage as sub-item + + if let Some(wear) = self.backup_disk_wear_percent { + sub_items.push(format!("Wear: {:.0}%", wear)); + } + if let (Some(used), Some(total)) = (self.backup_disk_used_gb, self.backup_disk_total_gb) { let used_str = Self::format_size_with_proper_units(used); let total_str = Self::format_size_with_proper_units(total); + sub_items.push(format!("Usage: {}/{}", used_str, total_str)); + } + + // Render sub-items with proper tree structure + let num_items = sub_items.len(); + for (i, item) in sub_items.into_iter().enumerate() { + let is_last = i == num_items - 1; + let tree_char = if is_last { " └─ " } else { " ├─ " }; lines.push(ratatui::text::Line::from(vec![ - ratatui::text::Span::styled(" └─ ", Typography::tree()), - ratatui::text::Span::styled(format!("Usage: {}/{}", used_str, total_str), Typography::secondary()) + ratatui::text::Span::styled(tree_char, Typography::tree()), + ratatui::text::Span::styled(item, Typography::secondary()) ])); } } diff --git a/dashboard/src/ui/widgets/system.rs b/dashboard/src/ui/widgets/system.rs index 876c383..911fdc0 100644 --- a/dashboard/src/ui/widgets/system.rs +++ b/dashboard/src/ui/widgets/system.rs @@ -513,48 +513,9 @@ impl SystemWidget { Span::styled("Storage:", Typography::widget_title()) ])); - // Storage items with overflow handling + // Storage items - let main overflow logic handle truncation let storage_lines = self.render_storage(); - let remaining_space = area.height.saturating_sub(lines.len() as u16); - - if storage_lines.len() <= remaining_space as usize { - // All storage lines fit - lines.extend(storage_lines); - } else if remaining_space >= 2 { - // Show what we can and add overflow indicator - let lines_to_show = (remaining_space - 1) as usize; // Reserve 1 line for overflow - lines.extend(storage_lines.iter().take(lines_to_show).cloned()); - - // Count hidden pools - let mut hidden_pools = 0; - let mut current_pool = String::new(); - for (i, line) in storage_lines.iter().enumerate() { - if i >= lines_to_show { - // Check if this line represents a new pool (no indentation) - if let Some(first_span) = line.spans.first() { - let text = first_span.content.as_ref(); - if !text.starts_with(" ") && text.contains(':') { - let pool_name = text.split(':').next().unwrap_or("").trim(); - if pool_name != current_pool { - hidden_pools += 1; - current_pool = pool_name.to_string(); - } - } - } - } - } - - if hidden_pools > 0 { - let overflow_text = format!( - "... and {} more pool{}", - hidden_pools, - if hidden_pools == 1 { "" } else { "s" } - ); - lines.push(Line::from(vec![ - Span::styled(overflow_text, Typography::muted()) - ])); - } - } + lines.extend(storage_lines); // Apply scroll offset let total_lines = lines.len(); diff --git a/shared/Cargo.toml b/shared/Cargo.toml index 09a95ad..062283f 100644 --- a/shared/Cargo.toml +++ b/shared/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cm-dashboard-shared" -version = "0.1.96" +version = "0.1.97" edition = "2021" [dependencies]