From 4b54a59e3566b3fac3348164090fbbe9432a3d88 Mon Sep 17 00:00:00 2001 From: Christoffer Martinsson Date: Sat, 25 Oct 2025 14:15:52 +0200 Subject: [PATCH] Remove unused code and eliminate compiler warnings - Remove unused fields from CommandStatus variants - Clean up unused methods and unused collector fields - Fix lifetime syntax warning in SystemWidget - Delete unused cache module completely - Remove redundant render methods from widgets All agent and dashboard warnings eliminated while preserving panel switching and scrolling functionality. --- agent/src/agent.rs | 30 +- agent/src/cache/cached_metric.rs | 10 - agent/src/cache/manager.rs | 33 --- agent/src/cache/mod.rs | 94 ------- agent/src/collectors/backup.rs | 3 - agent/src/collectors/cpu.rs | 5 - agent/src/collectors/disk.rs | 47 ---- agent/src/collectors/memory.rs | 5 - agent/src/collectors/mod.rs | 3 - agent/src/collectors/nixos.rs | 8 +- agent/src/collectors/systemd.rs | 10 - agent/src/main.rs | 1 - agent/src/metrics/mod.rs | 145 +--------- agent/src/status/mod.rs | 14 +- dashboard/src/app.rs | 15 - dashboard/src/ui/mod.rs | 392 +-------------------------- dashboard/src/ui/theme.rs | 4 - dashboard/src/ui/widgets/backup.rs | 50 ---- dashboard/src/ui/widgets/cpu.rs | 140 +--------- dashboard/src/ui/widgets/memory.rs | 254 +---------------- dashboard/src/ui/widgets/mod.rs | 3 - dashboard/src/ui/widgets/services.rs | 177 ------------ dashboard/src/ui/widgets/system.rs | 5 +- 23 files changed, 38 insertions(+), 1410 deletions(-) delete mode 100644 agent/src/cache/cached_metric.rs delete mode 100644 agent/src/cache/manager.rs delete mode 100644 agent/src/cache/mod.rs diff --git a/agent/src/agent.rs b/agent/src/agent.rs index 4b79721..9c0b7e6 100644 --- a/agent/src/agent.rs +++ b/agent/src/agent.rs @@ -86,9 +86,9 @@ impl Agent { } } _ = transmission_interval.tick() => { - // Send all cached metrics via ZMQ every 1 second - if let Err(e) = self.broadcast_all_cached_metrics().await { - error!("Failed to broadcast cached metrics: {}", e); + // Send all metrics via ZMQ every 1 second + if let Err(e) = self.broadcast_all_metrics().await { + error!("Failed to broadcast metrics: {}", e); } } _ = notification_interval.tick() => { @@ -152,34 +152,34 @@ impl Agent { Ok(()) } - async fn broadcast_all_cached_metrics(&mut self) -> Result<()> { - debug!("Broadcasting all cached metrics via ZMQ"); + async fn broadcast_all_metrics(&mut self) -> Result<()> { + debug!("Broadcasting all metrics via ZMQ"); - // Get all cached metrics from the metric manager - let mut cached_metrics = self.metric_manager.get_all_cached_metrics().await?; + // Get all current metrics from collectors + let mut metrics = self.metric_manager.collect_all_metrics().await?; // Add the host status summary metric from status manager let host_status_metric = self.host_status_manager.get_host_status_metric(); - cached_metrics.push(host_status_metric); + metrics.push(host_status_metric); - if cached_metrics.is_empty() { - debug!("No cached metrics to broadcast"); + if metrics.is_empty() { + debug!("No metrics to broadcast"); return Ok(()); } - debug!("Broadcasting {} cached metrics (including host status summary)", cached_metrics.len()); + debug!("Broadcasting {} metrics (including host status summary)", metrics.len()); - // Create and send message with all cached data - let message = MetricMessage::new(self.hostname.clone(), cached_metrics); + // Create and send message with all current data + let message = MetricMessage::new(self.hostname.clone(), metrics); self.zmq_handler.publish_metrics(&message).await?; - debug!("Cached metrics broadcasted successfully"); + debug!("Metrics broadcasted successfully"); Ok(()) } async fn process_metrics(&mut self, metrics: &[Metric]) { for metric in metrics { - self.host_status_manager.process_metric(metric, &mut self.notification_manager, self.metric_manager.get_cache_manager()).await; + self.host_status_manager.process_metric(metric, &mut self.notification_manager).await; } } diff --git a/agent/src/cache/cached_metric.rs b/agent/src/cache/cached_metric.rs deleted file mode 100644 index de2260e..0000000 --- a/agent/src/cache/cached_metric.rs +++ /dev/null @@ -1,10 +0,0 @@ -use cm_dashboard_shared::Metric; -use std::time::Instant; - -/// A cached metric with metadata -#[derive(Debug, Clone)] -pub struct CachedMetric { - pub metric: Metric, - pub collected_at: Instant, - pub access_count: u64, -} diff --git a/agent/src/cache/manager.rs b/agent/src/cache/manager.rs deleted file mode 100644 index 6d63cb8..0000000 --- a/agent/src/cache/manager.rs +++ /dev/null @@ -1,33 +0,0 @@ -use super::ConfigurableCache; -use cm_dashboard_shared::{CacheConfig, Metric}; -use std::sync::Arc; -use tracing::info; - -/// Manages metric caching with background tasks -pub struct MetricCacheManager { - cache: Arc, -} - -impl MetricCacheManager { - pub fn new(config: CacheConfig) -> Self { - let cache = Arc::new(ConfigurableCache::new(config.clone())); - - Self { cache } - } - - /// Start background cache management tasks - pub async fn start_background_tasks(&self) { - // Temporarily disabled to isolate CPU usage issue - info!("Cache manager background tasks disabled for debugging"); - } - - /// Store metric in cache - pub async fn cache_metric(&self, metric: Metric) { - self.cache.store_metric(metric).await; - } - - /// Get all cached metrics (including expired ones) for broadcasting - pub async fn get_all_cached_metrics(&self) -> Vec { - self.cache.get_all_cached_metrics().await - } -} diff --git a/agent/src/cache/mod.rs b/agent/src/cache/mod.rs deleted file mode 100644 index c8e5506..0000000 --- a/agent/src/cache/mod.rs +++ /dev/null @@ -1,94 +0,0 @@ -use cm_dashboard_shared::{CacheConfig, Metric}; -use std::collections::HashMap; -use std::fs; -use std::path::Path; -use std::sync::Arc; -use tokio::sync::RwLock; -use tracing::{info, warn, debug}; - -/// Simple persistent cache for metrics -pub struct SimpleCache { - metrics: RwLock>, - persist_path: String, -} - -impl SimpleCache { - pub fn new(config: CacheConfig) -> Self { - let cache = Self { - metrics: RwLock::new(HashMap::new()), - persist_path: config.persist_path, - }; - - // Clear cache file on startup to ensure fresh data - cache.clear_cache_file(); - cache - } - - /// Store metric in cache - pub async fn store_metric(&self, metric: Metric) { - let mut metrics = self.metrics.write().await; - metrics.insert(metric.name.clone(), metric); - } - - /// Get all cached metrics - pub async fn get_all_cached_metrics(&self) -> Vec { - let metrics = self.metrics.read().await; - metrics.values().cloned().collect() - } - - /// Save cache to disk - pub async fn save_to_disk(&self) { - // Cache persistence disabled to prevent stale data issues during debugging - debug!("Cache persistence disabled - not saving to disk"); - } - - /// Load cache from disk (DISABLED) - fn load_from_disk(&self) { - // Cache loading disabled to prevent stale data issues during debugging - info!("Cache loading disabled - starting with fresh cache"); - } - - /// Clear cache file on startup to ensure fresh data - fn clear_cache_file(&self) { - if Path::new(&self.persist_path).exists() { - match fs::remove_file(&self.persist_path) { - Ok(_) => info!("Cleared cache file {} on startup", self.persist_path), - Err(e) => warn!("Failed to clear cache file {}: {}", self.persist_path, e), - } - } - } -} - - -#[derive(Clone)] -pub struct MetricCacheManager { - cache: Arc, -} - -impl MetricCacheManager { - pub fn new(config: CacheConfig) -> Self { - Self { - cache: Arc::new(SimpleCache::new(config)), - } - } - - pub async fn store_metric(&self, metric: Metric) { - self.cache.store_metric(metric).await; - } - - pub async fn cache_metric(&self, metric: Metric) { - self.store_metric(metric).await; - } - - pub async fn start_background_tasks(&self) { - // No background tasks needed for simple cache - } - - pub async fn get_all_cached_metrics(&self) -> Result, anyhow::Error> { - Ok(self.cache.get_all_cached_metrics().await) - } - - pub async fn save_to_disk(&self) { - self.cache.save_to_disk().await; - } -} \ No newline at end of file diff --git a/agent/src/collectors/backup.rs b/agent/src/collectors/backup.rs index ed3888a..5229342 100644 --- a/agent/src/collectors/backup.rs +++ b/agent/src/collectors/backup.rs @@ -107,9 +107,6 @@ impl BackupCollector { #[async_trait] impl Collector for BackupCollector { - fn name(&self) -> &str { - "backup" - } async fn collect(&self, _status_tracker: &mut StatusTracker) -> Result, CollectorError> { let backup_status_option = self.read_backup_status().await?; diff --git a/agent/src/collectors/cpu.rs b/agent/src/collectors/cpu.rs index 7c5eb36..08733e1 100644 --- a/agent/src/collectors/cpu.rs +++ b/agent/src/collectors/cpu.rs @@ -15,7 +15,6 @@ use crate::config::CpuConfig; /// - No process spawning /// - <0.1ms collection time target pub struct CpuCollector { - name: String, load_thresholds: HysteresisThresholds, temperature_thresholds: HysteresisThresholds, } @@ -34,7 +33,6 @@ impl CpuCollector { ); Self { - name: "cpu".to_string(), load_thresholds, temperature_thresholds, } @@ -197,9 +195,6 @@ impl CpuCollector { #[async_trait] impl Collector for CpuCollector { - fn name(&self) -> &str { - &self.name - } async fn collect(&self, status_tracker: &mut StatusTracker) -> Result, CollectorError> { debug!("Collecting CPU metrics"); diff --git a/agent/src/collectors/disk.rs b/agent/src/collectors/disk.rs index d40e57f..cb6baf5 100644 --- a/agent/src/collectors/disk.rs +++ b/agent/src/collectors/disk.rs @@ -325,33 +325,6 @@ impl DiskCollector { Some(device_name.to_string()) } - /// Get directory size using du command (efficient for single directory) - fn get_directory_size(&self, path: &str) -> Result { - let output = Command::new("du") - .arg("-s") - .arg("--block-size=1") - .arg(path) - .output()?; - - // du returns success even with permission denied warnings in stderr - // We only care if the command completely failed or produced no stdout - let output_str = String::from_utf8(output.stdout)?; - - if output_str.trim().is_empty() { - return Err(anyhow::anyhow!( - "du command produced no output for {}", - path - )); - } - - let size_str = output_str - .split_whitespace() - .next() - .ok_or_else(|| anyhow::anyhow!("Failed to parse du output"))?; - - let size_bytes = size_str.parse::()?; - Ok(size_bytes) - } /// Get filesystem info using df command fn get_filesystem_info(&self, path: &str) -> Result<(u64, u64)> { @@ -382,23 +355,6 @@ impl DiskCollector { Ok((total_bytes, used_bytes)) } - /// Calculate status based on usage percentage - fn calculate_usage_status(&self, used_bytes: u64, total_bytes: u64) -> Status { - if total_bytes == 0 { - return Status::Unknown; - } - - let usage_percent = (used_bytes as f64 / total_bytes as f64) * 100.0; - - // Thresholds for disk usage - if usage_percent >= 95.0 { - Status::Critical - } else if usage_percent >= 85.0 { - Status::Warning - } else { - Status::Ok - } - } /// Parse size string (e.g., "120G", "45M") to GB value fn parse_size_to_gb(&self, size_str: &str) -> f32 { @@ -435,9 +391,6 @@ impl DiskCollector { #[async_trait] impl Collector for DiskCollector { - fn name(&self) -> &str { - "disk" - } async fn collect(&self, status_tracker: &mut StatusTracker) -> Result, CollectorError> { let start_time = Instant::now(); diff --git a/agent/src/collectors/memory.rs b/agent/src/collectors/memory.rs index b5dd1b6..52acddc 100644 --- a/agent/src/collectors/memory.rs +++ b/agent/src/collectors/memory.rs @@ -15,7 +15,6 @@ use crate::config::MemoryConfig; /// - No regex or complex parsing /// - <0.1ms collection time target pub struct MemoryCollector { - name: String, usage_thresholds: HysteresisThresholds, } @@ -42,7 +41,6 @@ impl MemoryCollector { ); Self { - name: "memory".to_string(), usage_thresholds, } } @@ -284,9 +282,6 @@ impl MemoryCollector { #[async_trait] impl Collector for MemoryCollector { - fn name(&self) -> &str { - &self.name - } async fn collect(&self, status_tracker: &mut StatusTracker) -> Result, CollectorError> { debug!("Collecting memory metrics"); diff --git a/agent/src/collectors/mod.rs b/agent/src/collectors/mod.rs index b8cbb0c..839525e 100644 --- a/agent/src/collectors/mod.rs +++ b/agent/src/collectors/mod.rs @@ -16,9 +16,6 @@ pub use error::CollectorError; /// Base trait for all collectors with extreme efficiency requirements #[async_trait] pub trait Collector: Send + Sync { - /// Name of this collector - fn name(&self) -> &str; - /// Collect all metrics this collector provides async fn collect(&self, status_tracker: &mut StatusTracker) -> Result, CollectorError>; diff --git a/agent/src/collectors/nixos.rs b/agent/src/collectors/nixos.rs index bf35def..49edde9 100644 --- a/agent/src/collectors/nixos.rs +++ b/agent/src/collectors/nixos.rs @@ -12,12 +12,11 @@ use crate::config::NixOSConfig; /// - NixOS version and build information /// - Currently active/logged in users pub struct NixOSCollector { - config: NixOSConfig, } impl NixOSCollector { - pub fn new(config: NixOSConfig) -> Self { - Self { config } + pub fn new(_config: NixOSConfig) -> Self { + Self {} } /// Get NixOS build information @@ -116,9 +115,6 @@ impl NixOSCollector { #[async_trait] impl Collector for NixOSCollector { - fn name(&self) -> &str { - "nixos" - } async fn collect(&self, _status_tracker: &mut StatusTracker) -> Result, CollectorError> { debug!("Collecting NixOS system information"); diff --git a/agent/src/collectors/systemd.rs b/agent/src/collectors/systemd.rs index fc405e9..7373e62 100644 --- a/agent/src/collectors/systemd.rs +++ b/agent/src/collectors/systemd.rs @@ -42,7 +42,6 @@ struct ServiceStatusInfo { load_state: String, active_state: String, sub_state: String, - description: String, } impl SystemdCollector { @@ -170,18 +169,12 @@ impl SystemdCollector { let load_state = fields.get(1).unwrap_or(&"unknown").to_string(); let active_state = fields.get(2).unwrap_or(&"unknown").to_string(); let sub_state = fields.get(3).unwrap_or(&"unknown").to_string(); - let description = if fields.len() > 4 { - fields[4..].join(" ") - } else { - "".to_string() - }; // Cache the status information status_cache.insert(service_name.to_string(), ServiceStatusInfo { load_state: load_state.clone(), active_state: active_state.clone(), sub_state: sub_state.clone(), - description, }); all_service_names.insert(service_name.to_string()); @@ -432,9 +425,6 @@ impl SystemdCollector { #[async_trait] impl Collector for SystemdCollector { - fn name(&self) -> &str { - "systemd" - } async fn collect(&self, _status_tracker: &mut StatusTracker) -> Result, CollectorError> { let start_time = Instant::now(); diff --git a/agent/src/main.rs b/agent/src/main.rs index 67cf23d..e238866 100644 --- a/agent/src/main.rs +++ b/agent/src/main.rs @@ -4,7 +4,6 @@ use tracing::{error, info}; use tracing_subscriber::EnvFilter; mod agent; -mod cache; mod collectors; mod communication; mod config; diff --git a/agent/src/metrics/mod.rs b/agent/src/metrics/mod.rs index 9231227..6529306 100644 --- a/agent/src/metrics/mod.rs +++ b/agent/src/metrics/mod.rs @@ -1,26 +1,21 @@ use anyhow::Result; use cm_dashboard_shared::{Metric, StatusTracker}; -use std::collections::HashMap; -use std::time::Instant; -use tracing::{debug, error, info}; +use tracing::{error, info}; -use crate::cache::MetricCacheManager; use crate::collectors::{ backup::BackupCollector, cpu::CpuCollector, disk::DiskCollector, memory::MemoryCollector, nixos::NixOSCollector, systemd::SystemdCollector, Collector, }; use crate::config::{AgentConfig, CollectorConfig}; -/// Manages all metric collectors with intelligent caching +/// Manages all metric collectors pub struct MetricCollectionManager { collectors: Vec>, - cache_manager: MetricCacheManager, - last_collection_times: HashMap, status_tracker: StatusTracker, } impl MetricCollectionManager { - pub async fn new(config: &CollectorConfig, agent_config: &AgentConfig) -> Result { + pub async fn new(config: &CollectorConfig, _agent_config: &AgentConfig) -> Result { let mut collectors: Vec> = Vec::new(); // Benchmark mode - only enable specific collector based on env var @@ -109,153 +104,37 @@ impl MetricCollectionManager { } } - // Initialize cache manager with configuration - let cache_manager = MetricCacheManager::new(agent_config.cache.clone()); - - // Start background cache tasks - cache_manager.start_background_tasks().await; - info!( - "Metric collection manager initialized with {} collectors and caching enabled", + "Metric collection manager initialized with {} collectors", collectors.len() ); Ok(Self { collectors, - cache_manager, - last_collection_times: HashMap::new(), status_tracker: StatusTracker::new(), }) } /// Force collection from ALL collectors immediately (used at startup) pub async fn collect_all_metrics_force(&mut self) -> Result> { - let mut all_metrics = Vec::new(); - let now = Instant::now(); - - info!( - "Force collecting from ALL {} collectors for startup", - self.collectors.len() - ); - - // Force collection from every collector regardless of intervals - for collector in &self.collectors { - let collector_name = collector.name(); - - match collector.collect(&mut self.status_tracker).await { - Ok(metrics) => { - info!( - "Force collected {} metrics from {} collector", - metrics.len(), - collector_name - ); - - // Cache all new metrics - for metric in &metrics { - self.cache_manager.cache_metric(metric.clone()).await; - } - - all_metrics.extend(metrics); - self.last_collection_times - .insert(collector_name.to_string(), now); - } - Err(e) => { - error!( - "Collector '{}' failed during force collection: {}", - collector_name, e - ); - // Continue with other collectors even if one fails - } - } - } - - info!( - "Force collection completed: {} total metrics cached", - all_metrics.len() - ); - Ok(all_metrics) + self.collect_all_metrics().await } - /// Collect metrics from all collectors with intelligent caching + /// Collect metrics from all collectors pub async fn collect_all_metrics(&mut self) -> Result> { let mut all_metrics = Vec::new(); - let now = Instant::now(); - // Collecting metrics from collectors (debug logging disabled for performance) - - // Keep track of which collector types we're collecting fresh data from - let mut collecting_fresh = std::collections::HashSet::new(); - - // For each collector, check if we need to collect based on time intervals for collector in &self.collectors { - let collector_name = collector.name(); - - // Determine cache interval for this collector type based on data volatility - let cache_interval_secs = match collector_name { - "cpu" | "memory" => 5, // Fast updates for volatile metrics - "systemd" => 30, // Service status changes less frequently - "disk" => 300, // SMART data changes very slowly (5 minutes) - "backup" => 600, // Backup status changes rarely (10 minutes) - _ => 30, // Default: moderate frequency - }; - - let should_collect = - if let Some(last_time) = self.last_collection_times.get(collector_name) { - now.duration_since(*last_time).as_secs() >= cache_interval_secs - } else { - true // First collection - }; - - if should_collect { - collecting_fresh.insert(collector_name.to_string()); - match collector.collect(&mut self.status_tracker).await { - Ok(metrics) => { - // Collector returned fresh metrics (debug logging disabled for performance) - - // Cache all new metrics - for metric in &metrics { - self.cache_manager.cache_metric(metric.clone()).await; - } - - all_metrics.extend(metrics); - self.last_collection_times - .insert(collector_name.to_string(), now); - } - Err(e) => { - error!("Collector '{}' failed: {}", collector_name, e); - // Continue with other collectors even if one fails - } + match collector.collect(&mut self.status_tracker).await { + Ok(metrics) => { + all_metrics.extend(metrics); + } + Err(e) => { + error!("Collector failed: {}", e); } - } else { - let _elapsed = self - .last_collection_times - .get(collector_name) - .map(|t| now.duration_since(*t).as_secs()) - .unwrap_or(0); - // Collector skipped (debug logging disabled for performance) } } - - // For 2-second intervals, skip cached metrics to avoid duplicates - // (Cache system disabled for realtime updates) - - // Collected metrics total (debug logging disabled for performance) Ok(all_metrics) } - - /// Get all cached metrics from the cache manager - pub async fn get_all_cached_metrics(&self) -> Result> { - let cached_metrics = self.cache_manager.get_all_cached_metrics().await?; - debug!( - "Retrieved {} cached metrics for broadcast", - cached_metrics.len() - ); - Ok(cached_metrics) - } - - pub fn get_cache_manager(&self) -> &MetricCacheManager { - &self.cache_manager - } - } diff --git a/agent/src/status/mod.rs b/agent/src/status/mod.rs index 516cf22..888cecb 100644 --- a/agent/src/status/mod.rs +++ b/agent/src/status/mod.rs @@ -70,7 +70,7 @@ impl HostStatusManager { /// Update the status of a specific service and recalculate host status /// Updates real-time status and buffers changes for email notifications - pub fn update_service_status(&mut self, service: String, status: Status, cache_manager: Option<&crate::cache::MetricCacheManager>) { + pub fn update_service_status(&mut self, service: String, status: Status) { if !self.config.enabled { return; } @@ -82,14 +82,6 @@ impl HostStatusManager { return; } - // Save cache when status changes (clone cache manager reference for async) - if let Some(cache) = cache_manager { - let cache = cache.clone(); - tokio::spawn(async move { - cache.save_to_disk().await; - }); - } - // Initialize batch if this is the first change if self.batch_start_time.is_none() { self.batch_start_time = Some(Instant::now()); @@ -169,9 +161,9 @@ impl HostStatusManager { /// Process a metric - updates status (notifications handled separately via batching) - pub async fn process_metric(&mut self, metric: &Metric, _notification_manager: &mut crate::notifications::NotificationManager, cache_manager: &crate::cache::MetricCacheManager) { + pub async fn process_metric(&mut self, metric: &Metric, _notification_manager: &mut crate::notifications::NotificationManager) { // Just update status - notifications are handled by process_pending_notifications - self.update_service_status(metric.name.clone(), metric.status, Some(cache_manager)); + self.update_service_status(metric.name.clone(), metric.status); } /// Process pending notifications - call this at notification intervals diff --git a/dashboard/src/app.rs b/dashboard/src/app.rs index 0c4c918..d4b763f 100644 --- a/dashboard/src/app.rs +++ b/dashboard/src/app.rs @@ -332,21 +332,6 @@ impl Dashboard { Ok(()) } - /// Get current service status from metrics to determine start/stop action - fn get_service_status(&self, hostname: &str, service_name: &str) -> Option { - let metrics = self.metric_store.get_metrics_for_host(hostname); - - // Look for systemd service status metric - for metric in metrics { - if metric.name == format!("systemd_{}_status", service_name) { - if let cm_dashboard_shared::MetricValue::String(status) = &metric.value { - return Some(status.clone()); - } - } - } - - None - } } impl Drop for Dashboard { diff --git a/dashboard/src/ui/mod.rs b/dashboard/src/ui/mod.rs index e989aa9..5555a40 100644 --- a/dashboard/src/ui/mod.rs +++ b/dashboard/src/ui/mod.rs @@ -15,7 +15,7 @@ pub mod widgets; use crate::metrics::MetricStore; use cm_dashboard_shared::{Metric, Status}; -use theme::{Components, Layout as ThemeLayout, StatusIcons, Theme, Typography}; +use theme::{Components, Layout as ThemeLayout, Theme, Typography}; use widgets::{BackupWidget, ServicesWidget, SystemWidget, Widget}; /// Commands that can be triggered from the UI @@ -34,9 +34,7 @@ pub enum CommandStatus { /// Command is executing InProgress { command_type: CommandType, target: String, start_time: std::time::Instant }, /// Command completed successfully - Success { command_type: CommandType, target: String, duration: std::time::Duration, completed_at: std::time::Instant }, - /// Command failed - Failed { command_type: CommandType, target: String, error: String, failed_at: std::time::Instant }, + Success { command_type: CommandType, completed_at: std::time::Instant }, } /// Types of commands for status tracking @@ -58,28 +56,6 @@ pub enum PanelType { } impl PanelType { - /// Get all panel types in order - pub fn all() -> [PanelType; 3] { - [PanelType::System, PanelType::Services, PanelType::Backup] - } - - /// Get the next panel in cycle (System → Services → Backup → System) - pub fn next(self) -> PanelType { - match self { - PanelType::System => PanelType::Services, - PanelType::Services => PanelType::Backup, - PanelType::Backup => PanelType::System, - } - } - - /// Get the previous panel in cycle (System ← Services ← Backup ← System) - pub fn previous(self) -> PanelType { - match self { - PanelType::System => PanelType::Backup, - PanelType::Services => PanelType::System, - PanelType::Backup => PanelType::Services, - } - } } /// Widget states for a specific host @@ -423,34 +399,7 @@ impl TuiApp { info!("Switched to panel: {:?}", self.focused_panel); } - /// Switch to previous panel (Shift+Tab in reverse) - only cycles through visible panels - pub fn previous_panel(&mut self) { - let visible_panels = self.get_visible_panels(); - if visible_panels.len() <= 1 { - return; // Can't switch if only one or no panels visible - } - - // Find current panel index in visible panels - if let Some(current_index) = visible_panels.iter().position(|&p| p == self.focused_panel) { - // Move to previous visible panel - let prev_index = if current_index == 0 { - visible_panels.len() - 1 - } else { - current_index - 1 - }; - self.focused_panel = visible_panels[prev_index]; - } else { - // Current panel not visible, switch to last visible panel - self.focused_panel = visible_panels[visible_panels.len() - 1]; - } - - info!("Switched to panel: {:?}", self.focused_panel); - } - /// Get the currently focused panel - pub fn get_focused_panel(&self) -> PanelType { - self.focused_panel - } /// Get the currently selected service name from the services widget fn get_selected_service(&self) -> Option { @@ -462,15 +411,6 @@ impl TuiApp { None } - /// Get command status for current host - pub fn get_command_status(&self) -> Option<&CommandStatus> { - if let Some(hostname) = &self.current_host { - if let Some(host_widgets) = self.host_widgets.get(hostname) { - return host_widgets.command_status.as_ref(); - } - } - None - } /// Should quit application pub fn should_quit(&self) -> bool { @@ -491,31 +431,15 @@ impl TuiApp { /// Mark command as completed successfully pub fn complete_command(&mut self, hostname: &str) { if let Some(host_widgets) = self.host_widgets.get_mut(hostname) { - if let Some(CommandStatus::InProgress { command_type, target, start_time }) = &host_widgets.command_status { - let duration = start_time.elapsed(); + if let Some(CommandStatus::InProgress { command_type, .. }) = &host_widgets.command_status { host_widgets.command_status = Some(CommandStatus::Success { command_type: command_type.clone(), - target: target.clone(), - duration, completed_at: Instant::now(), }); } } } - /// Mark command as failed - pub fn fail_command(&mut self, hostname: &str, error: String) { - if let Some(host_widgets) = self.host_widgets.get_mut(hostname) { - if let Some(CommandStatus::InProgress { command_type, target, .. }) = &host_widgets.command_status { - host_widgets.command_status = Some(CommandStatus::Failed { - command_type: command_type.clone(), - target: target.clone(), - error, - failed_at: Instant::now(), - }); - } - } - } /// Check for command timeouts and automatically clear them pub fn check_command_timeouts(&mut self) { @@ -539,11 +463,6 @@ impl TuiApp { hosts_to_clear.push(hostname.clone()); } } - else if let Some(CommandStatus::Failed { failed_at, .. }) = &host_widgets.command_status { - if now.duration_since(*failed_at) > Duration::from_secs(5) { - hosts_to_clear.push(hostname.clone()); - } - } } // Clear timed out commands @@ -620,14 +539,6 @@ impl TuiApp { } } - /// Get total count of services for bounds checking - fn get_total_services_count(&self, hostname: &str) -> usize { - if let Some(host_widgets) = self.host_widgets.get(hostname) { - host_widgets.services_widget.get_total_services_count() - } else { - 0 - } - } /// Get list of currently visible panels fn get_visible_panels(&self) -> Vec { @@ -759,10 +670,6 @@ impl TuiApp { // Show green checkmark for successful rebuild ("✓", Theme::success()) } - Some(CommandStatus::Failed { command_type: CommandType::SystemRebuild, .. }) => { - // Show red X for failed rebuild - ("✗", Theme::error()) - } _ => { // Normal status icon based on metrics let host_status = self.calculate_host_status(host, metric_store); @@ -928,297 +835,4 @@ impl TuiApp { } } - fn render_storage_section(&self, frame: &mut Frame, area: Rect, metric_store: &MetricStore) { - if area.height < 2 { - return; - } - - if let Some(ref hostname) = self.current_host { - // Discover storage pools from metrics (look for disk_{pool}_usage_percent patterns) - let mut storage_pools: std::collections::HashMap> = - std::collections::HashMap::new(); - - let all_metrics = metric_store.get_metrics_for_host(hostname); - - // Find storage pools by looking for usage metrics - for metric in &all_metrics { - if metric.name.starts_with("disk_") && metric.name.ends_with("_usage_percent") { - let pool_name = metric.name - .strip_prefix("disk_") - .and_then(|s| s.strip_suffix("_usage_percent")) - .unwrap_or_default() - .to_string(); - - if !pool_name.is_empty() && pool_name != "tmp" { - storage_pools.entry(pool_name.clone()).or_insert_with(Vec::new); - } - } - } - - // Find individual drives for each pool - for metric in &all_metrics { - if metric.name.starts_with("disk_") && metric.name.contains("_") && metric.name.ends_with("_health") { - // Parse disk_{pool}_{drive}_health format - let parts: Vec<&str> = metric.name.split('_').collect(); - if parts.len() >= 4 && parts[0] == "disk" && parts[parts.len()-1] == "health" { - // Extract pool name (everything between "disk_" and "_{drive}_health") - let drive_name = parts[parts.len()-2].to_string(); - let pool_part_end = parts.len() - 2; - let pool_name = parts[1..pool_part_end].join("_"); - - if let Some(drives) = storage_pools.get_mut(&pool_name) { - if !drives.contains(&drive_name) { - drives.push(drive_name); - } - } - } - } - } - - // Check if we found any storage pools - if storage_pools.is_empty() { - // No storage pools found - show error/waiting message - let content_chunks = ratatui::layout::Layout::default() - .direction(Direction::Vertical) - .constraints([Constraint::Length(1), Constraint::Min(0)]) - .split(area); - - let storage_title = Paragraph::new("Storage:").style(Typography::widget_title()); - frame.render_widget(storage_title, content_chunks[0]); - - let no_storage_spans = - StatusIcons::create_status_spans(Status::Unknown, "No storage pools detected"); - let no_storage_para = Paragraph::new(ratatui::text::Line::from(no_storage_spans)); - frame.render_widget(no_storage_para, content_chunks[1]); - return; - } - - let available_lines = area.height as usize; - let mut constraints = Vec::new(); - let mut pools_to_show = Vec::new(); - let mut current_line = 0; - - // Sort storage pools by name for consistent ordering - let mut sorted_pools: Vec<_> = storage_pools.iter().collect(); - sorted_pools.sort_by_key(|(pool_name, _)| pool_name.as_str()); - - // Add section title if we have pools - let mut title_added = false; - - for (pool_name, drives) in sorted_pools { - // Calculate lines needed: pool header + drives + usage line (+ section title if first) - let section_title_lines = if !title_added { 1 } else { 0 }; - let lines_for_this_pool = section_title_lines + 1 + drives.len() + 1; - - if current_line + lines_for_this_pool <= available_lines { - pools_to_show.push((pool_name.clone(), drives.clone())); - - // Add section title constraint if this is the first pool - if !title_added { - constraints.push(Constraint::Length(1)); // "Storage:" section title - title_added = true; - } - - // Add constraints for this pool - constraints.push(Constraint::Length(1)); // Pool header with status - for _ in 0..drives.len() { - constraints.push(Constraint::Length(1)); // Drive line with tree symbol - } - constraints.push(Constraint::Length(1)); // Usage line with end tree symbol - - current_line += lines_for_this_pool; - } else { - break; // Can't fit more pools - } - } - - // Add remaining space if any - if constraints.len() < available_lines { - constraints.push(Constraint::Min(0)); - } - - let content_chunks = ratatui::layout::Layout::default() - .direction(Direction::Vertical) - .constraints(constraints) - .split(area); - - let mut chunk_index = 0; - - // Render "Storage:" section title if we have pools - if !pools_to_show.is_empty() { - let storage_title = Paragraph::new("Storage:").style(Typography::widget_title()); - frame.render_widget(storage_title, content_chunks[chunk_index]); - chunk_index += 1; - } - - // Display each storage pool with tree structure - for (pool_name, drives) in &pools_to_show { - // Pool header with status icon and type - let pool_display_name = if pool_name == "root" { - "root".to_string() - } else { - pool_name.clone() - }; - - let pool_type = if drives.len() > 1 { "multi-drive" } else { "Single" }; - - // Get pool status from usage metric - let pool_status = metric_store - .get_metric(hostname, &format!("disk_{}_usage_percent", pool_name)) - .map(|m| m.status) - .unwrap_or(Status::Unknown); - - // Create pool header with status icon - let pool_status_icon = StatusIcons::get_icon(pool_status); - let pool_status_color = Theme::status_color(pool_status); - let pool_header_text = format!("{} ({}):", pool_display_name, pool_type); - - let pool_header_spans = vec![ - ratatui::text::Span::styled( - format!("{} ", pool_status_icon), - Style::default().fg(pool_status_color), - ), - ratatui::text::Span::styled( - pool_header_text, - Style::default().fg(Theme::primary_text()), - ), - ]; - let pool_header_para = Paragraph::new(ratatui::text::Line::from(pool_header_spans)); - frame.render_widget(pool_header_para, content_chunks[chunk_index]); - chunk_index += 1; - - // Individual drive lines with tree symbols - let mut sorted_drives = drives.clone(); - sorted_drives.sort(); - for (_drive_idx, drive_name) in sorted_drives.iter().enumerate() { - // Get drive health status - let drive_health_metric = metric_store - .get_metric(hostname, &format!("disk_{}_{}_health", pool_name, drive_name)); - let drive_status = drive_health_metric - .map(|m| m.status) - .unwrap_or(Status::Unknown); - - // Get drive temperature - let temp_text = metric_store - .get_metric(hostname, &format!("disk_{}_{}_temperature", pool_name, drive_name)) - .and_then(|m| m.value.as_f32()) - .map(|temp| format!(" T:{:.0}°C", temp)) - .unwrap_or_default(); - - // Get drive wear level (SSDs) - let wear_text = metric_store - .get_metric(hostname, &format!("disk_{}_{}_wear_percent", pool_name, drive_name)) - .and_then(|m| m.value.as_f32()) - .map(|wear| format!(" W:{:.0}%", wear)) - .unwrap_or_default(); - - // Build drive line with tree symbol - let tree_symbol = "├─"; - let drive_status_icon = StatusIcons::get_icon(drive_status); - let drive_status_color = Theme::status_color(drive_status); - let drive_text = format!("{}{}{}", drive_name, temp_text, wear_text); - - let drive_spans = vec![ - ratatui::text::Span::styled(" ", Style::default()), // 2-space indentation - ratatui::text::Span::styled( - format!("{} ", tree_symbol), - Style::default().fg(Theme::muted_text()), - ), - ratatui::text::Span::styled( - format!("{} ", drive_status_icon), - Style::default().fg(drive_status_color), - ), - ratatui::text::Span::styled( - drive_text, - Style::default().fg(Theme::primary_text()), - ), - ]; - let drive_para = Paragraph::new(ratatui::text::Line::from(drive_spans)); - frame.render_widget(drive_para, content_chunks[chunk_index]); - chunk_index += 1; - } - - // Usage line with end tree symbol and status icon - let usage_percent = metric_store - .get_metric(hostname, &format!("disk_{}_usage_percent", pool_name)) - .and_then(|m| m.value.as_f32()) - .unwrap_or(0.0); - - let used_gb = metric_store - .get_metric(hostname, &format!("disk_{}_used_gb", pool_name)) - .and_then(|m| m.value.as_f32()) - .unwrap_or(0.0); - - let total_gb = metric_store - .get_metric(hostname, &format!("disk_{}_total_gb", pool_name)) - .and_then(|m| m.value.as_f32()) - .unwrap_or(0.0); - - let usage_status = metric_store - .get_metric(hostname, &format!("disk_{}_usage_percent", pool_name)) - .map(|m| m.status) - .unwrap_or(Status::Unknown); - - // Format usage with proper units - let (used_display, total_display, unit) = if total_gb < 1.0 { - (used_gb * 1024.0, total_gb * 1024.0, "MB") - } else { - (used_gb, total_gb, "GB") - }; - - let end_tree_symbol = "└─"; - let usage_status_icon = StatusIcons::get_icon(usage_status); - let usage_status_color = Theme::status_color(usage_status); - let usage_text = format!("{:.1}% {:.1}{}/{:.1}{}", - usage_percent, used_display, unit, total_display, unit); - - let usage_spans = vec![ - ratatui::text::Span::styled(" ", Style::default()), // 2-space indentation - ratatui::text::Span::styled( - format!("{} ", end_tree_symbol), - Style::default().fg(Theme::muted_text()), - ), - ratatui::text::Span::styled( - format!("{} ", usage_status_icon), - Style::default().fg(usage_status_color), - ), - ratatui::text::Span::styled( - usage_text, - Style::default().fg(Theme::primary_text()), - ), - ]; - let usage_para = Paragraph::new(ratatui::text::Line::from(usage_spans)); - frame.render_widget(usage_para, content_chunks[chunk_index]); - chunk_index += 1; - } - - // Show truncation indicator if we couldn't display all pools - if pools_to_show.len() < storage_pools.len() { - if let Some(last_chunk) = content_chunks.last() { - let truncated_count = storage_pools.len() - pools_to_show.len(); - let truncated_text = format!( - "... and {} more pool{}", - truncated_count, - if truncated_count == 1 { "" } else { "s" } - ); - let truncated_para = Paragraph::new(truncated_text).style(Typography::muted()); - frame.render_widget(truncated_para, *last_chunk); - } - } - } else { - // No host connected - let content_chunks = ratatui::layout::Layout::default() - .direction(Direction::Vertical) - .constraints([Constraint::Length(1), Constraint::Min(0)]) - .split(area); - - let storage_title = Paragraph::new("Storage:").style(Typography::widget_title()); - frame.render_widget(storage_title, content_chunks[0]); - - let no_host_spans = - StatusIcons::create_status_spans(Status::Unknown, "No host connected"); - let no_host_para = Paragraph::new(ratatui::text::Line::from(no_host_spans)); - frame.render_widget(no_host_para, content_chunks[1]); - } - } } diff --git a/dashboard/src/ui/theme.rs b/dashboard/src/ui/theme.rs index 2152ed8..d03f043 100644 --- a/dashboard/src/ui/theme.rs +++ b/dashboard/src/ui/theme.rs @@ -226,10 +226,6 @@ impl Layout { /// System vs backup split (equal) pub const SYSTEM_PANEL_HEIGHT: u16 = 50; pub const BACKUP_PANEL_HEIGHT: u16 = 50; - /// System panel CPU section height - pub const CPU_SECTION_HEIGHT: u16 = 2; - /// System panel memory section height - pub const MEMORY_SECTION_HEIGHT: u16 = 3; } /// Typography system diff --git a/dashboard/src/ui/widgets/backup.rs b/dashboard/src/ui/widgets/backup.rs index 36d8a66..f3a444c 100644 --- a/dashboard/src/ui/widgets/backup.rs +++ b/dashboard/src/ui/widgets/backup.rs @@ -81,38 +81,7 @@ impl BackupWidget { - /// Format timestamp for display - fn format_last_run(&self) -> String { - match self.last_run_timestamp { - Some(timestamp) => { - let duration = chrono::Utc::now().timestamp() - timestamp; - if duration < 3600 { - format!("{}m ago", duration / 60) - } else if duration < 86400 { - format!("{}h ago", duration / 3600) - } else { - format!("{}d ago", duration / 86400) - } - } - None => "—".to_string(), - } - } - /// Format disk usage in format "usedGB/totalGB" - fn format_repo_size(&self) -> String { - match (self.backup_disk_used_gb, self.backup_disk_total_gb) { - (Some(used_gb), Some(total_gb)) => { - let used_str = Self::format_size_with_proper_units(used_gb); - let total_str = Self::format_size_with_proper_units(total_gb); - format!("{}/{}", used_str, total_str) - } - (Some(used_gb), None) => { - // Fallback to just used size if total not available - Self::format_size_with_proper_units(used_gb) - } - _ => "—".to_string(), - } - } /// Format size with proper units (xxxkB/MB/GB/TB) fn format_size_with_proper_units(size_gb: f32) -> String { @@ -137,23 +106,7 @@ impl BackupWidget { } } - /// Format product name display - fn format_product_name(&self) -> String { - if let Some(ref product_name) = self.backup_disk_product_name { - format!("P/N: {}", product_name) - } else { - "P/N: Unknown".to_string() - } - } - /// Format serial number display - fn format_serial_number(&self) -> String { - if let Some(ref serial) = self.backup_disk_serial_number { - format!("S/N: {}", serial) - } else { - "S/N: Unknown".to_string() - } - } /// Extract service name from metric name (e.g., "backup_service_gitea_status" -> "gitea") fn extract_service_name(metric_name: &str) -> Option { @@ -324,9 +277,6 @@ impl Widget for BackupWidget { } } - fn render(&mut self, frame: &mut Frame, area: Rect) { - self.render_with_scroll(frame, area, 0); - } } impl BackupWidget { diff --git a/dashboard/src/ui/widgets/cpu.rs b/dashboard/src/ui/widgets/cpu.rs index 0a64b53..d7ec2cd 100644 --- a/dashboard/src/ui/widgets/cpu.rs +++ b/dashboard/src/ui/widgets/cpu.rs @@ -1,139 +1 @@ -use cm_dashboard_shared::{Metric, Status}; -use ratatui::{ - layout::{Constraint, Direction, Layout, Rect}, - widgets::Paragraph, - Frame, -}; -use tracing::debug; - -use super::Widget; -use crate::ui::theme::{StatusIcons, Typography}; - -/// CPU widget displaying load, temperature, and frequency -#[derive(Clone)] -pub struct CpuWidget { - /// CPU load averages (1, 5, 15 minutes) - load_1min: Option, - load_5min: Option, - load_15min: Option, - /// CPU temperature in Celsius - temperature: Option, - /// CPU frequency in MHz - frequency: Option, - /// Aggregated status - status: Status, - /// Last update indicator - has_data: bool, -} - -impl CpuWidget { - pub fn new() -> Self { - Self { - load_1min: None, - load_5min: None, - load_15min: None, - temperature: None, - frequency: None, - status: Status::Unknown, - has_data: false, - } - } - - /// Format load average for display - fn format_load(&self) -> String { - match (self.load_1min, self.load_5min, self.load_15min) { - (Some(l1), Some(l5), Some(l15)) => { - format!("{:.2} {:.2} {:.2}", l1, l5, l15) - } - _ => "— — —".to_string(), - } - } - - /// Format frequency for display - fn format_frequency(&self) -> String { - match self.frequency { - Some(freq) => format!("{:.1} MHz", freq), - None => "— MHz".to_string(), - } - } -} - -impl Widget for CpuWidget { - fn update_from_metrics(&mut self, metrics: &[&Metric]) { - debug!("CPU widget updating with {} metrics", metrics.len()); - - // Reset status aggregation - let mut statuses = Vec::new(); - - for metric in metrics { - match metric.name.as_str() { - "cpu_load_1min" => { - if let Some(value) = metric.value.as_f32() { - self.load_1min = Some(value); - statuses.push(metric.status); - } - } - "cpu_load_5min" => { - if let Some(value) = metric.value.as_f32() { - self.load_5min = Some(value); - statuses.push(metric.status); - } - } - "cpu_load_15min" => { - if let Some(value) = metric.value.as_f32() { - self.load_15min = Some(value); - statuses.push(metric.status); - } - } - "cpu_temperature_celsius" => { - if let Some(value) = metric.value.as_f32() { - self.temperature = Some(value); - statuses.push(metric.status); - } - } - "cpu_frequency_mhz" => { - if let Some(value) = metric.value.as_f32() { - self.frequency = Some(value); - statuses.push(metric.status); - } - } - _ => {} - } - } - - // Aggregate status - self.status = if statuses.is_empty() { - Status::Unknown - } else { - Status::aggregate(&statuses) - }; - - self.has_data = !metrics.is_empty(); - - debug!( - "CPU widget updated: load={:?}, temp={:?}, freq={:?}, status={:?}", - self.load_1min, self.temperature, self.frequency, self.status - ); - } - - fn render(&mut self, frame: &mut Frame, area: Rect) { - let content_chunks = Layout::default() - .direction(Direction::Vertical) - .constraints([Constraint::Length(1), Constraint::Length(1)]) - .split(area); - let cpu_title = Paragraph::new("CPU:").style(Typography::widget_title()); - frame.render_widget(cpu_title, content_chunks[0]); - let load_freq_spans = StatusIcons::create_status_spans( - self.status, - &format!("Load: {} • {}", self.format_load(), self.format_frequency()), - ); - let load_freq_para = Paragraph::new(ratatui::text::Line::from(load_freq_spans)); - frame.render_widget(load_freq_para, content_chunks[1]); - } -} - -impl Default for CpuWidget { - fn default() -> Self { - Self::new() - } -} +// 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 index dd0b3f6..41bcbce 100644 --- a/dashboard/src/ui/widgets/memory.rs +++ b/dashboard/src/ui/widgets/memory.rs @@ -1,253 +1 @@ -use cm_dashboard_shared::{Metric, Status}; -use ratatui::{ - layout::{Constraint, Direction, Layout, Rect}, - widgets::Paragraph, - Frame, -}; -use tracing::debug; - -use super::Widget; -use crate::ui::theme::{StatusIcons, Typography}; - -/// Memory widget displaying usage, totals, and swap information -#[derive(Clone)] -pub struct MemoryWidget { - /// Memory usage percentage - usage_percent: Option, - /// Total memory in GB - total_gb: Option, - /// Used memory in GB - used_gb: Option, - /// Available memory in GB - available_gb: Option, - /// Total swap in GB - swap_total_gb: Option, - /// Used swap in GB - swap_used_gb: Option, - /// /tmp directory size in MB - tmp_size_mb: Option, - /// /tmp total size in MB - tmp_total_mb: Option, - /// /tmp usage percentage - tmp_usage_percent: Option, - /// Aggregated status - status: Status, - /// Last update indicator - has_data: bool, -} - -impl MemoryWidget { - pub fn new() -> Self { - Self { - usage_percent: None, - total_gb: None, - used_gb: None, - available_gb: None, - swap_total_gb: None, - swap_used_gb: None, - tmp_size_mb: None, - tmp_total_mb: None, - tmp_usage_percent: None, - status: Status::Unknown, - has_data: false, - } - } - - /// Get memory usage percentage for gauge - fn get_memory_percentage(&self) -> u16 { - match self.usage_percent { - Some(percent) => percent.min(100.0).max(0.0) as u16, - None => { - // Calculate from used/total if percentage not available - match (self.used_gb, self.total_gb) { - (Some(used), Some(total)) if total > 0.0 => { - let percent = (used / total * 100.0).min(100.0).max(0.0); - percent as u16 - } - _ => 0, - } - } - } - } - - /// Format size with proper units (kB/MB/GB) - fn format_size_units(size_mb: f32) -> String { - if size_mb >= 1024.0 { - // Convert to GB - let size_gb = size_mb / 1024.0; - format!("{:.1}GB", size_gb) - } else if size_mb >= 1.0 { - // Show as MB - format!("{:.0}MB", size_mb) - } else if size_mb >= 0.001 { - // Convert to kB - let size_kb = size_mb * 1024.0; - format!("{:.0}kB", size_kb) - } else { - // Show very small sizes in bytes - let size_bytes = size_mb * 1024.0 * 1024.0; - format!("{:.0}B", size_bytes) - } - } - - /// Format /tmp usage as "xx% yyykB/MB/GB/zzzGB" - fn format_tmp_usage(&self) -> String { - match (self.tmp_usage_percent, self.tmp_size_mb, self.tmp_total_mb) { - (Some(percent), Some(used_mb), Some(total_mb)) => { - let used_str = Self::format_size_units(used_mb); - let total_str = Self::format_size_units(total_mb); - format!("{:.1}% {}/{}", percent, used_str, total_str) - } - (Some(percent), Some(used_mb), None) => { - let used_str = Self::format_size_units(used_mb); - format!("{:.1}% {}", percent, used_str) - } - (None, Some(used_mb), Some(total_mb)) => { - let used_str = Self::format_size_units(used_mb); - let total_str = Self::format_size_units(total_mb); - format!("{}/{}", used_str, total_str) - } - (None, Some(used_mb), None) => Self::format_size_units(used_mb), - _ => "—".to_string(), - } - } - - /// Get tmp status based on usage percentage - fn get_tmp_status(&self) -> Status { - if let Some(tmp_percent) = self.tmp_usage_percent { - if tmp_percent >= 90.0 { - Status::Critical - } else if tmp_percent >= 70.0 { - Status::Warning - } else { - Status::Ok - } - } else { - Status::Unknown - } - } -} - -impl Widget for MemoryWidget { - fn update_from_metrics(&mut self, metrics: &[&Metric]) { - debug!("Memory widget updating with {} metrics", metrics.len()); - - // Reset status aggregation - let mut statuses = Vec::new(); - - for metric in metrics { - match metric.name.as_str() { - "memory_usage_percent" => { - if let Some(value) = metric.value.as_f32() { - self.usage_percent = Some(value); - statuses.push(metric.status); - } - } - "memory_total_gb" => { - if let Some(value) = metric.value.as_f32() { - self.total_gb = Some(value); - statuses.push(metric.status); - } - } - "memory_used_gb" => { - if let Some(value) = metric.value.as_f32() { - self.used_gb = Some(value); - statuses.push(metric.status); - } - } - "memory_available_gb" => { - if let Some(value) = metric.value.as_f32() { - self.available_gb = Some(value); - statuses.push(metric.status); - } - } - "memory_swap_total_gb" => { - if let Some(value) = metric.value.as_f32() { - self.swap_total_gb = Some(value); - statuses.push(metric.status); - } - } - "memory_swap_used_gb" => { - if let Some(value) = metric.value.as_f32() { - self.swap_used_gb = Some(value); - statuses.push(metric.status); - } - } - "disk_tmp_size_mb" => { - if let Some(value) = metric.value.as_f32() { - self.tmp_size_mb = Some(value); - statuses.push(metric.status); - } - } - "disk_tmp_total_mb" => { - if let Some(value) = metric.value.as_f32() { - self.tmp_total_mb = Some(value); - statuses.push(metric.status); - } - } - "disk_tmp_usage_percent" => { - if let Some(value) = metric.value.as_f32() { - self.tmp_usage_percent = Some(value); - statuses.push(metric.status); - } - } - _ => {} - } - } - - // Aggregate status - self.status = if statuses.is_empty() { - Status::Unknown - } else { - Status::aggregate(&statuses) - }; - - self.has_data = !metrics.is_empty(); - - debug!("Memory widget updated: usage={:?}%, total={:?}GB, swap_total={:?}GB, tmp={:?}/{:?}MB, status={:?}", - self.usage_percent, self.total_gb, self.swap_total_gb, self.tmp_size_mb, self.tmp_total_mb, self.status); - } - - fn render(&mut self, frame: &mut Frame, area: Rect) { - let content_chunks = Layout::default() - .direction(Direction::Vertical) - .constraints([ - Constraint::Length(1), - Constraint::Length(1), - Constraint::Length(1), - ]) - .split(area); - let mem_title = Paragraph::new("RAM:").style(Typography::widget_title()); - frame.render_widget(mem_title, content_chunks[0]); - - // Format used and total memory with smart units, percentage, and status icon - let used_str = self - .used_gb - .map_or("—".to_string(), |v| Self::format_size_units(v * 1024.0)); // Convert GB to MB for formatting - let total_str = self - .total_gb - .map_or("—".to_string(), |v| Self::format_size_units(v * 1024.0)); // Convert GB to MB for formatting - let percentage = self.get_memory_percentage(); - let mem_details_spans = StatusIcons::create_status_spans( - self.status, - &format!("Used: {}% {}/{}", percentage, used_str, total_str), - ); - let mem_details_para = Paragraph::new(ratatui::text::Line::from(mem_details_spans)); - frame.render_widget(mem_details_para, content_chunks[1]); - - // /tmp usage line with status icon - let tmp_status = self.get_tmp_status(); - let tmp_spans = StatusIcons::create_status_spans( - tmp_status, - &format!("tmp: {}", self.format_tmp_usage()), - ); - let tmp_para = Paragraph::new(ratatui::text::Line::from(tmp_spans)); - frame.render_widget(tmp_para, content_chunks[2]); - } -} - -impl Default for MemoryWidget { - fn default() -> Self { - Self::new() - } -} +// 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 026c6ec..d2dd2ab 100644 --- a/dashboard/src/ui/widgets/mod.rs +++ b/dashboard/src/ui/widgets/mod.rs @@ -1,5 +1,4 @@ use cm_dashboard_shared::Metric; -use ratatui::{layout::Rect, Frame}; pub mod backup; pub mod cpu; @@ -16,6 +15,4 @@ pub trait Widget { /// Update widget with new metrics data fn update_from_metrics(&mut self, metrics: &[&Metric]); - /// Render the widget to a terminal frame - fn render(&mut self, frame: &mut Frame, area: Rect); } diff --git a/dashboard/src/ui/widgets/services.rs b/dashboard/src/ui/widgets/services.rs index b8e8d00..35f18ce 100644 --- a/dashboard/src/ui/widgets/services.rs +++ b/dashboard/src/ui/widgets/services.rs @@ -163,15 +163,6 @@ impl ServicesWidget { (icon.to_string(), info.status.clone(), status_color) } - /// Create spans for sub-service with icon next to name - fn create_sub_service_spans( - &self, - name: &str, - info: &ServiceInfo, - is_last: bool, - ) -> Vec> { - self.create_sub_service_spans_with_status(name, info, is_last, None) - } /// Create spans for sub-service with icon next to name, considering command status fn create_sub_service_spans_with_status( @@ -432,16 +423,9 @@ impl Widget for ServicesWidget { ); } - fn render(&mut self, frame: &mut Frame, area: Rect) { - self.render_with_focus(frame, area, false); - } } impl ServicesWidget { - /// Render with optional focus indicator and scroll support - pub fn render_with_focus(&mut self, frame: &mut Frame, area: Rect, is_focused: bool) { - self.render_with_focus_and_scroll(frame, area, is_focused, 0); - } /// Render with focus, scroll, and command status for visual feedback pub fn render_with_command_status(&mut self, frame: &mut Frame, area: Rect, is_focused: bool, scroll_offset: usize, command_status: Option<&CommandStatus>) { @@ -635,167 +619,6 @@ impl ServicesWidget { } } } - - /// Render with focus indicator and scroll offset - pub fn render_with_focus_and_scroll(&mut self, frame: &mut Frame, area: Rect, is_focused: bool, scroll_offset: usize) { - let services_block = if is_focused { - Components::focused_widget_block("services") - } else { - Components::widget_block("services") - }; - let inner_area = services_block.inner(area); - frame.render_widget(services_block, area); - - let content_chunks = Layout::default() - .direction(Direction::Vertical) - .constraints([Constraint::Length(1), Constraint::Min(0)]) - .split(inner_area); - - // Header - let header = format!( - "{:<25} {:<10} {:<8} {:<8}", - "Service:", "Status:", "RAM:", "Disk:" - ); - let header_para = Paragraph::new(header).style(Typography::muted()); - frame.render_widget(header_para, content_chunks[0]); - - // Check if we have any services to display - if self.parent_services.is_empty() && self.sub_services.is_empty() { - let empty_text = Paragraph::new("No process data").style(Typography::muted()); - frame.render_widget(empty_text, content_chunks[1]); - return; - } - - // Build hierarchical service list for display - let mut display_lines: Vec<(String, Status, bool, Option<(ServiceInfo, bool)>)> = Vec::new(); - - // Sort parent services alphabetically for consistent order - let mut parent_services: Vec<_> = self.parent_services.iter().collect(); - parent_services.sort_by(|(a, _), (b, _)| a.cmp(b)); - - for (parent_name, parent_info) in parent_services { - // Add parent service line - let parent_line = self.format_parent_service_line(parent_name, parent_info); - display_lines.push((parent_line, parent_info.widget_status, false, None)); // false = not sub-service - - // Add sub-services for this parent (if any) - if let Some(sub_list) = self.sub_services.get(parent_name) { - // Sort sub-services by name for consistent display - let mut sorted_subs = sub_list.clone(); - sorted_subs.sort_by(|(a, _), (b, _)| a.cmp(b)); - - for (i, (sub_name, sub_info)) in sorted_subs.iter().enumerate() { - let is_last_sub = i == sorted_subs.len() - 1; - // Store sub-service info for custom span rendering - display_lines.push(( - sub_name.clone(), - sub_info.widget_status, - true, - Some((sub_info.clone(), is_last_sub)), - )); // true = sub-service, with is_last info - } - } - } - - // Apply scroll offset and render visible lines - let available_lines = content_chunks[1].height as usize; - let total_lines = display_lines.len(); - - // Calculate scroll boundaries - let max_scroll = if total_lines > available_lines { - total_lines - available_lines - } else { - total_lines.saturating_sub(1) - }; - let effective_scroll = scroll_offset.min(max_scroll); - - // Get visible lines after scrolling - let visible_lines: Vec<_> = display_lines - .iter() - .skip(effective_scroll) - .take(available_lines) - .collect(); - - let lines_to_show = visible_lines.len(); - - if lines_to_show > 0 { - let service_chunks = Layout::default() - .direction(Direction::Vertical) - .constraints(vec![Constraint::Length(1); lines_to_show]) - .split(content_chunks[1]); - - for (i, (line_text, line_status, is_sub, sub_info)) in visible_lines.iter().enumerate() - { - let actual_index = effective_scroll + i; // Real index in the full list - - // Only parent services can be selected - calculate parent service index - let is_selected = if !*is_sub { - // This is a parent service - count how many parent services came before this one - let parent_index = self.calculate_parent_service_index(&actual_index); - parent_index == self.selected_index - } else { - false // Sub-services are never selected - }; - - let mut spans = if *is_sub && sub_info.is_some() { - // Use custom sub-service span creation - let (service_info, is_last) = sub_info.as_ref().unwrap(); - self.create_sub_service_spans(line_text, service_info, *is_last) - } else { - // Use regular status spans for parent services - StatusIcons::create_status_spans(*line_status, line_text) - }; - - // Apply selection highlighting to parent services only, preserving status icon color - // Only show selection when Services panel is focused - if is_selected && !*is_sub && is_focused { - for (i, span) in spans.iter_mut().enumerate() { - if i == 0 { - // First span is the status icon - preserve its color - span.style = span.style.bg(Theme::highlight()); - } else { - // Other spans (text) get full selection highlighting - span.style = span.style - .bg(Theme::highlight()) - .fg(Theme::background()); - } - } - } - - let service_para = Paragraph::new(ratatui::text::Line::from(spans)); - - frame.render_widget(service_para, service_chunks[i]); - } - } - - // Show scroll indicator if there are more services than we can display - if total_lines > available_lines { - let hidden_above = effective_scroll; - let hidden_below = total_lines.saturating_sub(effective_scroll + available_lines); - - if hidden_above > 0 || hidden_below > 0 { - let scroll_text = if hidden_above > 0 && hidden_below > 0 { - format!("... {} above, {} below", hidden_above, hidden_below) - } else if hidden_above > 0 { - format!("... {} more above", hidden_above) - } else { - format!("... {} more below", hidden_below) - }; - - if available_lines > 0 && lines_to_show > 0 { - let last_line_area = Rect { - x: content_chunks[1].x, - y: content_chunks[1].y + (lines_to_show - 1) as u16, - width: content_chunks[1].width, - height: 1, - }; - - let scroll_para = Paragraph::new(scroll_text).style(Typography::muted()); - frame.render_widget(scroll_para, last_line_area); - } - } - } - } } impl Default for ServicesWidget { diff --git a/dashboard/src/ui/widgets/system.rs b/dashboard/src/ui/widgets/system.rs index 4551cde..c3793a3 100644 --- a/dashboard/src/ui/widgets/system.rs +++ b/dashboard/src/ui/widgets/system.rs @@ -249,7 +249,7 @@ impl SystemWidget { } /// Render storage section with tree structure - fn render_storage(&self) -> Vec { + fn render_storage(&self) -> Vec> { let mut lines = Vec::new(); for pool in &self.storage_pools { @@ -410,9 +410,6 @@ impl Widget for SystemWidget { self.update_storage_from_metrics(metrics); } - fn render(&mut self, frame: &mut Frame, area: Rect) { - self.render_with_scroll(frame, area, 0); - } } impl SystemWidget {