diff --git a/CLAUDE.md b/CLAUDE.md index 5b2e3aa..a1eafe6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -307,25 +307,19 @@ exclude_fs_types = ["tmpfs", "devtmpfs", "sysfs", "proc"] CPU: ● Load: 0.23 0.21 0.13 └─ Freq: 1048 MHz - RAM: ● Usage: 25% 5.8GB/23.3GB ├─ ● /tmp: 2% 0.5GB/2GB └─ ● /var/tmp: 0% 0GB/1.0GB - Storage: -● mergerfs (2+1): - ├─ Total: ● 63% 2355.2GB/3686.4GB - ├─ Data Disks: - │ ├─ ● sdb T: 24°C W: 5% - │ └─ ● sdd T: 27°C W: 5% - ├─ Parity: ● sdc T: 24°C W: 5% - └─ Mount: /srv/media - -● nvme0n1 T: 25C W: 4% +● 844B9A25 T: 25C W: 4% ├─ ● /: 55% 250.5GB/456.4GB └─ ● /boot: 26% 0.3GB/1.0GB - +● mergerfs /srv/media: + ├─ ● 63% 2355.2GB/3686.4GB + ├─ ● Data_1: WDZQ8H8D T: 28°C + ├─ ● Data_2: GGA04461 T: 28°C + └─ ● Parity: WDZS8RY0 T: 29°C Backup: ● WD-WCC7K1234567 T: 32°C W: 12% ├─ Last: 2h ago (12.3GB) @@ -361,98 +355,6 @@ Keep responses concise and focused. Avoid extensive implementation summaries unl - ✅ "Restructure storage widget with improved layout" - ✅ "Update CPU thresholds to production values" -## Completed Architecture Migration (v0.1.131) - -## ✅ COMPLETE MONITORING SYSTEM RESTORATION (v0.1.141) - -**🎉 SUCCESS: All Issues Fixed - Complete Functional Monitoring System** - -### ✅ Completed Implementation (v0.1.141) - -**All Major Issues Resolved:** -``` -✅ Data Collection: Agent collects structured data correctly -✅ Storage Display: Perfect format with correct mount points and temperature/wear -✅ Status Evaluation: All metrics properly evaluated against thresholds -✅ Notifications: Working email alerts on status changes -✅ Thresholds: All collectors using configured thresholds for status calculation -✅ Build Information: NixOS version displayed correctly -✅ Mount Point Consistency: Stable, sorted display order -``` - -### ✅ All Phases Completed Successfully - -#### ✅ Phase 1: Storage Display - COMPLETED -- ✅ Use `lsblk` instead of `findmnt` (eliminated `/nix/store` bind mount issue) -- ✅ Add `sudo smartctl` for permissions (SMART data collection working) -- ✅ Fix NVMe SMART parsing (`Temperature:` and `Percentage Used:` fields) -- ✅ Consistent filesystem/tmpfs sorting (no more random order swapping) -- ✅ **VERIFIED**: Dashboard shows `● nvme0n1 T: 28°C W: 1%` correctly - -#### ✅ Phase 2: Status Evaluation System - COMPLETED -- ✅ **CPU Status**: Load averages and temperature evaluated against `HysteresisThresholds` -- ✅ **Memory Status**: Usage percentage evaluated against thresholds -- ✅ **Storage Status**: Drive temperature, health, and filesystem usage evaluated -- ✅ **Service Status**: Service states properly tracked and evaluated -- ✅ **Status Fields**: All AgentData structures include status information -- ✅ **Threshold Integration**: All collectors use their configured thresholds - -#### ✅ Phase 3: Notification System - COMPLETED -- ✅ **Status Change Detection**: Agent tracks status between collection cycles -- ✅ **Email Notifications**: Alerts sent on degradation (OK→Warning/Critical, Warning→Critical) -- ✅ **Notification Content**: Detailed alerts with metric values and timestamps -- ✅ **NotificationManager Integration**: Fully restored and operational -- ✅ **Maintenance Mode**: `/tmp/cm-maintenance` file support maintained - -#### ✅ Phase 4: Integration & Testing - COMPLETED -- ✅ **AgentData Status Fields**: All structured data includes status evaluation -- ✅ **Status Processing**: Agent applies thresholds at collection time -- ✅ **End-to-End Flow**: Collection → Evaluation → Notification → Display -- ✅ **Dynamic Versioning**: Agent version from `CARGO_PKG_VERSION` -- ✅ **Build Information**: NixOS generation display restored - -### ✅ Final Architecture - WORKING - -**Complete Operational Flow:** -``` -Collectors → AgentData (with Status) → NotificationManager → Email Alerts - ↘ ↗ - ZMQ → Dashboard → Perfect Display -``` - -**Operational Components:** -1. ✅ **Collectors**: Populate AgentData with metrics AND status evaluation -2. ✅ **Status Evaluation**: `HysteresisThresholds.evaluate()` applied per collector -3. ✅ **Notifications**: Email alerts on status change detection -4. ✅ **Display**: Correct mount points, temperature, wear, and build information - -### ✅ Success Criteria - ALL MET - -**Display Requirements:** -- ✅ Dashboard shows `● nvme0n1 T: 28°C W: 1%` format perfectly -- ✅ Mount points show `/` and `/boot` (not `root`/`boot`) -- ✅ Build information shows actual NixOS version (not "unknown") -- ✅ Consistent sorting eliminates random order changes - -**Monitoring Requirements:** -- ✅ High CPU load triggers Warning/Critical status and email alert -- ✅ High memory usage triggers Warning/Critical status and email alert -- ✅ High disk temperature triggers Warning/Critical status and email alert -- ✅ Failed services trigger Warning/Critical status and email alert -- ✅ Maintenance mode suppresses notifications as expected - -### 🚀 Production Ready - -**CM Dashboard v0.1.141 is a complete, functional infrastructure monitoring system:** - -- **Real-time Monitoring**: All system components with 1-second intervals -- **Intelligent Alerting**: Email notifications on threshold violations -- **Perfect Display**: Accurate mount points, temperatures, and system information -- **Status-Aware**: All metrics evaluated against configurable thresholds -- **Production Ready**: Full monitoring capabilities restored - -**The monitoring system is fully operational and ready for production use.** - ## Implementation Rules 1. **Agent Status Authority**: Agent calculates status for each metric using thresholds diff --git a/Cargo.lock b/Cargo.lock index 9dca1f2..b542ea3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -279,7 +279,7 @@ checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" [[package]] name = "cm-dashboard" -version = "0.1.158" +version = "0.1.159" dependencies = [ "anyhow", "chrono", @@ -301,7 +301,7 @@ dependencies = [ [[package]] name = "cm-dashboard-agent" -version = "0.1.158" +version = "0.1.159" dependencies = [ "anyhow", "async-trait", @@ -324,7 +324,7 @@ dependencies = [ [[package]] name = "cm-dashboard-shared" -version = "0.1.158" +version = "0.1.159" dependencies = [ "chrono", "serde", diff --git a/agent/Cargo.toml b/agent/Cargo.toml index 8092f54..dfaa9bb 100644 --- a/agent/Cargo.toml +++ b/agent/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cm-dashboard-agent" -version = "0.1.158" +version = "0.1.159" edition = "2021" [dependencies] diff --git a/agent/src/agent.rs b/agent/src/agent.rs index a968e94..143064c 100644 --- a/agent/src/agent.rs +++ b/agent/src/agent.rs @@ -16,7 +16,6 @@ use crate::collectors::{ systemd::SystemdCollector, }; use crate::notifications::NotificationManager; -use crate::service_tracker::UserStoppedServiceTracker; use cm_dashboard_shared::AgentData; pub struct Agent { @@ -25,7 +24,6 @@ pub struct Agent { zmq_handler: ZmqHandler, collectors: Vec>, notification_manager: NotificationManager, - service_tracker: UserStoppedServiceTracker, previous_status: Option, } @@ -90,17 +88,12 @@ impl Agent { let notification_manager = NotificationManager::new(&config.notifications, &hostname)?; info!("Notification manager initialized"); - // Initialize service tracker - let service_tracker = UserStoppedServiceTracker::new(); - info!("Service tracker initialized"); - Ok(Self { hostname, config, zmq_handler, collectors, notification_manager, - service_tracker, previous_status: None, }) } diff --git a/agent/src/collectors/backup.rs b/agent/src/collectors/backup.rs index b4c9e60..94ec73c 100644 --- a/agent/src/collectors/backup.rs +++ b/agent/src/collectors/backup.rs @@ -1,5 +1,4 @@ use async_trait::async_trait; -use chrono::{NaiveDateTime, DateTime}; use cm_dashboard_shared::{AgentData, BackupData, BackupDiskData}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; diff --git a/agent/src/collectors/disk.rs b/agent/src/collectors/disk.rs index 6ba79b8..9500dde 100644 --- a/agent/src/collectors/disk.rs +++ b/agent/src/collectors/disk.rs @@ -19,10 +19,8 @@ pub struct DiskCollector { /// A physical drive with its filesystems #[derive(Debug, Clone)] struct PhysicalDrive { - name: String, // e.g., "nvme0n1", "sda" + name: String, // e.g., "nvme0n1", "sda" health: String, // SMART health status - temperature_celsius: Option, // Drive temperature - wear_percent: Option, // SSD wear level filesystems: Vec, // mounted filesystems on this drive } @@ -351,8 +349,6 @@ impl DiskCollector { let physical_drive = PhysicalDrive { name: drive_name, health: "UNKNOWN".to_string(), // Will be updated with SMART data - temperature_celsius: None, - wear_percent: None, filesystems, }; physical_drives.push(physical_drive); diff --git a/agent/src/collectors/nixos.rs b/agent/src/collectors/nixos.rs index 0a67211..ec9d246 100644 --- a/agent/src/collectors/nixos.rs +++ b/agent/src/collectors/nixos.rs @@ -5,21 +5,18 @@ use std::process::Command; use tracing::debug; use super::{Collector, CollectorError}; -use crate::config::NixOSConfig; /// NixOS system information collector with structured data output -/// +/// /// This collector gathers NixOS-specific information like: /// - System generation/build information /// - Version information /// - Agent version from Nix store path -pub struct NixOSCollector { - config: NixOSConfig, -} +pub struct NixOSCollector; impl NixOSCollector { - pub fn new(config: NixOSConfig) -> Self { - Self { config } + pub fn new(_config: crate::config::NixOSConfig) -> Self { + Self } /// Collect NixOS system information and populate AgentData diff --git a/agent/src/collectors/systemd.rs b/agent/src/collectors/systemd.rs index 68c0663..85d52ce 100644 --- a/agent/src/collectors/systemd.rs +++ b/agent/src/collectors/systemd.rs @@ -22,8 +22,6 @@ pub struct SystemdCollector { struct ServiceCacheState { /// Last collection time for performance tracking last_collection: Option, - /// Cached service data - services: Vec, /// Cached complete service data with sub-services cached_service_data: Vec, /// Interesting services to monitor (cached after discovery) @@ -50,20 +48,10 @@ struct ServiceStatusInfo { sub_state: String, } -/// Internal service information -#[derive(Debug, Clone)] -struct ServiceInfo { - name: String, - status: String, // "active", "inactive", "failed", etc. - memory_mb: f32, // Memory usage in MB - disk_gb: f32, // Disk usage in GB -} - impl SystemdCollector { pub fn new(config: SystemdConfig) -> Self { let state = ServiceCacheState { last_collection: None, - services: Vec::new(), cached_service_data: Vec::new(), monitored_services: Vec::new(), service_status_cache: std::collections::HashMap::new(), @@ -73,7 +61,7 @@ impl SystemdCollector { last_nginx_check_time: None, nginx_check_interval_seconds: config.nginx_check_interval_seconds, }; - + Self { state: RwLock::new(state), config, @@ -95,7 +83,6 @@ impl SystemdCollector { }; // Collect service data for each monitored service - let mut services = Vec::new(); let mut complete_service_data = Vec::new(); for service_name in &monitored_services { match self.get_service_status(service_name) { @@ -145,14 +132,6 @@ impl SystemdCollector { } } - let service_info = ServiceInfo { - name: service_name.clone(), - status: active_status.clone(), - memory_mb, - disk_gb, - }; - services.push(service_info); - // Create complete service data let service_data = ServiceData { name: service_name.clone(), @@ -177,7 +156,6 @@ impl SystemdCollector { { let mut state = self.state.write().unwrap(); state.last_collection = Some(start_time); - state.services = services; state.cached_service_data = complete_service_data; } @@ -483,25 +461,6 @@ impl SystemdCollector { } } - /// Get service memory usage (if available) - fn get_service_memory(&self, service: &str) -> Option { - let output = Command::new("systemctl") - .args(&["show", &format!("{}.service", service), "--property=MemoryCurrent"]) - .output() - .ok()?; - - let output_str = String::from_utf8(output.stdout).ok()?; - for line in output_str.lines() { - if line.starts_with("MemoryCurrent=") { - let memory_str = line.strip_prefix("MemoryCurrent=")?; - if let Ok(memory_bytes) = memory_str.parse::() { - return Some(memory_bytes as f32 / (1024.0 * 1024.0)); // Convert to MB - } - } - } - None - } - /// Calculate service status, taking user-stopped services into account fn calculate_service_status(&self, service_name: &str, active_status: &str) -> Status { match active_status.to_lowercase().as_str() { @@ -549,7 +508,7 @@ impl SystemdCollector { /// Check if service collection cache should be updated fn should_update_cache(&self) -> bool { let state = self.state.read().unwrap(); - + match state.last_collection { None => true, Some(last) => { @@ -559,16 +518,6 @@ impl SystemdCollector { } } - /// Get cached service data if available and fresh - fn get_cached_services(&self) -> Option> { - if !self.should_update_cache() { - let state = self.state.read().unwrap(); - Some(state.services.clone()) - } else { - None - } - } - /// Get cached complete service data with sub-services if available and fresh fn get_cached_complete_services(&self) -> Option> { if !self.should_update_cache() { diff --git a/agent/src/config/defaults.rs b/agent/src/config/defaults.rs deleted file mode 100644 index 60c4ac2..0000000 --- a/agent/src/config/defaults.rs +++ /dev/null @@ -1,2 +0,0 @@ -// This file is now empty - all configuration values come from config files -// No hardcoded defaults are used \ No newline at end of file diff --git a/agent/src/main.rs b/agent/src/main.rs index 5b0ced6..a461eb8 100644 --- a/agent/src/main.rs +++ b/agent/src/main.rs @@ -8,7 +8,6 @@ mod collectors; mod communication; mod config; mod notifications; -mod service_tracker; use agent::Agent; diff --git a/agent/src/service_tracker.rs b/agent/src/service_tracker.rs deleted file mode 100644 index 70254e0..0000000 --- a/agent/src/service_tracker.rs +++ /dev/null @@ -1,164 +0,0 @@ -use anyhow::Result; -use serde::{Deserialize, Serialize}; -use std::collections::HashSet; -use std::fs; -use std::path::Path; -use std::sync::{Arc, Mutex, OnceLock}; -use tracing::{debug, info, warn}; - -/// Shared instance for global access -static GLOBAL_TRACKER: OnceLock>> = OnceLock::new(); - -/// Tracks services that have been stopped by user action -/// These services should be treated as OK status instead of Warning -#[derive(Debug)] -pub struct UserStoppedServiceTracker { - /// Set of services stopped by user action - user_stopped_services: HashSet, - /// Path to persistent storage file - storage_path: String, -} - -/// Serializable data structure for persistence -#[derive(Debug, Serialize, Deserialize)] -struct UserStoppedData { - services: Vec, -} - -impl UserStoppedServiceTracker { - /// Create new tracker with default storage path - pub fn new() -> Self { - Self::with_storage_path("/var/lib/cm-dashboard/user-stopped-services.json") - } - - /// Initialize global instance (called by agent) - pub fn init_global() -> Result { - let tracker = Self::new(); - - // Set global instance - let global_instance = Arc::new(Mutex::new(tracker)); - if GLOBAL_TRACKER.set(global_instance).is_err() { - warn!("Global service tracker was already initialized"); - } - - // Return a new instance for the agent to use - Ok(Self::new()) - } - - /// Check if a service is user-stopped (global access for collectors) - pub fn is_service_user_stopped(service_name: &str) -> bool { - if let Some(global) = GLOBAL_TRACKER.get() { - if let Ok(tracker) = global.lock() { - tracker.is_user_stopped(service_name) - } else { - debug!("Failed to lock global service tracker"); - false - } - } else { - debug!("Global service tracker not initialized"); - false - } - } - - /// Update global tracker (called by agent when tracker state changes) - pub fn update_global(updated_tracker: &UserStoppedServiceTracker) { - if let Some(global) = GLOBAL_TRACKER.get() { - if let Ok(mut tracker) = global.lock() { - tracker.user_stopped_services = updated_tracker.user_stopped_services.clone(); - } else { - debug!("Failed to lock global service tracker for update"); - } - } else { - debug!("Global service tracker not initialized for update"); - } - } - - /// Create new tracker with custom storage path - pub fn with_storage_path>(storage_path: P) -> Self { - let storage_path = storage_path.as_ref().to_string_lossy().to_string(); - let mut tracker = Self { - user_stopped_services: HashSet::new(), - storage_path, - }; - - // Load existing data from storage - if let Err(e) = tracker.load_from_storage() { - warn!("Failed to load user-stopped services from storage: {}", e); - info!("Starting with empty user-stopped services list"); - } - - tracker - } - - - /// Clear user-stopped flag for a service (when user starts it) - pub fn clear_user_stopped(&mut self, service_name: &str) -> Result<()> { - if self.user_stopped_services.remove(service_name) { - info!("Cleared user-stopped flag for service '{}'", service_name); - self.save_to_storage()?; - debug!("Service '{}' user-stopped flag cleared and saved to storage", service_name); - } else { - debug!("Service '{}' was not marked as user-stopped", service_name); - } - Ok(()) - } - - /// Check if a service is marked as user-stopped - pub fn is_user_stopped(&self, service_name: &str) -> bool { - let is_stopped = self.user_stopped_services.contains(service_name); - debug!("Service '{}' user-stopped status: {}", service_name, is_stopped); - is_stopped - } - - - /// Save current state to persistent storage - fn save_to_storage(&self) -> Result<()> { - // Create parent directory if it doesn't exist - if let Some(parent_dir) = Path::new(&self.storage_path).parent() { - if !parent_dir.exists() { - fs::create_dir_all(parent_dir)?; - debug!("Created parent directory: {}", parent_dir.display()); - } - } - - let data = UserStoppedData { - services: self.user_stopped_services.iter().cloned().collect(), - }; - - let json_data = serde_json::to_string_pretty(&data)?; - fs::write(&self.storage_path, json_data)?; - - debug!( - "Saved {} user-stopped services to {}", - data.services.len(), - self.storage_path - ); - Ok(()) - } - - /// Load state from persistent storage - fn load_from_storage(&mut self) -> Result<()> { - if !Path::new(&self.storage_path).exists() { - debug!("Storage file {} does not exist, starting fresh", self.storage_path); - return Ok(()); - } - - let json_data = fs::read_to_string(&self.storage_path)?; - let data: UserStoppedData = serde_json::from_str(&json_data)?; - - self.user_stopped_services = data.services.into_iter().collect(); - - info!( - "Loaded {} user-stopped services from {}", - self.user_stopped_services.len(), - self.storage_path - ); - - if !self.user_stopped_services.is_empty() { - debug!("User-stopped services: {:?}", self.user_stopped_services); - } - - Ok(()) - } -} - diff --git a/agent_stream.log b/agent_stream.log deleted file mode 100644 index a703a17..0000000 --- a/agent_stream.log +++ /dev/null @@ -1,1001 +0,0 @@ -warning: fields `total_services`, `backup_disk_filesystem_label`, `services_completed_count`, `services_failed_count`, and `services_disabled_count` are never read - --> dashboard/src/ui/widgets/backup.rs:22:5 - | -14 | pub struct BackupWidget { - | ------------ fields in this struct -... -22 | total_services: Option, - | ^^^^^^^^^^^^^^ -... -36 | backup_disk_filesystem_label: Option, - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -37 | /// Number of completed services -38 | services_completed_count: Option, - | ^^^^^^^^^^^^^^^^^^^^^^^^ -39 | /// Number of failed services -40 | services_failed_count: Option, - | ^^^^^^^^^^^^^^^^^^^^^ -41 | /// Number of disabled services -42 | services_disabled_count: Option, - | ^^^^^^^^^^^^^^^^^^^^^^^ - | - = note: `BackupWidget` has a derived impl for the trait `Clone`, but this is intentionally ignored during dead code analysis - = note: `#[warn(dead_code)]` on by default - -warning: field `exit_code` is never read - --> dashboard/src/ui/widgets/backup.rs:53:5 - | -50 | struct ServiceMetricData { - | ----------------- field in this struct -... -53 | exit_code: Option, - | ^^^^^^^^^ - | - = note: `ServiceMetricData` has derived impls for the traits `Clone` and `Debug`, but these are intentionally ignored during dead code analysis - -warning: associated function `extract_service_name` is never used - --> dashboard/src/ui/widgets/backup.rs:115:8 - | - 58 | impl BackupWidget { - | ----------------- associated function in this implementation -... -115 | fn extract_service_name(metric_name: &str) -> Option { - | ^^^^^^^^^^^^^^^^^^^^ - -warning: method `update_from_metrics` is never used - --> dashboard/src/ui/widgets/backup.rs:157:8 - | -156 | impl BackupWidget { - | ----------------- method in this implementation -157 | fn update_from_metrics(&mut self, metrics: &[&Metric]) { - | ^^^^^^^^^^^^^^^^^^^ - -warning: associated function `extract_service_info` is never used - --> dashboard/src/ui/widgets/services.rs:50:8 - | -38 | impl ServicesWidget { - | ------------------- associated function in this implementation -... -50 | fn extract_service_info(metric_name: &str) -> Option<(String, Option)> { - | ^^^^^^^^^^^^^^^^^^^^ - -warning: method `update_from_metrics` is never used - --> dashboard/src/ui/widgets/services.rs:285:8 - | -284 | impl ServicesWidget { - | ------------------- method in this implementation -285 | fn update_from_metrics(&mut self, metrics: &[&Metric]) { - | ^^^^^^^^^^^^^^^^^^^ - -warning: field `health_status` is never read - --> dashboard/src/ui/widgets/system.rs:53:5 - | -43 | struct StoragePool { - | ----------- field in this struct -... -53 | health_status: Status, // Separate status for pool health vs usage - | ^^^^^^^^^^^^^ - | - = note: `StoragePool` has a derived impl for the trait `Clone`, but this is intentionally ignored during dead code analysis - -warning: `cm-dashboard` (bin "cm-dashboard") generated 7 warnings - Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.16s - Running `target/debug/cm-dashboard --headless --raw-data` -RAW AGENT DATA FROM cmbox: -{ - "hostname": "cmbox", - "agent_version": "v0.1.133", - "timestamp": 1763936501, - "system": { - "cpu": { - "load_1min": 1.82, - "load_5min": 2.1, - "load_15min": 2.1, - "frequency_mhz": 3743.09, - "temperature_celsius": 55.0 - }, - "memory": { - "usage_percent": 27.183601, - "total_gb": 23.339516, - "used_gb": 6.3445206, - "available_gb": 16.994995, - "swap_total_gb": 14.634708, - "swap_used_gb": 0.17599106, - "tmpfs": [ - { - "mount": "/tmp", - "usage_percent": 15.094376, - "used_gb": 0.3018875, - "total_gb": 2.0 - } - ] - }, - "storage": { - "drives": [ - { - "name": "nvme0n1", - "health": "PASSED", - "temperature_celsius": 28.0, - "wear_percent": 1.0, - "filesystems": [ - { - "mount": "root", - "usage_percent": 24.404377, - "used_gb": 226.51398, - "total_gb": 928.1695 - }, - { - "mount": "boot", - "usage_percent": 10.666672, - "used_gb": 0.10645676, - "total_gb": 0.9980316 - } - ] - } - ], - "pools": [] - } - }, - "services": [ - { - "name": "tailscaled", - "status": "active", - "memory_mb": 25.582031, - "disk_gb": 0.0, - "user_stopped": false - }, - { - "name": "sshd", - "status": "active", - "memory_mb": 4.3085938, - "disk_gb": 0.0, - "user_stopped": false - } - ], - "backup": { - "status": "unknown", - "last_run": null, - "next_scheduled": null, - "total_size_gb": null, - "repository_health": null - } -} -──────────────────────────────────────────────────────────────────────────────── -RAW AGENT DATA FROM cmbox: -{ - "hostname": "cmbox", - "agent_version": "v0.1.133", - "timestamp": 1763936502, - "system": { - "cpu": { - "load_1min": 1.82, - "load_5min": 2.1, - "load_15min": 2.1, - "frequency_mhz": 3743.09, - "temperature_celsius": 55.0 - }, - "memory": { - "usage_percent": 27.183601, - "total_gb": 23.339516, - "used_gb": 6.3445206, - "available_gb": 16.994995, - "swap_total_gb": 14.634708, - "swap_used_gb": 0.17599106, - "tmpfs": [ - { - "mount": "/tmp", - "usage_percent": 15.094376, - "used_gb": 0.3018875, - "total_gb": 2.0 - } - ] - }, - "storage": { - "drives": [ - { - "name": "nvme0n1", - "health": "PASSED", - "temperature_celsius": 28.0, - "wear_percent": 1.0, - "filesystems": [ - { - "mount": "root", - "usage_percent": 24.404377, - "used_gb": 226.51398, - "total_gb": 928.1695 - }, - { - "mount": "boot", - "usage_percent": 10.666672, - "used_gb": 0.10645676, - "total_gb": 0.9980316 - } - ] - } - ], - "pools": [] - } - }, - "services": [ - { - "name": "tailscaled", - "status": "active", - "memory_mb": 25.582031, - "disk_gb": 0.0, - "user_stopped": false - }, - { - "name": "sshd", - "status": "active", - "memory_mb": 4.3085938, - "disk_gb": 0.0, - "user_stopped": false - } - ], - "backup": { - "status": "unknown", - "last_run": null, - "next_scheduled": null, - "total_size_gb": null, - "repository_health": null - } -} -──────────────────────────────────────────────────────────────────────────────── -RAW AGENT DATA FROM cmbox: -{ - "hostname": "cmbox", - "agent_version": "v0.1.133", - "timestamp": 1763936503, - "system": { - "cpu": { - "load_1min": 1.82, - "load_5min": 2.1, - "load_15min": 2.1, - "frequency_mhz": 3743.09, - "temperature_celsius": 55.0 - }, - "memory": { - "usage_percent": 27.183601, - "total_gb": 23.339516, - "used_gb": 6.3445206, - "available_gb": 16.994995, - "swap_total_gb": 14.634708, - "swap_used_gb": 0.17599106, - "tmpfs": [ - { - "mount": "/tmp", - "usage_percent": 15.094376, - "used_gb": 0.3018875, - "total_gb": 2.0 - } - ] - }, - "storage": { - "drives": [ - { - "name": "nvme0n1", - "health": "PASSED", - "temperature_celsius": 28.0, - "wear_percent": 1.0, - "filesystems": [ - { - "mount": "root", - "usage_percent": 24.404377, - "used_gb": 226.51398, - "total_gb": 928.1695 - }, - { - "mount": "boot", - "usage_percent": 10.666672, - "used_gb": 0.10645676, - "total_gb": 0.9980316 - } - ] - } - ], - "pools": [] - } - }, - "services": [ - { - "name": "tailscaled", - "status": "active", - "memory_mb": 25.582031, - "disk_gb": 0.0, - "user_stopped": false - }, - { - "name": "sshd", - "status": "active", - "memory_mb": 4.3085938, - "disk_gb": 0.0, - "user_stopped": false - } - ], - "backup": { - "status": "unknown", - "last_run": null, - "next_scheduled": null, - "total_size_gb": null, - "repository_health": null - } -} -──────────────────────────────────────────────────────────────────────────────── -RAW AGENT DATA FROM cmbox: -{ - "hostname": "cmbox", - "agent_version": "v0.1.133", - "timestamp": 1763936505, - "system": { - "cpu": { - "load_1min": 1.75, - "load_5min": 2.08, - "load_15min": 2.1, - "frequency_mhz": 3600.005, - "temperature_celsius": 56.0 - }, - "memory": { - "usage_percent": 26.780334, - "total_gb": 23.339516, - "used_gb": 6.2504005, - "available_gb": 17.089115, - "swap_total_gb": 14.634708, - "swap_used_gb": 0.17599106, - "tmpfs": [ - { - "mount": "/tmp", - "usage_percent": 15.095139, - "used_gb": 0.30190277, - "total_gb": 2.0 - } - ] - }, - "storage": { - "drives": [ - { - "name": "nvme0n1", - "health": "PASSED", - "temperature_celsius": 28.0, - "wear_percent": 1.0, - "filesystems": [ - { - "mount": "root", - "usage_percent": 24.404377, - "used_gb": 226.51398, - "total_gb": 928.1695 - }, - { - "mount": "boot", - "usage_percent": 10.666672, - "used_gb": 0.10645676, - "total_gb": 0.9980316 - } - ] - } - ], - "pools": [] - } - }, - "services": [ - { - "name": "tailscaled", - "status": "active", - "memory_mb": 25.59375, - "disk_gb": 0.0, - "user_stopped": false - }, - { - "name": "sshd", - "status": "active", - "memory_mb": 4.3085938, - "disk_gb": 0.0, - "user_stopped": false - } - ], - "backup": { - "status": "unknown", - "last_run": null, - "next_scheduled": null, - "total_size_gb": null, - "repository_health": null - } -} -──────────────────────────────────────────────────────────────────────────────── -RAW AGENT DATA FROM cmbox: -{ - "hostname": "cmbox", - "agent_version": "v0.1.133", - "timestamp": 1763936506, - "system": { - "cpu": { - "load_1min": 1.75, - "load_5min": 2.08, - "load_15min": 2.1, - "frequency_mhz": 3600.005, - "temperature_celsius": 56.0 - }, - "memory": { - "usage_percent": 26.780334, - "total_gb": 23.339516, - "used_gb": 6.2504005, - "available_gb": 17.089115, - "swap_total_gb": 14.634708, - "swap_used_gb": 0.17599106, - "tmpfs": [ - { - "mount": "/tmp", - "usage_percent": 15.095139, - "used_gb": 0.30190277, - "total_gb": 2.0 - } - ] - }, - "storage": { - "drives": [ - { - "name": "nvme0n1", - "health": "PASSED", - "temperature_celsius": 28.0, - "wear_percent": 1.0, - "filesystems": [ - { - "mount": "root", - "usage_percent": 24.404377, - "used_gb": 226.51398, - "total_gb": 928.1695 - }, - { - "mount": "boot", - "usage_percent": 10.666672, - "used_gb": 0.10645676, - "total_gb": 0.9980316 - } - ] - } - ], - "pools": [] - } - }, - "services": [ - { - "name": "tailscaled", - "status": "active", - "memory_mb": 25.59375, - "disk_gb": 0.0, - "user_stopped": false - }, - { - "name": "sshd", - "status": "active", - "memory_mb": 4.3085938, - "disk_gb": 0.0, - "user_stopped": false - } - ], - "backup": { - "status": "unknown", - "last_run": null, - "next_scheduled": null, - "total_size_gb": null, - "repository_health": null - } -} -──────────────────────────────────────────────────────────────────────────────── -RAW AGENT DATA FROM cmbox: -{ - "hostname": "cmbox", - "agent_version": "v0.1.133", - "timestamp": 1763936507, - "system": { - "cpu": { - "load_1min": 1.75, - "load_5min": 2.08, - "load_15min": 2.1, - "frequency_mhz": 3600.005, - "temperature_celsius": 56.0 - }, - "memory": { - "usage_percent": 26.780334, - "total_gb": 23.339516, - "used_gb": 6.2504005, - "available_gb": 17.089115, - "swap_total_gb": 14.634708, - "swap_used_gb": 0.17599106, - "tmpfs": [ - { - "mount": "/tmp", - "usage_percent": 15.095139, - "used_gb": 0.30190277, - "total_gb": 2.0 - } - ] - }, - "storage": { - "drives": [ - { - "name": "nvme0n1", - "health": "PASSED", - "temperature_celsius": 28.0, - "wear_percent": 1.0, - "filesystems": [ - { - "mount": "root", - "usage_percent": 24.404377, - "used_gb": 226.51398, - "total_gb": 928.1695 - }, - { - "mount": "boot", - "usage_percent": 10.666672, - "used_gb": 0.10645676, - "total_gb": 0.9980316 - } - ] - } - ], - "pools": [] - } - }, - "services": [ - { - "name": "tailscaled", - "status": "active", - "memory_mb": 25.59375, - "disk_gb": 0.0, - "user_stopped": false - }, - { - "name": "sshd", - "status": "active", - "memory_mb": 4.3085938, - "disk_gb": 0.0, - "user_stopped": false - } - ], - "backup": { - "status": "unknown", - "last_run": null, - "next_scheduled": null, - "total_size_gb": null, - "repository_health": null - } -} -──────────────────────────────────────────────────────────────────────────────── -RAW AGENT DATA FROM cmbox: -{ - "hostname": "cmbox", - "agent_version": "v0.1.133", - "timestamp": 1763936508, - "system": { - "cpu": { - "load_1min": 1.75, - "load_5min": 2.08, - "load_15min": 2.1, - "frequency_mhz": 3600.005, - "temperature_celsius": 56.0 - }, - "memory": { - "usage_percent": 26.780334, - "total_gb": 23.339516, - "used_gb": 6.2504005, - "available_gb": 17.089115, - "swap_total_gb": 14.634708, - "swap_used_gb": 0.17599106, - "tmpfs": [ - { - "mount": "/tmp", - "usage_percent": 15.095139, - "used_gb": 0.30190277, - "total_gb": 2.0 - } - ] - }, - "storage": { - "drives": [ - { - "name": "nvme0n1", - "health": "PASSED", - "temperature_celsius": 28.0, - "wear_percent": 1.0, - "filesystems": [ - { - "mount": "root", - "usage_percent": 24.404377, - "used_gb": 226.51398, - "total_gb": 928.1695 - }, - { - "mount": "boot", - "usage_percent": 10.666672, - "used_gb": 0.10645676, - "total_gb": 0.9980316 - } - ] - } - ], - "pools": [] - } - }, - "services": [ - { - "name": "tailscaled", - "status": "active", - "memory_mb": 25.59375, - "disk_gb": 0.0, - "user_stopped": false - }, - { - "name": "sshd", - "status": "active", - "memory_mb": 4.3085938, - "disk_gb": 0.0, - "user_stopped": false - } - ], - "backup": { - "status": "unknown", - "last_run": null, - "next_scheduled": null, - "total_size_gb": null, - "repository_health": null - } -} -──────────────────────────────────────────────────────────────────────────────── -RAW AGENT DATA FROM cmbox: -{ - "hostname": "cmbox", - "agent_version": "v0.1.133", - "timestamp": 1763936509, - "system": { - "cpu": { - "load_1min": 1.75, - "load_5min": 2.08, - "load_15min": 2.1, - "frequency_mhz": 3638.71, - "temperature_celsius": 56.0 - }, - "memory": { - "usage_percent": 27.014532, - "total_gb": 23.339516, - "used_gb": 6.3050613, - "available_gb": 17.034454, - "swap_total_gb": 14.634708, - "swap_used_gb": 0.17599106, - "tmpfs": [ - { - "mount": "/tmp", - "usage_percent": 15.095139, - "used_gb": 0.30190277, - "total_gb": 2.0 - } - ] - }, - "storage": { - "drives": [ - { - "name": "nvme0n1", - "health": "PASSED", - "temperature_celsius": 28.0, - "wear_percent": 1.0, - "filesystems": [ - { - "mount": "root", - "usage_percent": 24.404377, - "used_gb": 226.51398, - "total_gb": 928.1695 - }, - { - "mount": "boot", - "usage_percent": 10.666672, - "used_gb": 0.10645676, - "total_gb": 0.9980316 - } - ] - } - ], - "pools": [] - } - }, - "services": [ - { - "name": "tailscaled", - "status": "active", - "memory_mb": 25.59375, - "disk_gb": 0.0, - "user_stopped": false - }, - { - "name": "sshd", - "status": "active", - "memory_mb": 4.3085938, - "disk_gb": 0.0, - "user_stopped": false - } - ], - "backup": { - "status": "unknown", - "last_run": null, - "next_scheduled": null, - "total_size_gb": null, - "repository_health": null - } -} -──────────────────────────────────────────────────────────────────────────────── -RAW AGENT DATA FROM cmbox: -{ - "hostname": "cmbox", - "agent_version": "v0.1.133", - "timestamp": 1763936509, - "system": { - "cpu": { - "load_1min": 0.0, - "load_5min": 0.0, - "load_15min": 0.0, - "frequency_mhz": 0.0, - "temperature_celsius": null - }, - "memory": { - "usage_percent": 0.0, - "total_gb": 0.0, - "used_gb": 0.0, - "available_gb": 0.0, - "swap_total_gb": 0.0, - "swap_used_gb": 0.0, - "tmpfs": [] - }, - "storage": { - "drives": [], - "pools": [] - } - }, - "services": [], - "backup": { - "status": "unknown", - "last_run": null, - "next_scheduled": null, - "total_size_gb": null, - "repository_health": null - } -} -──────────────────────────────────────────────────────────────────────────────── -RAW AGENT DATA FROM cmbox: -{ - "hostname": "cmbox", - "agent_version": "v0.1.133", - "timestamp": 1763936510, - "system": { - "cpu": { - "load_1min": 1.75, - "load_5min": 2.08, - "load_15min": 2.1, - "frequency_mhz": 3638.71, - "temperature_celsius": 56.0 - }, - "memory": { - "usage_percent": 27.014532, - "total_gb": 23.339516, - "used_gb": 6.3050613, - "available_gb": 17.034454, - "swap_total_gb": 14.634708, - "swap_used_gb": 0.17599106, - "tmpfs": [ - { - "mount": "/tmp", - "usage_percent": 15.095139, - "used_gb": 0.30190277, - "total_gb": 2.0 - } - ] - }, - "storage": { - "drives": [ - { - "name": "nvme0n1", - "health": "PASSED", - "temperature_celsius": 28.0, - "wear_percent": 1.0, - "filesystems": [ - { - "mount": "root", - "usage_percent": 24.404377, - "used_gb": 226.51398, - "total_gb": 928.1695 - }, - { - "mount": "boot", - "usage_percent": 10.666672, - "used_gb": 0.10645676, - "total_gb": 0.9980316 - } - ] - } - ], - "pools": [] - } - }, - "services": [ - { - "name": "tailscaled", - "status": "active", - "memory_mb": 25.59375, - "disk_gb": 0.0, - "user_stopped": false - }, - { - "name": "sshd", - "status": "active", - "memory_mb": 4.3085938, - "disk_gb": 0.0, - "user_stopped": false - } - ], - "backup": { - "status": "unknown", - "last_run": null, - "next_scheduled": null, - "total_size_gb": null, - "repository_health": null - } -} -──────────────────────────────────────────────────────────────────────────────── -RAW AGENT DATA FROM cmbox: -{ - "hostname": "cmbox", - "agent_version": "v0.1.133", - "timestamp": 1763936511, - "system": { - "cpu": { - "load_1min": 1.75, - "load_5min": 2.08, - "load_15min": 2.1, - "frequency_mhz": 3638.71, - "temperature_celsius": 56.0 - }, - "memory": { - "usage_percent": 27.014532, - "total_gb": 23.339516, - "used_gb": 6.3050613, - "available_gb": 17.034454, - "swap_total_gb": 14.634708, - "swap_used_gb": 0.17599106, - "tmpfs": [ - { - "mount": "/tmp", - "usage_percent": 15.095139, - "used_gb": 0.30190277, - "total_gb": 2.0 - } - ] - }, - "storage": { - "drives": [ - { - "name": "nvme0n1", - "health": "PASSED", - "temperature_celsius": 28.0, - "wear_percent": 1.0, - "filesystems": [ - { - "mount": "root", - "usage_percent": 24.404377, - "used_gb": 226.51398, - "total_gb": 928.1695 - }, - { - "mount": "boot", - "usage_percent": 10.666672, - "used_gb": 0.10645676, - "total_gb": 0.9980316 - } - ] - } - ], - "pools": [] - } - }, - "services": [ - { - "name": "tailscaled", - "status": "active", - "memory_mb": 25.59375, - "disk_gb": 0.0, - "user_stopped": false - }, - { - "name": "sshd", - "status": "active", - "memory_mb": 4.3085938, - "disk_gb": 0.0, - "user_stopped": false - } - ], - "backup": { - "status": "unknown", - "last_run": null, - "next_scheduled": null, - "total_size_gb": null, - "repository_health": null - } -} -──────────────────────────────────────────────────────────────────────────────── -RAW AGENT DATA FROM cmbox: -{ - "hostname": "cmbox", - "agent_version": "v0.1.133", - "timestamp": 1763936512, - "system": { - "cpu": { - "load_1min": 1.75, - "load_5min": 2.08, - "load_15min": 2.1, - "frequency_mhz": 3638.71, - "temperature_celsius": 56.0 - }, - "memory": { - "usage_percent": 27.014532, - "total_gb": 23.339516, - "used_gb": 6.3050613, - "available_gb": 17.034454, - "swap_total_gb": 14.634708, - "swap_used_gb": 0.17599106, - "tmpfs": [ - { - "mount": "/tmp", - "usage_percent": 15.095139, - "used_gb": 0.30190277, - "total_gb": 2.0 - } - ] - }, - "storage": { - "drives": [ - { - "name": "nvme0n1", - "health": "PASSED", - "temperature_celsius": 28.0, - "wear_percent": 1.0, - "filesystems": [ - { - "mount": "root", - "usage_percent": 24.404377, - "used_gb": 226.51398, - "total_gb": 928.1695 - }, - { - "mount": "boot", - "usage_percent": 10.666672, - "used_gb": 0.10645676, - "total_gb": 0.9980316 - } - ] - } - ], - "pools": [] - } - }, - "services": [ - { - "name": "tailscaled", - "status": "active", - "memory_mb": 25.59375, - "disk_gb": 0.0, - "user_stopped": false - }, - { - "name": "sshd", - "status": "active", - "memory_mb": 4.3085938, - "disk_gb": 0.0, - "user_stopped": false - } - ], - "backup": { - "status": "unknown", - "last_run": null, - "next_scheduled": null, - "total_size_gb": null, - "repository_health": null - } -} -──────────────────────────────────────────────────────────────────────────────── -Terminated diff --git a/dashboard/Cargo.toml b/dashboard/Cargo.toml index 21b0888..cef9dbe 100644 --- a/dashboard/Cargo.toml +++ b/dashboard/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cm-dashboard" -version = "0.1.158" +version = "0.1.159" edition = "2021" [dependencies] diff --git a/dashboard/src/app.rs b/dashboard/src/app.rs index a260fbd..a05b1fd 100644 --- a/dashboard/src/app.rs +++ b/dashboard/src/app.rs @@ -20,13 +20,12 @@ pub struct Dashboard { tui_app: Option, terminal: Option>>, headless: bool, - raw_data: bool, initial_commands_sent: std::collections::HashSet, config: DashboardConfig, } impl Dashboard { - pub async fn new(config_path: Option, headless: bool, raw_data: bool) -> Result { + pub async fn new(config_path: Option, headless: bool) -> Result { info!("Initializing dashboard"); // Load configuration - try default path if not specified @@ -120,7 +119,6 @@ impl Dashboard { tui_app, terminal, headless, - raw_data, initial_commands_sent: std::collections::HashSet::new(), config, }) @@ -205,13 +203,6 @@ impl Dashboard { .insert(agent_data.hostname.clone()); } - // Show raw data if requested (before processing) - if self.raw_data { - println!("RAW AGENT DATA FROM {}:", agent_data.hostname); - println!("{}", serde_json::to_string_pretty(&agent_data).unwrap_or_else(|e| format!("Serialization error: {}", e))); - println!("{}", "─".repeat(80)); - } - // Store structured data directly self.metric_store.store_agent_data(agent_data); diff --git a/dashboard/src/main.rs b/dashboard/src/main.rs index cf0bdcb..0429706 100644 --- a/dashboard/src/main.rs +++ b/dashboard/src/main.rs @@ -51,10 +51,6 @@ struct Cli { /// Run in headless mode (no TUI, just logging) #[arg(long)] headless: bool, - - /// Show raw agent data in headless mode - #[arg(long)] - raw_data: bool, } #[tokio::main] @@ -90,7 +86,7 @@ async fn main() -> Result<()> { } // Create and run dashboard - let mut dashboard = Dashboard::new(cli.config, cli.headless, cli.raw_data).await?; + let mut dashboard = Dashboard::new(cli.config, cli.headless).await?; // Setup graceful shutdown let ctrl_c = async { diff --git a/dashboard/src/ui/theme.rs b/dashboard/src/ui/theme.rs index 6bcab4a..955244b 100644 --- a/dashboard/src/ui/theme.rs +++ b/dashboard/src/ui/theme.rs @@ -225,9 +225,6 @@ impl Layout { pub const LEFT_PANEL_WIDTH: u16 = 45; /// Right panel percentage (services) pub const RIGHT_PANEL_WIDTH: u16 = 55; - /// System vs backup split (equal) - pub const SYSTEM_PANEL_HEIGHT: u16 = 50; - pub const BACKUP_PANEL_HEIGHT: u16 = 50; } /// Typography system diff --git a/dashboard/src/ui/widgets/cpu.rs b/dashboard/src/ui/widgets/cpu.rs deleted file mode 100644 index d7ec2cd..0000000 --- a/dashboard/src/ui/widgets/cpu.rs +++ /dev/null @@ -1 +0,0 @@ -// This file is intentionally left minimal - CPU functionality is handled by the SystemWidget \ No newline at end of file diff --git a/dashboard/src/ui/widgets/memory.rs b/dashboard/src/ui/widgets/memory.rs deleted file mode 100644 index 41bcbce..0000000 --- a/dashboard/src/ui/widgets/memory.rs +++ /dev/null @@ -1 +0,0 @@ -// This file is intentionally left minimal - Memory functionality is handled by the SystemWidget \ No newline at end of file diff --git a/dashboard/src/ui/widgets/mod.rs b/dashboard/src/ui/widgets/mod.rs index 550add0..6d97c15 100644 --- a/dashboard/src/ui/widgets/mod.rs +++ b/dashboard/src/ui/widgets/mod.rs @@ -1,7 +1,5 @@ use cm_dashboard_shared::AgentData; -pub mod cpu; -pub mod memory; pub mod services; pub mod system; diff --git a/dashboard/src/ui/widgets/system.rs b/dashboard/src/ui/widgets/system.rs index 6fa42d8..32ab695 100644 --- a/dashboard/src/ui/widgets/system.rs +++ b/dashboard/src/ui/widgets/system.rs @@ -508,55 +508,6 @@ fn truncate_serial(serial: &str) -> String { } } -/// Helper function to render a drive in a MergerFS pool -fn render_mergerfs_drive<'a>(drive: &StorageDrive, tree_symbol: &'a str, lines: &mut Vec>) { - let mut drive_details = Vec::new(); - if let Some(temp) = drive.temperature { - drive_details.push(format!("T: {}°C", temp as i32)); - } - if let Some(wear) = drive.wear_percent { - drive_details.push(format!("W: {}%", wear as i32)); - } - - let drive_text = if !drive_details.is_empty() { - format!("{} {}", drive.name, drive_details.join(" ")) - } else { - drive.name.clone() - }; - - let mut drive_spans = vec![ - Span::styled(tree_symbol, Typography::tree()), - ]; - drive_spans.extend(StatusIcons::create_status_spans(drive.status.clone(), &drive_text)); - lines.push(Line::from(drive_spans)); -} - -/// Helper function to render a drive in a storage pool -fn render_pool_drive(drive: &StorageDrive, is_last: bool, lines: &mut Vec>) { - let tree_symbol = if is_last { " └─" } else { " ├─" }; - - let mut drive_details = Vec::new(); - if let Some(temp) = drive.temperature { - drive_details.push(format!("T: {}°C", temp as i32)); - } - if let Some(wear) = drive.wear_percent { - drive_details.push(format!("W: {}%", wear as i32)); - } - - let drive_text = if !drive_details.is_empty() { - format!("● {} {}", drive.name, drive_details.join(" ")) - } else { - format!("● {}", drive.name) - }; - - let mut drive_spans = vec![ - Span::styled(tree_symbol, Typography::tree()), - Span::raw(" "), - ]; - drive_spans.extend(StatusIcons::create_status_spans(drive.status.clone(), &drive_text)); - lines.push(Line::from(drive_spans)); -} - impl SystemWidget { /// Render backup section for display fn render_backup(&self) -> Vec> { @@ -622,43 +573,6 @@ impl SystemWidget { lines } - /// Format time ago from timestamp - fn format_time_ago(&self, timestamp: u64) -> String { - let now = chrono::Utc::now().timestamp() as u64; - let seconds_ago = now.saturating_sub(timestamp); - - let hours = seconds_ago / 3600; - let minutes = (seconds_ago % 3600) / 60; - - if hours > 0 { - format!("{}h ago", hours) - } else if minutes > 0 { - format!("{}m ago", minutes) - } else { - "now".to_string() - } - } - - /// Format time until from future timestamp - fn format_time_until(&self, timestamp: u64) -> String { - let now = chrono::Utc::now().timestamp() as u64; - if timestamp <= now { - return "overdue".to_string(); - } - - let seconds_until = timestamp - now; - let hours = seconds_until / 3600; - let minutes = (seconds_until % 3600) / 60; - - if hours > 0 { - format!("in {}h", hours) - } else if minutes > 0 { - format!("in {}m", minutes) - } else { - "soon".to_string() - } - } - /// Render system widget pub fn render(&mut self, frame: &mut Frame, area: Rect, hostname: &str, config: Option<&crate::config::DashboardConfig>) { let mut lines = Vec::new(); diff --git a/shared/Cargo.toml b/shared/Cargo.toml index 7c3cdf9..1873cab 100644 --- a/shared/Cargo.toml +++ b/shared/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cm-dashboard-shared" -version = "0.1.158" +version = "0.1.159" edition = "2021" [dependencies]