From d922e8d6f3d42daf9258dd4f22fa09356a06c46a Mon Sep 17 00:00:00 2001 From: Christoffer Martinsson Date: Tue, 9 Dec 2025 19:22:51 +0100 Subject: [PATCH] Restructure backup display to show per-repository metrics Remove disk-based backup display and implement repository-centric view with per-repo archive counts and sizes. Backup now uses NFS storage instead of direct disk monitoring. Changes: - Remove BackupDiskData, add BackupRepositoryData structure - Display format: "Repo " with per-repo details - Show archive count and size (MB/GB) for each repository - Agent aggregates repo data from backup status TOML files - Dashboard renders repo list with individual status indicators --- Cargo.lock | 6 +- agent/Cargo.toml | 2 +- agent/src/collectors/backup.rs | 102 ++++++++----------------- dashboard/Cargo.toml | 2 +- dashboard/src/ui/widgets/system.rs | 117 ++++++++++------------------- shared/Cargo.toml | 2 +- shared/src/agent_data.rs | 33 +++----- 7 files changed, 88 insertions(+), 176 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b22329d..b9769da 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -279,7 +279,7 @@ checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" [[package]] name = "cm-dashboard" -version = "0.1.265" +version = "0.1.267" dependencies = [ "anyhow", "chrono", @@ -301,7 +301,7 @@ dependencies = [ [[package]] name = "cm-dashboard-agent" -version = "0.1.265" +version = "0.1.267" dependencies = [ "anyhow", "async-trait", @@ -325,7 +325,7 @@ dependencies = [ [[package]] name = "cm-dashboard-shared" -version = "0.1.265" +version = "0.1.267" dependencies = [ "chrono", "serde", diff --git a/agent/Cargo.toml b/agent/Cargo.toml index d449a4d..6bc6e57 100644 --- a/agent/Cargo.toml +++ b/agent/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cm-dashboard-agent" -version = "0.1.266" +version = "0.1.267" edition = "2021" [dependencies] diff --git a/agent/src/collectors/backup.rs b/agent/src/collectors/backup.rs index cfc9718..c85c0b4 100644 --- a/agent/src/collectors/backup.rs +++ b/agent/src/collectors/backup.rs @@ -1,7 +1,7 @@ use async_trait::async_trait; -use cm_dashboard_shared::{AgentData, BackupData, BackupDiskData, Status}; +use cm_dashboard_shared::{AgentData, BackupData, BackupRepositoryData, Status}; use serde::{Deserialize, Serialize}; -use std::collections::{HashMap, HashSet}; +use std::collections::HashMap; use std::fs; use std::path::{Path, PathBuf}; use tracing::{debug, warn}; @@ -83,17 +83,6 @@ impl BackupCollector { } } - /// Calculate usage status from disk usage percentage - fn calculate_usage_status(usage_percent: f32) -> Status { - if usage_percent < 80.0 { - Status::Ok - } else if usage_percent < 90.0 { - Status::Warning - } else { - Status::Critical - } - } - /// Convert BackupStatusToml to BackupData and populate AgentData async fn populate_backup_data(&self, agent_data: &mut AgentData) -> Result<(), CollectorError> { let status_files = self.scan_status_files().await?; @@ -101,76 +90,47 @@ impl BackupCollector { if status_files.is_empty() { debug!("No backup status files found"); agent_data.backup = BackupData { + last_backup_time: None, + backup_status: Status::Unknown, repositories: Vec::new(), - repository_status: Status::Unknown, - disks: Vec::new(), }; return Ok(()); } - let mut all_repositories = HashSet::new(); - let mut disks = Vec::new(); + // Aggregate repository data across all backup status files + let mut repo_map: HashMap = HashMap::new(); let mut worst_status = Status::Ok; + let mut latest_backup_time: Option = None; for status_file in status_files { match self.read_status_file(&status_file).await { Ok(backup_status) => { - // Collect all service names - for service_name in backup_status.services.keys() { - all_repositories.insert(service_name.clone()); - } - // Calculate backup status let backup_status_enum = Self::calculate_backup_status(&backup_status.status); + worst_status = worst_status.max(backup_status_enum); - // Calculate usage status from disk space - let (usage_percent, used_gb, total_gb, usage_status) = if let Some(disk_space) = &backup_status.disk_space { - let usage_pct = disk_space.usage_percent as f32; - ( - usage_pct, - disk_space.used_gb as f32, - disk_space.total_gb as f32, - Self::calculate_usage_status(usage_pct), - ) - } else { - (0.0, 0.0, 0.0, Status::Unknown) - }; + // Track latest backup time + if latest_backup_time.is_none() || Some(&backup_status.start_time) > latest_backup_time.as_ref() { + latest_backup_time = Some(backup_status.start_time.clone()); + } - // Update worst status - worst_status = worst_status.max(backup_status_enum).max(usage_status); + // Process each service in this backup + for (service_name, service_status) in backup_status.services { + // Convert bytes to GB + let repo_size_gb = service_status.repo_size_bytes as f32 / 1_073_741_824.0; - // Build service list for this disk - let services: Vec = backup_status.services.keys().cloned().collect(); + // Calculate service status + let service_status_enum = Self::calculate_backup_status(&service_status.status); + worst_status = worst_status.max(service_status_enum); - // Get min and max archive counts to detect inconsistencies - let archives_min: i64 = backup_status.services.values() - .map(|service| service.archive_count) - .min() - .unwrap_or(0); - - let archives_max: i64 = backup_status.services.values() - .map(|service| service.archive_count) - .max() - .unwrap_or(0); - - // Create disk data - let disk_data = BackupDiskData { - serial: backup_status.disk_serial_number.unwrap_or_else(|| "Unknown".to_string()), - product_name: backup_status.disk_product_name, - wear_percent: backup_status.disk_wear_percent, - temperature_celsius: None, // Not available in current TOML - last_backup_time: Some(backup_status.start_time), - backup_status: backup_status_enum, - disk_usage_percent: usage_percent, - disk_used_gb: used_gb, - disk_total_gb: total_gb, - usage_status, - services, - archives_min, - archives_max, - }; - - disks.push(disk_data); + // Update or insert repository data + repo_map.insert(service_name.clone(), BackupRepositoryData { + name: service_name, + archive_count: service_status.archive_count, + repo_size_gb, + status: service_status_enum, + }); + } } Err(e) => { warn!("Failed to read backup status file {:?}: {}", status_file, e); @@ -178,12 +138,14 @@ impl BackupCollector { } } - let repositories: Vec = all_repositories.into_iter().collect(); + // Convert HashMap to sorted Vec + let mut repositories: Vec = repo_map.into_values().collect(); + repositories.sort_by(|a, b| a.name.cmp(&b.name)); agent_data.backup = BackupData { + last_backup_time: latest_backup_time, + backup_status: worst_status, repositories, - repository_status: worst_status, - disks, }; Ok(()) diff --git a/dashboard/Cargo.toml b/dashboard/Cargo.toml index b9b9461..7bb41b9 100644 --- a/dashboard/Cargo.toml +++ b/dashboard/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cm-dashboard" -version = "0.1.266" +version = "0.1.267" edition = "2021" [dependencies] diff --git a/dashboard/src/ui/widgets/system.rs b/dashboard/src/ui/widgets/system.rs index 0c7ec9b..4b7d642 100644 --- a/dashboard/src/ui/widgets/system.rs +++ b/dashboard/src/ui/widgets/system.rs @@ -44,9 +44,9 @@ pub struct SystemWidget { storage_pools: Vec, // Backup metrics - backup_repositories: Vec, - backup_repository_status: Status, - backup_disks: Vec, + backup_last_time: Option, + backup_status: Status, + backup_repositories: Vec, // Overall status has_data: bool, @@ -112,9 +112,9 @@ impl SystemWidget { tmp_status: Status::Unknown, tmpfs_mounts: Vec::new(), storage_pools: Vec::new(), + backup_last_time: None, + backup_status: Status::Unknown, backup_repositories: Vec::new(), - backup_repository_status: Status::Unknown, - backup_disks: Vec::new(), has_data: false, scroll_offset: 0, last_viewport_height: 0, @@ -221,9 +221,9 @@ impl Widget for SystemWidget { // Extract backup data let backup = &agent_data.backup; + self.backup_last_time = backup.last_backup_time.clone(); + self.backup_status = backup.backup_status; self.backup_repositories = backup.repositories.clone(); - self.backup_repository_status = backup.repository_status; - self.backup_disks = backup.disks.clone(); // Clamp scroll offset to valid range after update // This prevents scroll issues when switching between hosts @@ -533,79 +533,41 @@ impl SystemWidget { fn render_backup(&self) -> Vec> { let mut lines = Vec::new(); - // First section: Repository status and list - if !self.backup_repositories.is_empty() { - let repo_text = format!("Repo: {}", self.backup_repositories.len()); - let repo_spans = StatusIcons::create_status_spans(self.backup_repository_status, &repo_text); - lines.push(Line::from(repo_spans)); - - // List all repositories (sorted for consistent display) - let mut sorted_repos = self.backup_repositories.clone(); - sorted_repos.sort(); - let repo_count = sorted_repos.len(); - for (idx, repo) in sorted_repos.iter().enumerate() { - let tree_char = if idx == repo_count - 1 { "└─" } else { "├─" }; - lines.push(Line::from(vec![ - Span::styled(format!(" {} ", tree_char), Typography::tree()), - Span::styled(repo.clone(), Typography::secondary()), - ])); - } + if self.backup_repositories.is_empty() { + return lines; } - // Second section: Per-disk backup information (sorted by serial for consistent display) - let mut sorted_disks = self.backup_disks.clone(); - sorted_disks.sort_by(|a, b| a.serial.cmp(&b.serial)); - for disk in &sorted_disks { - let truncated_serial = truncate_serial(&disk.serial); - let mut details = Vec::new(); + // Format backup time (use complete timestamp) + let time_display = if let Some(ref time_str) = self.backup_last_time { + time_str.clone() + } else { + "unknown".to_string() + }; - if let Some(temp) = disk.temperature_celsius { - details.push(format!("T: {}°C", temp as i32)); - } - if let Some(wear) = disk.wear_percent { - details.push(format!("W: {}%", wear as i32)); - } + // Header: "Repo " + let repo_text = format!("Repo {}", time_display); + let repo_spans = StatusIcons::create_status_spans(self.backup_status, &repo_text); + lines.push(Line::from(repo_spans)); - let disk_text = if !details.is_empty() { - format!("{} {}", truncated_serial, details.join(" ")) + // List all repositories with archive count and size + let repo_count = self.backup_repositories.len(); + for (idx, repo) in self.backup_repositories.iter().enumerate() { + let tree_char = if idx == repo_count - 1 { "└─" } else { "├─" }; + + // Format size: use MB for < 1GB, otherwise GB + let size_display = if repo.repo_size_gb < 1.0 { + format!("{:.0}MB", repo.repo_size_gb * 1024.0) } else { - truncated_serial + format!("{:.1}GB", repo.repo_size_gb) }; - // Overall disk status (worst of backup and usage) - let disk_status = disk.backup_status.max(disk.usage_status); - let disk_spans = StatusIcons::create_status_spans(disk_status, &disk_text); - lines.push(Line::from(disk_spans)); + let repo_text = format!("{} ({}) {}", repo.name, repo.archive_count, size_display); - // Show backup time with status - if let Some(backup_time) = &disk.last_backup_time { - let time_text = format!("Backup: {}", backup_time); - let mut time_spans = vec![ - Span::styled(" ├─ ", Typography::tree()), - ]; - time_spans.extend(StatusIcons::create_status_spans(disk.backup_status, &time_text)); - lines.push(Line::from(time_spans)); - } - - // Show usage with status and archive count - let archive_display = if disk.archives_min == disk.archives_max { - format!("{}", disk.archives_min) - } else { - format!("{}-{}", disk.archives_min, disk.archives_max) - }; - - let usage_text = format!( - "Usage: ({}) {:.0}% {:.0}GB/{:.0}GB", - archive_display, - disk.disk_usage_percent, - disk.disk_used_gb, - disk.disk_total_gb - ); - let mut usage_spans = vec![ - Span::styled(" └─ ", Typography::tree()), + let mut repo_spans = vec![ + Span::styled(format!(" {} ", tree_char), Typography::tree()), ]; - usage_spans.extend(StatusIcons::create_status_spans(disk.usage_status, &usage_text)); - lines.push(Line::from(usage_spans)); + repo_spans.extend(StatusIcons::create_status_spans(repo.status, &repo_text)); + lines.push(Line::from(repo_spans)); } lines @@ -876,13 +838,10 @@ impl SystemWidget { } // Backup section - if !self.backup_repositories.is_empty() || !self.backup_disks.is_empty() { - count += 1; // Header - if !self.backup_repositories.is_empty() { - count += 1; // Repo header - count += self.backup_repositories.len(); - } - count += self.backup_disks.len() * 3; // Each disk has 3 lines + if !self.backup_repositories.is_empty() { + count += 1; // Header: "Backup:" + count += 1; // Repo count and timestamp header + count += self.backup_repositories.len(); // Individual repos } count @@ -988,7 +947,7 @@ impl SystemWidget { lines.extend(storage_lines); // Backup section (if available) - if !self.backup_repositories.is_empty() || !self.backup_disks.is_empty() { + if !self.backup_repositories.is_empty() { lines.push(Line::from(vec![ Span::styled("Backup:", Typography::widget_title()) ])); diff --git a/shared/Cargo.toml b/shared/Cargo.toml index 623efd8..f93f3c7 100644 --- a/shared/Cargo.toml +++ b/shared/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cm-dashboard-shared" -version = "0.1.266" +version = "0.1.267" edition = "2021" [dependencies] diff --git a/shared/src/agent_data.rs b/shared/src/agent_data.rs index 96e9d36..146f8d5 100644 --- a/shared/src/agent_data.rs +++ b/shared/src/agent_data.rs @@ -182,27 +182,18 @@ pub struct SubServiceMetric { /// Backup system data #[derive(Debug, Clone, Serialize, Deserialize)] pub struct BackupData { - pub repositories: Vec, - pub repository_status: Status, - pub disks: Vec, -} - -/// Backup repository disk information -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct BackupDiskData { - pub serial: String, - pub product_name: Option, - pub wear_percent: Option, - pub temperature_celsius: Option, pub last_backup_time: Option, pub backup_status: Status, - pub disk_usage_percent: f32, - pub disk_used_gb: f32, - pub disk_total_gb: f32, - pub usage_status: Status, - pub services: Vec, - pub archives_min: i64, - pub archives_max: i64, + pub repositories: Vec, +} + +/// Individual backup repository information +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct BackupRepositoryData { + pub name: String, + pub archive_count: i64, + pub repo_size_gb: f32, + pub status: Status, } impl AgentData { @@ -245,9 +236,9 @@ impl AgentData { }, services: Vec::new(), backup: BackupData { + last_backup_time: None, + backup_status: Status::Unknown, repositories: Vec::new(), - repository_status: Status::Unknown, - disks: Vec::new(), }, } }