Fix storage display format and clean up warnings
All checks were successful
Build and Release / build-and-release (push) Successful in 1m9s

Update storage display to match CLAUDE.md specification:
- Show drive temp/wear on main line: nvme0n1 T: 25°C W: 4%
- Display individual filesystems as sub-items: /: 55% 250.5GB/456.4GB
- Remove Total usage line in favor of filesystem breakdown

Clean up code warnings:
- Remove unused heartbeat methods and fields
- Remove unused backup widget fields and methods
- Add allow attributes for legacy methods
This commit is contained in:
Christoffer Martinsson 2025-11-24 16:03:31 +01:00
parent bea2d120b5
commit 11d1c2dc94
8 changed files with 122 additions and 167 deletions

6
Cargo.lock generated
View File

@ -279,7 +279,7 @@ checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d"
[[package]] [[package]]
name = "cm-dashboard" name = "cm-dashboard"
version = "0.1.136" version = "0.1.137"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"chrono", "chrono",
@ -301,7 +301,7 @@ dependencies = [
[[package]] [[package]]
name = "cm-dashboard-agent" name = "cm-dashboard-agent"
version = "0.1.136" version = "0.1.137"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",
@ -324,7 +324,7 @@ dependencies = [
[[package]] [[package]]
name = "cm-dashboard-shared" name = "cm-dashboard-shared"
version = "0.1.136" version = "0.1.137"
dependencies = [ dependencies = [
"chrono", "chrono",
"serde", "serde",

View File

@ -1,6 +1,6 @@
[package] [package]
name = "cm-dashboard-agent" name = "cm-dashboard-agent"
version = "0.1.137" version = "0.1.138"
edition = "2021" edition = "2021"
[dependencies] [dependencies]

View File

@ -262,47 +262,11 @@ impl Agent {
agent_data.system.memory.swap_used_gb = value; agent_data.system.memory.swap_used_gb = value;
} }
} }
// Tmpfs metrics // Tmpfs metrics - handle multiple auto-discovered tmpfs mounts
else if metric.name.starts_with("memory_tmp_") { else if metric.name.starts_with("memory_tmpfs_") {
// For now, create a single /tmp tmpfs entry if let Some((mount_point, metric_type)) = self.parse_tmpfs_metric_name(&metric.name) {
if metric.name == "memory_tmp_usage_percent" {
if let Some(value) = metric.value.as_f32() { if let Some(value) = metric.value.as_f32() {
if let Some(tmpfs) = agent_data.system.memory.tmpfs.get_mut(0) { self.update_tmpfs_data(&mut agent_data.system.memory.tmpfs, &mount_point, &metric_type, value);
tmpfs.usage_percent = value;
} else {
agent_data.system.memory.tmpfs.push(TmpfsData {
mount: "/tmp".to_string(),
usage_percent: value,
used_gb: 0.0,
total_gb: 0.0,
});
}
}
} else if metric.name == "memory_tmp_used_gb" {
if let Some(value) = metric.value.as_f32() {
if let Some(tmpfs) = agent_data.system.memory.tmpfs.get_mut(0) {
tmpfs.used_gb = value;
} else {
agent_data.system.memory.tmpfs.push(TmpfsData {
mount: "/tmp".to_string(),
usage_percent: 0.0,
used_gb: value,
total_gb: 0.0,
});
}
}
} else if metric.name == "memory_tmp_total_gb" {
if let Some(value) = metric.value.as_f32() {
if let Some(tmpfs) = agent_data.system.memory.tmpfs.get_mut(0) {
tmpfs.total_gb = value;
} else {
agent_data.system.memory.tmpfs.push(TmpfsData {
mount: "/tmp".to_string(),
usage_percent: 0.0,
used_gb: 0.0,
total_gb: value,
});
}
} }
} }
} }
@ -394,6 +358,63 @@ impl Agent {
Ok(()) Ok(())
} }
/// Parse tmpfs metric name to extract mount point and metric type
/// Example: "memory_tmpfs_tmp_usage_percent" -> ("/tmp", "usage_percent")
fn parse_tmpfs_metric_name(&self, metric_name: &str) -> Option<(String, String)> {
if !metric_name.starts_with("memory_tmpfs_") {
return None;
}
let remainder = &metric_name[13..]; // Remove "memory_tmpfs_" prefix
// Find the last underscore to separate metric type from mount point
if let Some(last_underscore) = remainder.rfind('_') {
let mount_safe = &remainder[..last_underscore];
let metric_type = &remainder[last_underscore + 1..];
// Convert safe mount name back to actual mount point
let mount_point = if mount_safe.is_empty() {
"/"
} else {
&format!("/{}", mount_safe.replace('_', "/"))
};
Some((mount_point.to_string(), metric_type.to_string()))
} else {
None
}
}
/// Update tmpfs data in the tmpfs vector
fn update_tmpfs_data(&self, tmpfs_vec: &mut Vec<TmpfsData>, mount_point: &str, metric_type: &str, value: f32) {
// Find existing tmpfs entry
let existing_index = tmpfs_vec.iter()
.position(|tmpfs| tmpfs.mount == mount_point);
let tmpfs_index = if let Some(index) = existing_index {
index
} else {
// Create new entry
tmpfs_vec.push(TmpfsData {
mount: mount_point.to_string(),
usage_percent: 0.0,
used_gb: 0.0,
total_gb: 0.0,
});
tmpfs_vec.len() - 1
};
// Update the tmpfs entry
if let Some(tmpfs) = tmpfs_vec.get_mut(tmpfs_index) {
match metric_type {
"usage_percent" => tmpfs.usage_percent = value,
"used_gb" => tmpfs.used_gb = value,
"total_gb" => tmpfs.total_gb = value,
_ => {} // Unknown metric type, ignore
}
}
}
/// Extract drive name from metric like "disk_nvme0n1_temperature" /// Extract drive name from metric like "disk_nvme0n1_temperature"
fn extract_drive_name(&self, metric_name: &str) -> Option<String> { fn extract_drive_name(&self, metric_name: &str) -> Option<String> {
if metric_name.starts_with("disk_") { if metric_name.starts_with("disk_") {
@ -529,31 +550,6 @@ impl Agent {
} }
/// Create heartbeat metric for host connectivity detection /// Create heartbeat metric for host connectivity detection
fn get_heartbeat_metric(&self) -> Metric {
use std::time::{SystemTime, UNIX_EPOCH};
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
Metric::new(
"agent_heartbeat".to_string(),
MetricValue::Integer(timestamp as i64),
Status::Ok,
)
}
/// Send standalone heartbeat for connectivity detection
async fn send_heartbeat(&mut self) -> Result<()> {
// Create minimal agent data with just heartbeat
let agent_data = AgentData::new(self.hostname.clone(), self.get_agent_version());
// Heartbeat timestamp is already set in AgentData::new()
self.zmq_handler.publish_agent_data(&agent_data).await?;
debug!("Sent standalone heartbeat for connectivity detection");
Ok(())
}
async fn handle_commands(&mut self) -> Result<()> { async fn handle_commands(&mut self) -> Result<()> {
// Try to receive commands (non-blocking) // Try to receive commands (non-blocking)

View File

@ -1,6 +1,6 @@
[package] [package]
name = "cm-dashboard" name = "cm-dashboard"
version = "0.1.137" version = "0.1.138"
edition = "2021" edition = "2021"
[dependencies] [dependencies]

View File

@ -18,8 +18,6 @@ pub struct BackupWidget {
duration_seconds: Option<i64>, duration_seconds: Option<i64>,
/// Last backup timestamp /// Last backup timestamp
last_run_timestamp: Option<i64>, last_run_timestamp: Option<i64>,
/// Total number of backup services
total_services: Option<i64>,
/// Total repository size in GB /// Total repository size in GB
total_repo_size_gb: Option<f32>, total_repo_size_gb: Option<f32>,
/// Total disk space for backups in GB /// Total disk space for backups in GB
@ -32,14 +30,6 @@ pub struct BackupWidget {
backup_disk_serial_number: Option<String>, backup_disk_serial_number: Option<String>,
/// Backup disk wear percentage from SMART data /// Backup disk wear percentage from SMART data
backup_disk_wear_percent: Option<f32>, backup_disk_wear_percent: Option<f32>,
/// Backup disk filesystem label
backup_disk_filesystem_label: Option<String>,
/// Number of completed services
services_completed_count: Option<i64>,
/// Number of failed services
services_failed_count: Option<i64>,
/// Number of disabled services
services_disabled_count: Option<i64>,
/// All individual service metrics for detailed display /// All individual service metrics for detailed display
service_metrics: Vec<ServiceMetricData>, service_metrics: Vec<ServiceMetricData>,
/// Last update indicator /// Last update indicator
@ -50,7 +40,6 @@ pub struct BackupWidget {
struct ServiceMetricData { struct ServiceMetricData {
name: String, name: String,
status: Status, status: Status,
exit_code: Option<i64>,
archive_count: Option<i64>, archive_count: Option<i64>,
repo_size_gb: Option<f32>, repo_size_gb: Option<f32>,
} }
@ -61,17 +50,12 @@ impl BackupWidget {
overall_status: Status::Unknown, overall_status: Status::Unknown,
duration_seconds: None, duration_seconds: None,
last_run_timestamp: None, last_run_timestamp: None,
total_services: None,
total_repo_size_gb: None, total_repo_size_gb: None,
backup_disk_total_gb: None, backup_disk_total_gb: None,
backup_disk_used_gb: None, backup_disk_used_gb: None,
backup_disk_product_name: None, backup_disk_product_name: None,
backup_disk_serial_number: None, backup_disk_serial_number: None,
backup_disk_wear_percent: None, backup_disk_wear_percent: None,
backup_disk_filesystem_label: None,
services_completed_count: None,
services_failed_count: None,
services_disabled_count: None,
service_metrics: Vec::new(), service_metrics: Vec::new(),
has_data: false, has_data: false,
} }
@ -112,6 +96,7 @@ impl BackupWidget {
/// Extract service name from metric name (e.g., "backup_service_gitea_status" -> "gitea") /// Extract service name from metric name (e.g., "backup_service_gitea_status" -> "gitea")
#[allow(dead_code)]
fn extract_service_name(metric_name: &str) -> Option<String> { fn extract_service_name(metric_name: &str) -> Option<String> {
if metric_name.starts_with("backup_service_") { if metric_name.starts_with("backup_service_") {
let name_part = &metric_name[15..]; // Remove "backup_service_" prefix let name_part = &metric_name[15..]; // Remove "backup_service_" prefix
@ -119,8 +104,6 @@ impl BackupWidget {
// Try to extract service name by removing known suffixes // Try to extract service name by removing known suffixes
if let Some(service_name) = name_part.strip_suffix("_status") { if let Some(service_name) = name_part.strip_suffix("_status") {
Some(service_name.to_string()) Some(service_name.to_string())
} else if let Some(service_name) = name_part.strip_suffix("_exit_code") {
Some(service_name.to_string())
} else if let Some(service_name) = name_part.strip_suffix("_archive_count") { } else if let Some(service_name) = name_part.strip_suffix("_archive_count") {
Some(service_name.to_string()) Some(service_name.to_string())
} else if let Some(service_name) = name_part.strip_suffix("_repo_size_gb") { } else if let Some(service_name) = name_part.strip_suffix("_repo_size_gb") {
@ -154,6 +137,7 @@ impl Widget for BackupWidget {
} }
impl BackupWidget { impl BackupWidget {
#[allow(dead_code)]
fn update_from_metrics(&mut self, metrics: &[&Metric]) { fn update_from_metrics(&mut self, metrics: &[&Metric]) {
debug!("Backup widget updating with {} metrics", metrics.len()); debug!("Backup widget updating with {} metrics", metrics.len());
for metric in metrics { for metric in metrics {
@ -199,9 +183,6 @@ impl BackupWidget {
"backup_last_run_timestamp" => { "backup_last_run_timestamp" => {
self.last_run_timestamp = metric.value.as_i64(); self.last_run_timestamp = metric.value.as_i64();
} }
"backup_total_services" => {
self.total_services = metric.value.as_i64();
}
"backup_total_repo_size_gb" => { "backup_total_repo_size_gb" => {
self.total_repo_size_gb = metric.value.as_f32(); self.total_repo_size_gb = metric.value.as_f32();
} }
@ -220,18 +201,6 @@ impl BackupWidget {
"backup_disk_wear_percent" => { "backup_disk_wear_percent" => {
self.backup_disk_wear_percent = metric.value.as_f32(); self.backup_disk_wear_percent = metric.value.as_f32();
} }
"backup_disk_filesystem_label" => {
self.backup_disk_filesystem_label = Some(metric.value.as_string());
}
"backup_services_completed_count" => {
self.services_completed_count = metric.value.as_i64();
}
"backup_services_failed_count" => {
self.services_failed_count = metric.value.as_i64();
}
"backup_services_disabled_count" => {
self.services_disabled_count = metric.value.as_i64();
}
_ => { _ => {
// Handle individual service metrics // Handle individual service metrics
if let Some(service_name) = Self::extract_service_name(&metric.name) { if let Some(service_name) = Self::extract_service_name(&metric.name) {
@ -243,7 +212,6 @@ impl BackupWidget {
ServiceMetricData { ServiceMetricData {
name: service_name, name: service_name,
status: Status::Unknown, status: Status::Unknown,
exit_code: None,
archive_count: None, archive_count: None,
repo_size_gb: None, repo_size_gb: None,
} }
@ -252,8 +220,6 @@ impl BackupWidget {
if metric.name.ends_with("_status") { if metric.name.ends_with("_status") {
entry.status = metric.status; entry.status = metric.status;
debug!("Set status for {}: {:?}", entry.name, entry.status); debug!("Set status for {}: {:?}", entry.name, entry.status);
} else if metric.name.ends_with("_exit_code") {
entry.exit_code = metric.value.as_i64();
} else if metric.name.ends_with("_archive_count") { } else if metric.name.ends_with("_archive_count") {
entry.archive_count = metric.value.as_i64(); entry.archive_count = metric.value.as_i64();
debug!( debug!(

View File

@ -47,6 +47,7 @@ impl ServicesWidget {
} }
/// Extract service name and determine if it's a parent or sub-service /// Extract service name and determine if it's a parent or sub-service
#[allow(dead_code)]
fn extract_service_info(metric_name: &str) -> Option<(String, Option<String>)> { fn extract_service_info(metric_name: &str) -> Option<(String, Option<String>)> {
if metric_name.starts_with("service_") { if metric_name.starts_with("service_") {
if let Some(end_pos) = metric_name if let Some(end_pos) = metric_name
@ -277,6 +278,7 @@ impl Widget for ServicesWidget {
} }
impl ServicesWidget { impl ServicesWidget {
#[allow(dead_code)]
fn update_from_metrics(&mut self, metrics: &[&Metric]) { fn update_from_metrics(&mut self, metrics: &[&Metric]) {
debug!("Services widget updating with {} metrics", metrics.len()); debug!("Services widget updating with {} metrics", metrics.len());

View File

@ -31,6 +31,8 @@ pub struct SystemWidget {
tmp_total_gb: Option<f32>, tmp_total_gb: Option<f32>,
memory_status: Status, memory_status: Status,
tmp_status: Status, tmp_status: Status,
/// All tmpfs mounts (for auto-discovery support)
tmpfs_mounts: Vec<cm_dashboard_shared::TmpfsData>,
// Storage metrics (collected from disk metrics) // Storage metrics (collected from disk metrics)
storage_pools: Vec<StoragePool>, storage_pools: Vec<StoragePool>,
@ -50,7 +52,6 @@ struct StoragePool {
used_gb: Option<f32>, used_gb: Option<f32>,
total_gb: Option<f32>, total_gb: Option<f32>,
status: Status, status: Status,
health_status: Status, // Separate status for pool health vs usage
} }
#[derive(Clone)] #[derive(Clone)]
@ -88,6 +89,7 @@ impl SystemWidget {
tmp_total_gb: None, tmp_total_gb: None,
memory_status: Status::Unknown, memory_status: Status::Unknown,
tmp_status: Status::Unknown, tmp_status: Status::Unknown,
tmpfs_mounts: Vec::new(),
storage_pools: Vec::new(), storage_pools: Vec::new(),
has_data: false, has_data: false,
} }
@ -121,20 +123,6 @@ impl SystemWidget {
} }
} }
/// Format /tmp usage
fn format_tmp_usage(&self) -> String {
match (self.tmp_usage_percent, self.tmp_used_gb, self.tmp_total_gb) {
(Some(pct), Some(used), Some(total)) => {
let used_str = if used < 0.1 {
format!("{:.0}B", used * 1024.0) // Show as MB if very small
} else {
format!("{:.1}GB", used)
};
format!("{:.0}% {}/{:.1}GB", pct, used_str, total)
}
_ => "—% —GB/—GB".to_string(),
}
}
/// Get the current agent hash for rebuild completion detection /// Get the current agent hash for rebuild completion detection
pub fn _get_agent_hash(&self) -> Option<&String> { pub fn _get_agent_hash(&self) -> Option<&String> {
@ -166,7 +154,10 @@ impl Widget for SystemWidget {
self.memory_total_gb = Some(memory.total_gb); self.memory_total_gb = Some(memory.total_gb);
self.memory_status = Status::Ok; self.memory_status = Status::Ok;
// Extract tmpfs data // Store all tmpfs mounts for display
self.tmpfs_mounts = memory.tmpfs.clone();
// Extract tmpfs data (maintain backward compatibility for /tmp)
if let Some(tmp_data) = memory.tmpfs.iter().find(|t| t.mount == "/tmp") { if let Some(tmp_data) = memory.tmpfs.iter().find(|t| t.mount == "/tmp") {
self.tmp_usage_percent = Some(tmp_data.usage_percent); self.tmp_usage_percent = Some(tmp_data.usage_percent);
self.tmp_used_gb = Some(tmp_data.used_gb); self.tmp_used_gb = Some(tmp_data.used_gb);
@ -196,7 +187,6 @@ impl SystemWidget {
used_gb: None, used_gb: None,
total_gb: None, total_gb: None,
status: Status::Ok, status: Status::Ok,
health_status: Status::Ok,
}; };
// Add drive info // Add drive info
@ -278,40 +268,27 @@ impl SystemWidget {
let pool_spans = StatusIcons::create_status_spans(pool.status.clone(), &pool_label); let pool_spans = StatusIcons::create_status_spans(pool.status.clone(), &pool_label);
lines.push(Line::from(pool_spans)); lines.push(Line::from(pool_spans));
// Pool total usage line // Show individual filesystems for physical drives (matching CLAUDE.md format)
if let (Some(usage), Some(used), Some(total)) = (pool.usage_percent, pool.used_gb, pool.total_gb) {
let usage_spans = vec![
Span::styled(" ├─ ", Typography::tree()),
Span::raw(" "),
];
let mut usage_line_spans = usage_spans;
usage_line_spans.extend(StatusIcons::create_status_spans(pool.status.clone(), &format!("Total: {}% {:.1}GB/{:.1}GB", usage as i32, used, total)));
lines.push(Line::from(usage_line_spans));
}
// Drive details for physical drives
if pool.pool_type.starts_with("drive") { if pool.pool_type.starts_with("drive") {
for drive in &pool.drives { // Show filesystem entries like: ├─ ● /: 55% 250.5GB/456.4GB
if drive.name == pool.name { for (i, filesystem) in pool.filesystems.iter().enumerate() {
let mut drive_details = Vec::new(); let is_last = i == pool.filesystems.len() - 1;
if let Some(temp) = drive.temperature { let tree_symbol = if is_last { " └─ " } else { " ├─ " };
drive_details.push(format!("T: {}°C", temp as i32));
}
if let Some(wear) = drive.wear_percent {
drive_details.push(format!("W: {}%", wear as i32));
}
if !drive_details.is_empty() { let fs_text = format!("{}: {:.0}% {:.1}GB/{:.1}GB",
let drive_text = format!("{} {}", drive.name, drive_details.join(" ")); filesystem.mount_point,
let drive_spans = vec![ filesystem.usage_percent.unwrap_or(0.0),
Span::styled(" └─ ", Typography::tree()), filesystem.used_gb.unwrap_or(0.0),
Span::raw(" "), filesystem.total_gb.unwrap_or(0.0));
let mut fs_spans = vec![
Span::styled(tree_symbol, Typography::tree()),
]; ];
let mut drive_line_spans = drive_spans; fs_spans.extend(StatusIcons::create_status_spans(
drive_line_spans.extend(StatusIcons::create_status_spans(drive.status.clone(), &drive_text)); filesystem.status.clone(),
lines.push(Line::from(drive_line_spans)); &fs_text
} ));
} lines.push(Line::from(fs_spans));
} }
} else { } else {
// For mergerfs pools, show data drives and parity drives in tree structure // For mergerfs pools, show data drives and parity drives in tree structure
@ -432,15 +409,29 @@ impl SystemWidget {
); );
lines.push(Line::from(memory_spans)); lines.push(Line::from(memory_spans));
let tmp_text = self.format_tmp_usage(); // Display all tmpfs mounts
let mut tmp_spans = vec![ for (i, tmpfs) in self.tmpfs_mounts.iter().enumerate() {
Span::styled(" └─ ", Typography::tree()), let is_last = i == self.tmpfs_mounts.len() - 1;
let tree_symbol = if is_last { " └─ " } else { " ├─ " };
let usage_text = if tmpfs.total_gb > 0.0 {
format!("{:.0}% {:.1}GB/{:.1}GB",
tmpfs.usage_percent,
tmpfs.used_gb,
tmpfs.total_gb)
} else {
"— —/—".to_string()
};
let mut tmpfs_spans = vec![
Span::styled(tree_symbol, Typography::tree()),
]; ];
tmp_spans.extend(StatusIcons::create_status_spans( tmpfs_spans.extend(StatusIcons::create_status_spans(
self.tmp_status.clone(), Status::Ok, // TODO: Calculate status based on usage_percent
&format!("/tmp: {}", tmp_text) &format!("{}: {}", tmpfs.mount, usage_text)
)); ));
lines.push(Line::from(tmp_spans)); lines.push(Line::from(tmpfs_spans));
}
// Storage section // Storage section
lines.push(Line::from(vec![ lines.push(Line::from(vec![

View File

@ -1,6 +1,6 @@
[package] [package]
name = "cm-dashboard-shared" name = "cm-dashboard-shared"
version = "0.1.137" version = "0.1.138"
edition = "2021" edition = "2021"
[dependencies] [dependencies]