Update version to 0.1.22 and fix system metric status calculation
All checks were successful
Build and Release / build-and-release (push) Successful in 1m11s

- Fix /tmp usage status to use proper thresholds instead of hardcoded Ok status
- Fix wear level status to use configurable thresholds instead of hardcoded values
- Add dedicated tmp_status field to SystemWidget for proper /tmp status display
- Remove host-level hourglass icon during service operations
- Implement immediate service status updates after start/stop/restart commands
- Remove active users display and collection from NixOS section
- Fix immediate host status aggregation transmission to dashboard
This commit is contained in:
Christoffer Martinsson 2025-10-28 13:21:56 +01:00
parent 43242debce
commit 2910b7d875
12 changed files with 51 additions and 105 deletions

6
Cargo.lock generated
View File

@ -270,7 +270,7 @@ checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d"
[[package]] [[package]]
name = "cm-dashboard" name = "cm-dashboard"
version = "0.1.20" version = "0.1.21"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"chrono", "chrono",
@ -291,7 +291,7 @@ dependencies = [
[[package]] [[package]]
name = "cm-dashboard-agent" name = "cm-dashboard-agent"
version = "0.1.20" version = "0.1.21"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",
@ -314,7 +314,7 @@ dependencies = [
[[package]] [[package]]
name = "cm-dashboard-shared" name = "cm-dashboard-shared"
version = "0.1.20" version = "0.1.21"
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.21" version = "0.1.22"
edition = "2021" edition = "2021"
[dependencies] [dependencies]

View File

@ -270,7 +270,7 @@ impl Agent {
} }
/// Handle systemd service control commands /// Handle systemd service control commands
async fn handle_service_control(&self, service_name: &str, action: &ServiceAction) -> Result<()> { async fn handle_service_control(&mut self, service_name: &str, action: &ServiceAction) -> Result<()> {
let action_str = match action { let action_str = match action {
ServiceAction::Start => "start", ServiceAction::Start => "start",
ServiceAction::Stop => "stop", ServiceAction::Stop => "stop",
@ -300,9 +300,12 @@ impl Agent {
// Force refresh metrics after service control to update service status // Force refresh metrics after service control to update service status
if matches!(action, ServiceAction::Start | ServiceAction::Stop | ServiceAction::Restart) { if matches!(action, ServiceAction::Start | ServiceAction::Stop | ServiceAction::Restart) {
info!("Triggering metric refresh after service control"); info!("Triggering immediate metric refresh after service control");
// Note: We can't call self.collect_metrics_only() here due to borrowing issues if let Err(e) = self.collect_metrics_only().await {
// The next metric collection cycle will pick up the changes error!("Failed to refresh metrics after service control: {}", e);
} else {
info!("Service status refreshed immediately after {} {}", action_str, service_name);
}
} }
Ok(()) Ok(())

View File

@ -556,8 +556,8 @@ impl Collector for DiskCollector {
// Drive wear level (for SSDs) // Drive wear level (for SSDs)
if let Some(wear) = drive.wear_level { if let Some(wear) = drive.wear_level {
let wear_status = if wear >= 90.0 { Status::Critical } let wear_status = if wear >= self.config.wear_critical_percent { Status::Critical }
else if wear >= 80.0 { Status::Warning } else if wear >= self.config.wear_warning_percent { Status::Warning }
else { Status::Ok }; else { Status::Ok };
metrics.push(Metric { metrics.push(Metric {

View File

@ -187,7 +187,7 @@ impl MemoryCollector {
} }
// Monitor tmpfs (/tmp) usage // Monitor tmpfs (/tmp) usage
if let Ok(tmpfs_metrics) = self.get_tmpfs_metrics() { if let Ok(tmpfs_metrics) = self.get_tmpfs_metrics(status_tracker) {
metrics.extend(tmpfs_metrics); metrics.extend(tmpfs_metrics);
} }
@ -195,7 +195,7 @@ impl MemoryCollector {
} }
/// Get tmpfs (/tmp) usage metrics /// Get tmpfs (/tmp) usage metrics
fn get_tmpfs_metrics(&self) -> Result<Vec<Metric>, CollectorError> { fn get_tmpfs_metrics(&self, status_tracker: &mut StatusTracker) -> Result<Vec<Metric>, CollectorError> {
use std::process::Command; use std::process::Command;
let output = Command::new("df") let output = Command::new("df")
@ -249,12 +249,15 @@ impl MemoryCollector {
let mut metrics = Vec::new(); let mut metrics = Vec::new();
let timestamp = chrono::Utc::now().timestamp() as u64; let timestamp = chrono::Utc::now().timestamp() as u64;
// Calculate status using same thresholds as main memory
let tmp_status = self.calculate_usage_status("memory_tmp_usage_percent", usage_percent, status_tracker);
metrics.push(Metric { metrics.push(Metric {
name: "memory_tmp_usage_percent".to_string(), name: "memory_tmp_usage_percent".to_string(),
value: MetricValue::Float(usage_percent), value: MetricValue::Float(usage_percent),
unit: Some("%".to_string()), unit: Some("%".to_string()),
description: Some("tmpfs /tmp usage percentage".to_string()), description: Some("tmpfs /tmp usage percentage".to_string()),
status: Status::Ok, status: tmp_status,
timestamp, timestamp,
}); });

View File

@ -10,7 +10,6 @@ use crate::config::NixOSConfig;
/// ///
/// Collects NixOS-specific system information including: /// Collects NixOS-specific system information including:
/// - NixOS version and build information /// - NixOS version and build information
/// - Currently active/logged in users
pub struct NixOSCollector { pub struct NixOSCollector {
} }
@ -65,27 +64,6 @@ impl NixOSCollector {
Err("Could not extract hash from nix store path".into()) Err("Could not extract hash from nix store path".into())
} }
/// Get currently active users
fn get_active_users(&self) -> Result<Vec<String>, Box<dyn std::error::Error>> {
let output = Command::new("who").output()?;
if !output.status.success() {
return Err("who command failed".into());
}
let who_output = String::from_utf8_lossy(&output.stdout);
let mut users = std::collections::HashSet::new();
for line in who_output.lines() {
if let Some(username) = line.split_whitespace().next() {
if !username.is_empty() {
users.insert(username.to_string());
}
}
}
Ok(users.into_iter().collect())
}
} }
#[async_trait] #[async_trait]
@ -121,31 +99,6 @@ impl Collector for NixOSCollector {
} }
} }
// Collect active users
match self.get_active_users() {
Ok(users) => {
let users_str = users.join(", ");
metrics.push(Metric {
name: "system_active_users".to_string(),
value: MetricValue::String(users_str),
unit: None,
description: Some("Currently active users".to_string()),
status: Status::Ok,
timestamp,
});
}
Err(e) => {
debug!("Failed to get active users: {}", e);
metrics.push(Metric {
name: "system_active_users".to_string(),
value: MetricValue::String("unknown".to_string()),
unit: None,
description: Some("Active users (failed to detect)".to_string()),
status: Status::Unknown,
timestamp,
});
}
}
// Collect config hash // Collect config hash
match self.get_config_hash() { match self.get_config_hash() {

View File

@ -160,27 +160,37 @@ impl HostStatusManager {
/// Process a metric - updates status and queues for aggregated notifications if status changed /// Process a metric - updates status and queues for aggregated notifications if status changed
pub async fn process_metric(&mut self, metric: &Metric, _notification_manager: &mut crate::notifications::NotificationManager) -> bool { pub async fn process_metric(&mut self, metric: &Metric, _notification_manager: &mut crate::notifications::NotificationManager) -> bool {
let old_status = self.service_statuses.get(&metric.name).copied(); let old_service_status = self.service_statuses.get(&metric.name).copied();
let new_status = metric.status; let old_host_status = self.current_host_status;
let new_service_status = metric.status;
// Update status // Update status (this recalculates host status internally)
self.update_service_status(metric.name.clone(), new_status); self.update_service_status(metric.name.clone(), new_service_status);
// Check if status actually changed (ignore first-time status setting) let new_host_status = self.current_host_status;
if let Some(old_status) = old_status { let mut status_changed = false;
if old_status != new_status {
debug!("Status change detected for {}: {:?} -> {:?}", metric.name, old_status, new_status); // Check if service status actually changed (ignore first-time status setting)
if let Some(old_service_status) = old_service_status {
if old_service_status != new_service_status {
debug!("Service status change detected for {}: {:?} -> {:?}", metric.name, old_service_status, new_service_status);
// Queue change for aggregated notification (not immediate) // Queue change for aggregated notification (not immediate)
self.queue_status_change(&metric.name, old_status, new_status); self.queue_status_change(&metric.name, old_service_status, new_service_status);
return true; // Status changed - caller should trigger immediate transmission status_changed = true;
} }
} else { } else {
debug!("Initial status set for {}: {:?}", metric.name, new_status); debug!("Initial status set for {}: {:?}", metric.name, new_service_status);
} }
false // No status change (or first-time status) // Check if host status changed (this should trigger immediate transmission)
if old_host_status != new_host_status {
debug!("Host status change detected: {:?} -> {:?}", old_host_status, new_host_status);
status_changed = true;
}
status_changed // Return true if either service or host status changed
} }
/// Queue status change for aggregated notification /// Queue status change for aggregated notification

View File

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

View File

@ -14,7 +14,7 @@ use app::Dashboard;
/// Get hardcoded version /// Get hardcoded version
fn get_version() -> &'static str { fn get_version() -> &'static str {
"v0.1.21" "v0.1.22"
} }
/// Check if running inside tmux session /// Check if running inside tmux session

View File

@ -724,24 +724,9 @@ impl TuiApp {
spans.push(Span::styled(" ", Typography::title())); spans.push(Span::styled(" ", Typography::title()));
} }
// Check if this host has a command status that affects the icon // Always show normal status icon based on metrics (no command status at host level)
let (status_icon, status_color) = if let Some(host_widgets) = self.host_widgets.get(host) { let host_status = self.calculate_host_status(host, metric_store);
match &host_widgets.command_status { let (status_icon, status_color) = (StatusIcons::get_icon(host_status), Theme::status_color(host_status));
Some(CommandStatus::InProgress { .. }) => {
// Show working indicator for in-progress commands
("", Theme::highlight())
}
_ => {
// 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
let host_status = self.calculate_host_status(host, metric_store);
(StatusIcons::get_icon(host_status), Theme::status_color(host_status))
};
// Add status icon // Add status icon
spans.push(Span::styled( spans.push(Span::styled(

View File

@ -15,7 +15,6 @@ pub struct SystemWidget {
// NixOS information // NixOS information
nixos_build: Option<String>, nixos_build: Option<String>,
config_hash: Option<String>, config_hash: Option<String>,
active_users: Option<String>,
agent_hash: Option<String>, agent_hash: Option<String>,
// CPU metrics // CPU metrics
@ -33,6 +32,7 @@ pub struct SystemWidget {
tmp_used_gb: Option<f32>, tmp_used_gb: Option<f32>,
tmp_total_gb: Option<f32>, tmp_total_gb: Option<f32>,
memory_status: Status, memory_status: Status,
tmp_status: Status,
// Storage metrics (collected from disk metrics) // Storage metrics (collected from disk metrics)
storage_pools: Vec<StoragePool>, storage_pools: Vec<StoragePool>,
@ -66,7 +66,6 @@ impl SystemWidget {
Self { Self {
nixos_build: None, nixos_build: None,
config_hash: None, config_hash: None,
active_users: None,
agent_hash: None, agent_hash: None,
cpu_load_1min: None, cpu_load_1min: None,
cpu_load_5min: None, cpu_load_5min: None,
@ -80,6 +79,7 @@ impl SystemWidget {
tmp_used_gb: None, tmp_used_gb: None,
tmp_total_gb: None, tmp_total_gb: None,
memory_status: Status::Unknown, memory_status: Status::Unknown,
tmp_status: Status::Unknown,
storage_pools: Vec::new(), storage_pools: Vec::new(),
has_data: false, has_data: false,
} }
@ -334,11 +334,6 @@ impl Widget for SystemWidget {
self.config_hash = Some(hash.clone()); self.config_hash = Some(hash.clone());
} }
} }
"system_active_users" => {
if let MetricValue::String(users) = &metric.value {
self.active_users = Some(users.clone());
}
}
"agent_version" => { "agent_version" => {
if let MetricValue::String(version) = &metric.value { if let MetricValue::String(version) = &metric.value {
self.agent_hash = Some(version.clone()); self.agent_hash = Some(version.clone());
@ -390,6 +385,7 @@ impl Widget for SystemWidget {
"memory_tmp_usage_percent" => { "memory_tmp_usage_percent" => {
if let MetricValue::Float(usage) = metric.value { if let MetricValue::Float(usage) = metric.value {
self.tmp_usage_percent = Some(usage); self.tmp_usage_percent = Some(usage);
self.tmp_status = metric.status.clone();
} }
} }
"memory_tmp_used_gb" => { "memory_tmp_used_gb" => {
@ -432,10 +428,6 @@ impl SystemWidget {
Span::styled(format!("Agent: {}", agent_version_text), Typography::secondary()) Span::styled(format!("Agent: {}", agent_version_text), Typography::secondary())
])); ]));
let users_text = self.active_users.as_deref().unwrap_or("unknown");
lines.push(Line::from(vec![
Span::styled(format!("Active users: {}", users_text), Typography::secondary())
]));
// CPU section // CPU section
lines.push(Line::from(vec![ lines.push(Line::from(vec![
@ -472,7 +464,7 @@ impl SystemWidget {
Span::styled(" └─ ", Typography::tree()), Span::styled(" └─ ", Typography::tree()),
]; ];
tmp_spans.extend(StatusIcons::create_status_spans( tmp_spans.extend(StatusIcons::create_status_spans(
self.memory_status.clone(), self.tmp_status.clone(),
&format!("/tmp: {}", tmp_text) &format!("/tmp: {}", tmp_text)
)); ));
lines.push(Line::from(tmp_spans)); lines.push(Line::from(tmp_spans));

View File

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