diff --git a/Cargo.lock b/Cargo.lock index 5a838b8..34643e4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -279,7 +279,7 @@ checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" [[package]] name = "cm-dashboard" -version = "0.1.156" +version = "0.1.157" dependencies = [ "anyhow", "chrono", @@ -301,7 +301,7 @@ dependencies = [ [[package]] name = "cm-dashboard-agent" -version = "0.1.156" +version = "0.1.157" dependencies = [ "anyhow", "async-trait", @@ -324,7 +324,7 @@ dependencies = [ [[package]] name = "cm-dashboard-shared" -version = "0.1.156" +version = "0.1.157" dependencies = [ "chrono", "serde", diff --git a/agent/Cargo.toml b/agent/Cargo.toml index 7e18d77..d1e6fb2 100644 --- a/agent/Cargo.toml +++ b/agent/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cm-dashboard-agent" -version = "0.1.156" +version = "0.1.157" edition = "2021" [dependencies] diff --git a/agent/src/collectors/disk.rs b/agent/src/collectors/disk.rs index 945c442..ffb31b7 100644 --- a/agent/src/collectors/disk.rs +++ b/agent/src/collectors/disk.rs @@ -639,10 +639,19 @@ impl DiskCollector { }).collect(); // Calculate overall pool health string and status - let (pool_health, health_status) = match (failed_data, failed_parity) { - (0, 0) => ("healthy".to_string(), cm_dashboard_shared::Status::Ok), - (1, 0) | (0, 1) => ("degraded".to_string(), cm_dashboard_shared::Status::Warning), - _ => ("critical".to_string(), cm_dashboard_shared::Status::Critical), + // SnapRAID logic: can tolerate up to N parity drive failures (where N = number of parity drives) + // If data drives fail AND we've lost parity protection, that's critical + let (pool_health, health_status) = if failed_data == 0 && failed_parity == 0 { + ("healthy".to_string(), cm_dashboard_shared::Status::Ok) + } else if failed_data == 0 && failed_parity > 0 { + // Parity failed but no data loss - degraded (reduced protection) + ("degraded".to_string(), cm_dashboard_shared::Status::Warning) + } else if failed_data == 1 && failed_parity == 0 { + // One data drive failed, parity intact - degraded (recoverable) + ("degraded".to_string(), cm_dashboard_shared::Status::Warning) + } else { + // Multiple data drives failed OR data+parity failed = data loss risk + ("critical".to_string(), cm_dashboard_shared::Status::Critical) }; // Calculate pool usage status using config thresholds diff --git a/agent/src/collectors/nixos.rs b/agent/src/collectors/nixos.rs index aef3729..0a67211 100644 --- a/agent/src/collectors/nixos.rs +++ b/agent/src/collectors/nixos.rs @@ -83,14 +83,25 @@ impl NixOSCollector { std::env::var("CM_DASHBOARD_VERSION").unwrap_or_else(|_| "unknown".to_string()) } - /// Get NixOS system generation (build) information + /// Get NixOS system generation (build) information from git commit async fn get_nixos_generation(&self) -> Option { - match Command::new("nixos-version").output() { - Ok(output) => { - let version_str = String::from_utf8_lossy(&output.stdout); - Some(version_str.trim().to_string()) + // Try to read git commit hash from file written during rebuild + let commit_file = "/var/lib/cm-dashboard/git-commit"; + match fs::read_to_string(commit_file) { + Ok(content) => { + let commit_hash = content.trim(); + if commit_hash.len() >= 7 { + debug!("Found git commit hash: {}", commit_hash); + Some(commit_hash.to_string()) + } else { + debug!("Git commit hash too short: {}", commit_hash); + None + } + } + Err(e) => { + debug!("Failed to read git commit file {}: {}", commit_file, e); + None } - Err(_) => None, } } } diff --git a/dashboard/Cargo.toml b/dashboard/Cargo.toml index 40bc76e..c474a29 100644 --- a/dashboard/Cargo.toml +++ b/dashboard/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cm-dashboard" -version = "0.1.156" +version = "0.1.157" edition = "2021" [dependencies] diff --git a/dashboard/src/ui/widgets/system.rs b/dashboard/src/ui/widgets/system.rs index 2fe642a..6fa42d8 100644 --- a/dashboard/src/ui/widgets/system.rs +++ b/dashboard/src/ui/widgets/system.rs @@ -368,12 +368,8 @@ impl SystemWidget { // Pool header line with type and health let pool_label = if pool.pool_type == "drive" { // For physical drives, show the drive name with temperature and wear percentage if available - // Look for any drive with temp/wear data (physical drives may have drives named after the pool) - let drive_info = pool.drives.iter() - .find(|d| d.name == pool.name) - .or_else(|| pool.drives.first()); - - if let Some(drive) = drive_info { + // Physical drives only have one drive entry + if let Some(drive) = pool.drives.first() { let mut drive_details = Vec::new(); if let Some(temp) = drive.temperature { drive_details.push(format!("T: {}°C", temp as i32)); @@ -381,11 +377,11 @@ impl SystemWidget { if let Some(wear) = drive.wear_percent { drive_details.push(format!("W: {}%", wear as i32)); } - + if !drive_details.is_empty() { - format!("{} {}", pool.name, drive_details.join(" ")) + format!("{} {}", drive.name, drive_details.join(" ")) } else { - pool.name.clone() + drive.name.clone() } } else { pool.name.clone() @@ -443,7 +439,9 @@ impl SystemWidget { lines.push(Line::from(total_spans)); // Data drives - at same level as parity + let has_parity = !pool.parity_drives.is_empty(); for (i, drive) in pool.data_drives.iter().enumerate() { + let is_last_data = i == pool.data_drives.len() - 1; let mut drive_details = Vec::new(); if let Some(temp) = drive.temperature { drive_details.push(format!("T: {}°C", temp as i32)); @@ -458,16 +456,19 @@ impl SystemWidget { format!("Data_{}: {}", i + 1, drive.name) }; + // Last data drive uses └─ if there's no parity, otherwise ├─ + let tree_symbol = if is_last_data && !has_parity { " └─ " } else { " ├─ " }; let mut data_spans = vec![ - Span::styled(" ├─ ", Typography::tree()), + Span::styled(tree_symbol, Typography::tree()), ]; data_spans.extend(StatusIcons::create_status_spans(drive.status.clone(), &drive_text)); lines.push(Line::from(data_spans)); } - // Parity drives - last item + // Parity drives - last item(s) if !pool.parity_drives.is_empty() { - for drive in &pool.parity_drives { + for (i, drive) in pool.parity_drives.iter().enumerate() { + let is_last = i == pool.parity_drives.len() - 1; let mut drive_details = Vec::new(); if let Some(temp) = drive.temperature { drive_details.push(format!("T: {}°C", temp as i32)); @@ -482,8 +483,9 @@ impl SystemWidget { format!("Parity: {}", drive.name) }; + let tree_symbol = if is_last { " └─ " } else { " ├─ " }; let mut parity_spans = vec![ - Span::styled(" └─ ", Typography::tree()), + Span::styled(tree_symbol, Typography::tree()), ]; parity_spans.extend(StatusIcons::create_status_spans(drive.status.clone(), &drive_text)); lines.push(Line::from(parity_spans)); diff --git a/shared/Cargo.toml b/shared/Cargo.toml index 125df54..e3cadb5 100644 --- a/shared/Cargo.toml +++ b/shared/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cm-dashboard-shared" -version = "0.1.156" +version = "0.1.157" edition = "2021" [dependencies]