diff --git a/CLAUDE.md b/CLAUDE.md index a83b869..d6f1285 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -6,52 +6,65 @@ A high-performance Rust-based TUI dashboard for monitoring CMTEC infrastructure. ## Implementation Strategy -### Next Phase: Systemd Collector Optimization (Based on TODO.md) +### Current Implementation Status -**Current Status**: Reverted to working baseline (commit 245e546) after optimization broke service discovery. +**Systemd Collector Optimization - COMPLETED** ✅ -**Planned Implementation Steps** (step-by-step to avoid breaking functionality): +All phases successfully implemented: +- ✅ **Phase 1**: Exact name filtering implemented +- ✅ **Phase 2**: User service collection removed +- ✅ **Phase 3**: Wildcard pattern support added +- ✅ **Phase 4**: Status caching and systemctl call optimization completed +- ❌ **Phase 5**: Skipped (would increase systemctl calls vs current caching) -**Phase 1: Exact Name Filtering** -- Replace `contains()` matching with exact name matching for service filters -- Change `service_name.contains(pattern) || pattern.contains(service_name)` to `service_name == pattern` -- Test: Ensure cmbox remains visible with exact service names in config -- Commit and test after each change +**Performance Results:** +- Reduced from ~21 systemctl calls to 1 call every 10 seconds (configurable) +- Fixed RwLock deadlock issues +- Removed hardcoded discovery intervals -**Phase 2: Remove User Service Collection** -- Remove all `sudo -u` systemctl commands for user services -- Remove user_unit_files_output and user_units_output logic -- Keep only system service discovery via `systemctl list-units --type=service` -- Test: Verify system services still discovered correctly +### Next Priority: System Panel Enhancement (Based on TODO.md) -**Phase 3: Add Wildcard Support** -- Implement glob pattern matching for service filters -- Support patterns like "nginx*" to match "nginx", "nginx-config-reload", etc. -- Use fnmatch or similar for wildcard expansion -- Test: Verify patterns work as expected +**Target Layout:** +``` +NixOS: +Version: xxxxxxxxxx +Active users: cm, simon +CPU: +● Load: 0.02 0.31 0.86 • 3000 MHz +RAM: +● Usage: 33% 2.6GB/7.6GB +● /tmp: 0% 0B/2.0GB +Storage: +● root (Single): + ├─ ● nvme0n1 Temp: 40C Wear: 4% + └─ ● 8% 75.0GB/906.2GB +``` -**Phase 4: Optimize systemctl Calls** -- Cache service status information during discovery -- Eliminate redundant `systemctl is-active` and `systemctl show` calls per service -- Parse status from `systemctl list-units` output directly -- Test: Ensure performance improvement without functionality loss +**Implementation Tasks:** +1. **NixOS Version Display** + - Collect system version information + - Show timestamp/version for latest nixos rebuild -**Phase 5: Include-Only Discovery** -- Remove auto-discovery of all services -- Only check services explicitly listed in service_name_filters -- Skip systemctl discovery entirely, use configured list directly -- Test: Verify only configured services are monitored +2. **Active Users Display** + - Implement user session detection + - Show currently logged in/active users -**Critical Requirements:** -- Each phase must be tested independently -- cmbox must remain visible in dashboard after each change -- No functionality regressions allowed -- Commit each phase separately with descriptive messages +3. **System Widget Layout Update** + - Update dashboard to match new layout specification + - Integrate NixOS version and user information -**Rollback Strategy:** -- If any phase breaks functionality, immediately revert that specific commit -- Do not attempt to "fix forward" - revert and redesign the problematic step -- Each phase should be atomic and independently revertible +### Future Priorities + +**Keyboard Navigation (Dashboard):** +- Change host switching to "Shift-Tab" +- Add panel navigation with "Tab" +- Add scrolling support for overflow content + +**Remote Execution (Agent/Dashboard):** +- Dynamic statusbar with context shortcuts +- Remote nixos rebuild commands +- Service start/stop/restart controls +- Backup trigger functionality ## Core Architecture Principles - CRITICAL diff --git a/TODO.md b/TODO.md index c9c22e4..4333cc5 100644 --- a/TODO.md +++ b/TODO.md @@ -16,13 +16,13 @@ NixOS: Version: xxxxxxxxxx Active users: cm, simon CPU: -● Load: 0.02 0.31 0.86 • 3000.2 MHz +● Load: 0.02 0.31 0.86 • 3000 MHz RAM: ● Usage: 33% 2.6GB/7.6GB ● /tmp: 0% 0B/2.0GB Storage: ● root (Single): - ├─ ● nvme0n1 Temp: 40C Wear: 4% + ├─ ● nvme0n1 T: 40C • W: 4% └─ ● 8% 75.0GB/906.2GB ''' diff --git a/agent/src/collectors/mod.rs b/agent/src/collectors/mod.rs index 36814fa..b8cbb0c 100644 --- a/agent/src/collectors/mod.rs +++ b/agent/src/collectors/mod.rs @@ -7,6 +7,7 @@ pub mod cpu; pub mod disk; pub mod error; pub mod memory; +pub mod nixos; pub mod systemd; pub use error::CollectorError; diff --git a/agent/src/collectors/nixos.rs b/agent/src/collectors/nixos.rs new file mode 100644 index 0000000..452415d --- /dev/null +++ b/agent/src/collectors/nixos.rs @@ -0,0 +1,163 @@ +use async_trait::async_trait; +use cm_dashboard_shared::{Metric, MetricValue, Status, StatusTracker}; +use std::process::Command; +use tracing::debug; + +use super::{Collector, CollectorError}; +use crate::config::NixOSConfig; + +/// NixOS system information collector +/// +/// Collects NixOS-specific system information including: +/// - 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 } + } + + /// Get NixOS version information + fn get_nixos_version(&self) -> Result<(String, Option), Box> { + // Try nixos-version command first + if let Ok(output) = Command::new("nixos-version").output() { + if output.status.success() { + let version_line = String::from_utf8_lossy(&output.stdout); + let version = version_line.trim().to_string(); + + // Extract build date if present (format: "24.05.20241023.abcdef (Vicuna)") + let build_date = if version.contains('.') { + let parts: Vec<&str> = version.split('.').collect(); + if parts.len() >= 3 && parts[2].len() >= 8 { + Some(parts[2][..8].to_string()) // Extract YYYYMMDD + } else { + None + } + } else { + None + }; + + return Ok((version, build_date)); + } + } + + // Fallback to /etc/os-release + if let Ok(content) = std::fs::read_to_string("/etc/os-release") { + for line in content.lines() { + if line.starts_with("VERSION_ID=") { + let version = line + .strip_prefix("VERSION_ID=") + .unwrap_or("") + .trim_matches('"') + .to_string(); + return Ok((version, None)); + } + } + } + + Err("Could not determine NixOS version".into()) + } + + /// Get currently active users + fn get_active_users(&self) -> Result, Box> { + let output = Command::new("who").output()?; + + if !output.status.success() { + return Err("who command failed".into()); + } + + let who_output = String::from_utf8_lossy(&output.stdout); + let mut users = std::collections::HashSet::new(); + + for line in who_output.lines() { + if let Some(username) = line.split_whitespace().next() { + if !username.is_empty() { + users.insert(username.to_string()); + } + } + } + + Ok(users.into_iter().collect()) + } +} + +#[async_trait] +impl Collector for NixOSCollector { + fn name(&self) -> &str { + "nixos" + } + + async fn collect(&self, _status_tracker: &mut StatusTracker) -> Result, CollectorError> { + debug!("Collecting NixOS system information"); + let mut metrics = Vec::new(); + let timestamp = chrono::Utc::now().timestamp() as u64; + + // Collect NixOS version information + match self.get_nixos_version() { + Ok((version, build_date)) => { + metrics.push(Metric { + name: "system_nixos_version".to_string(), + value: MetricValue::String(version), + unit: None, + description: Some("NixOS version".to_string()), + status: Status::Ok, + timestamp, + }); + + if let Some(date) = build_date { + metrics.push(Metric { + name: "system_nixos_build_date".to_string(), + value: MetricValue::String(date), + unit: None, + description: Some("NixOS build date".to_string()), + status: Status::Ok, + timestamp, + }); + } + } + Err(e) => { + debug!("Failed to get NixOS version: {}", e); + metrics.push(Metric { + name: "system_nixos_version".to_string(), + value: MetricValue::String("unknown".to_string()), + unit: None, + description: Some("NixOS version (failed to detect)".to_string()), + status: Status::Unknown, + timestamp, + }); + } + } + + // Collect active users + match self.get_active_users() { + Ok(users) => { + let users_str = users.join(", "); + metrics.push(Metric { + name: "system_active_users".to_string(), + value: MetricValue::String(users_str), + unit: None, + description: Some("Currently active users".to_string()), + status: Status::Ok, + timestamp, + }); + } + Err(e) => { + debug!("Failed to get active users: {}", e); + metrics.push(Metric { + name: "system_active_users".to_string(), + value: MetricValue::String("unknown".to_string()), + unit: None, + description: Some("Active users (failed to detect)".to_string()), + status: Status::Unknown, + timestamp, + }); + } + } + + debug!("Collected {} NixOS metrics", metrics.len()); + Ok(metrics) + } +} \ No newline at end of file diff --git a/agent/src/config/mod.rs b/agent/src/config/mod.rs index e9bd11f..0053f70 100644 --- a/agent/src/config/mod.rs +++ b/agent/src/config/mod.rs @@ -39,6 +39,7 @@ pub struct CollectorConfig { pub smart: SmartConfig, pub backup: BackupConfig, pub network: NetworkConfig, + pub nixos: NixOSConfig, } /// CPU collector configuration @@ -113,6 +114,13 @@ pub struct SmartConfig { pub wear_critical_percent: f32, } +/// NixOS collector configuration +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct NixOSConfig { + pub enabled: bool, + pub interval_seconds: u64, +} + /// Backup collector configuration #[derive(Debug, Clone, Serialize, Deserialize)] pub struct BackupConfig { diff --git a/agent/src/metrics/mod.rs b/agent/src/metrics/mod.rs index 8376cd5..9231227 100644 --- a/agent/src/metrics/mod.rs +++ b/agent/src/metrics/mod.rs @@ -7,7 +7,7 @@ use tracing::{debug, error, info}; use crate::cache::MetricCacheManager; use crate::collectors::{ backup::BackupCollector, cpu::CpuCollector, disk::DiskCollector, memory::MemoryCollector, - systemd::SystemdCollector, Collector, + nixos::NixOSCollector, systemd::SystemdCollector, Collector, }; use crate::config::{AgentConfig, CollectorConfig}; @@ -100,6 +100,12 @@ impl MetricCollectionManager { collectors.push(Box::new(backup_collector)); info!("Backup collector initialized"); } + + if config.nixos.enabled { + let nixos_collector = NixOSCollector::new(config.nixos.clone()); + collectors.push(Box::new(nixos_collector)); + info!("NixOS collector initialized"); + } } } diff --git a/dashboard/src/ui/mod.rs b/dashboard/src/ui/mod.rs index 35c62f4..0943467 100644 --- a/dashboard/src/ui/mod.rs +++ b/dashboard/src/ui/mod.rs @@ -16,15 +16,13 @@ pub mod widgets; use crate::metrics::MetricStore; use cm_dashboard_shared::{Metric, Status}; use theme::{Components, Layout as ThemeLayout, StatusIcons, Theme, Typography}; -use widgets::{BackupWidget, CpuWidget, MemoryWidget, ServicesWidget, Widget}; +use widgets::{BackupWidget, ServicesWidget, SystemWidget, Widget}; /// Widget states for a specific host #[derive(Clone)] pub struct HostWidgets { - /// CPU widget state - pub cpu_widget: CpuWidget, - /// Memory widget state - pub memory_widget: MemoryWidget, + /// System widget state (includes CPU, Memory, NixOS info, Storage) + pub system_widget: SystemWidget, /// Services widget state pub services_widget: ServicesWidget, /// Backup widget state @@ -36,8 +34,7 @@ pub struct HostWidgets { impl HostWidgets { pub fn new() -> Self { Self { - cpu_widget: CpuWidget::new(), - memory_widget: MemoryWidget::new(), + system_widget: SystemWidget::new(), services_widget: ServicesWidget::new(), backup_widget: BackupWidget::new(), last_update: None, @@ -115,10 +112,27 @@ impl TuiApp { // Now get host widgets and update them let host_widgets = self.get_or_create_host_widgets(&hostname); - host_widgets.cpu_widget.update_from_metrics(&cpu_metrics); - host_widgets - .memory_widget - .update_from_metrics(&memory_metrics); + // Collect all system metrics (CPU, memory, NixOS, disk/storage) + let mut system_metrics = cpu_metrics; + system_metrics.extend(memory_metrics); + + // Add NixOS metrics + let nixos_metrics: Vec<&Metric> = all_metrics + .iter() + .filter(|m| m.name.starts_with("system_nixos") || m.name.starts_with("system_active")) + .copied() + .collect(); + system_metrics.extend(nixos_metrics); + + // Add disk/storage metrics + let disk_metrics: Vec<&Metric> = all_metrics + .iter() + .filter(|m| m.name.starts_with("disk_")) + .copied() + .collect(); + system_metrics.extend(disk_metrics); + + host_widgets.system_widget.update_from_metrics(&system_metrics); host_widgets .services_widget .update_from_metrics(&service_metrics); @@ -396,22 +410,11 @@ impl TuiApp { let system_block = Components::widget_block("system"); let inner_area = system_block.inner(area); frame.render_widget(system_block, area); - let content_chunks = ratatui::layout::Layout::default() - .direction(Direction::Vertical) - .constraints([ - Constraint::Length(ThemeLayout::CPU_SECTION_HEIGHT), // CPU section (title, load) - Constraint::Length(ThemeLayout::MEMORY_SECTION_HEIGHT), // Memory section (title, used, /tmp) - Constraint::Min(0), // Storage section - ]) - .split(inner_area); - // Get current host widgets, create if none exist if let Some(hostname) = self.current_host.clone() { let host_widgets = self.get_or_create_host_widgets(&hostname); - host_widgets.cpu_widget.render(frame, content_chunks[0]); - host_widgets.memory_widget.render(frame, content_chunks[1]); + host_widgets.system_widget.render(frame, inner_area); } - self.render_storage_section(frame, content_chunks[2], metric_store); } fn render_backup_panel(&mut self, frame: &mut Frame, area: Rect) { diff --git a/dashboard/src/ui/widgets/mod.rs b/dashboard/src/ui/widgets/mod.rs index 24dc8a9..d2a6bf1 100644 --- a/dashboard/src/ui/widgets/mod.rs +++ b/dashboard/src/ui/widgets/mod.rs @@ -5,11 +5,13 @@ pub mod backup; pub mod cpu; pub mod memory; pub mod services; +pub mod system; pub use backup::BackupWidget; pub use cpu::CpuWidget; pub use memory::MemoryWidget; pub use services::ServicesWidget; +pub use system::SystemWidget; /// Widget trait for UI components that display metrics pub trait Widget { diff --git a/dashboard/src/ui/widgets/system.rs b/dashboard/src/ui/widgets/system.rs new file mode 100644 index 0000000..c178bec --- /dev/null +++ b/dashboard/src/ui/widgets/system.rs @@ -0,0 +1,438 @@ +use cm_dashboard_shared::{Metric, MetricValue, Status}; +use ratatui::{ + layout::Rect, + style::Style, + text::{Line, Span, Text}, + widgets::{Block, Borders, Paragraph}, + Frame, +}; + +use super::Widget; +use crate::ui::theme::{StatusIcons, Typography}; + +/// System widget displaying NixOS info, CPU, RAM, and Storage in unified layout +#[derive(Clone)] +pub struct SystemWidget { + // NixOS information + nixos_version: Option, + nixos_build_date: Option, + active_users: Option, + + // CPU metrics + cpu_load_1min: Option, + cpu_load_5min: Option, + cpu_load_15min: Option, + cpu_frequency: Option, + cpu_status: Status, + + // Memory metrics + memory_usage_percent: Option, + memory_used_gb: Option, + memory_total_gb: Option, + tmp_usage_percent: Option, + tmp_used_gb: Option, + tmp_total_gb: Option, + memory_status: Status, + + // Storage metrics (collected from disk metrics) + storage_pools: Vec, + + // Overall status + has_data: bool, +} + +#[derive(Clone)] +struct StoragePool { + name: String, + pool_type: String, // "Single", "Raid0", etc. + drives: Vec, + usage_percent: Option, + used_gb: Option, + total_gb: Option, + status: Status, +} + +#[derive(Clone)] +struct StorageDrive { + name: String, + temperature: Option, + wear_percent: Option, + status: Status, +} + +impl SystemWidget { + pub fn new() -> Self { + Self { + nixos_version: None, + nixos_build_date: None, + active_users: None, + cpu_load_1min: None, + cpu_load_5min: None, + cpu_load_15min: None, + cpu_frequency: None, + cpu_status: Status::Unknown, + memory_usage_percent: None, + memory_used_gb: None, + memory_total_gb: None, + tmp_usage_percent: None, + tmp_used_gb: None, + tmp_total_gb: None, + memory_status: Status::Unknown, + storage_pools: Vec::new(), + has_data: false, + } + } + + /// Format CPU load averages + fn format_cpu_load(&self) -> String { + match (self.cpu_load_1min, self.cpu_load_5min, self.cpu_load_15min) { + (Some(l1), Some(l5), Some(l15)) => { + format!("{:.2} {:.2} {:.2}", l1, l5, l15) + } + _ => "— — —".to_string(), + } + } + + /// Format CPU frequency + fn format_cpu_frequency(&self) -> String { + match self.cpu_frequency { + Some(freq) => format!("{:.0} MHz", freq), + None => "— MHz".to_string(), + } + } + + /// Format memory usage + fn format_memory_usage(&self) -> String { + match (self.memory_usage_percent, self.memory_used_gb, self.memory_total_gb) { + (Some(pct), Some(used), Some(total)) => { + format!("{:.0}% {:.1}GB/{:.1}GB", pct, used, total) + } + _ => "—% —GB/—GB".to_string(), + } + } + + /// Format /tmp usage + fn format_tmp_usage(&self) -> String { + match (self.tmp_usage_percent, self.tmp_used_gb, self.tmp_total_gb) { + (Some(pct), Some(used), Some(total)) => { + let used_str = if used < 0.1 { + format!("{:.0}B", used * 1024.0) // Show as MB if very small + } else { + format!("{:.1}GB", used) + }; + format!("{:.0}% {}/{:.1}GB", pct, used_str, total) + } + _ => "—% —GB/—GB".to_string(), + } + } + + /// Parse storage metrics into pools and drives + fn update_storage_from_metrics(&mut self, metrics: &[&Metric]) { + let mut pools: std::collections::HashMap = std::collections::HashMap::new(); + + for metric in metrics { + if metric.name.starts_with("disk_") { + if let Some(pool_name) = self.extract_pool_name(&metric.name) { + let pool = pools.entry(pool_name.clone()).or_insert_with(|| StoragePool { + name: pool_name.clone(), + pool_type: "Single".to_string(), // Default, could be enhanced + drives: Vec::new(), + usage_percent: None, + used_gb: None, + total_gb: None, + status: Status::Unknown, + }); + + // Parse different metric types + if metric.name.contains("_usage_percent") { + if let MetricValue::Float(usage) = metric.value { + pool.usage_percent = Some(usage); + pool.status = metric.status.clone(); + } + } else if metric.name.contains("_used_gb") { + if let MetricValue::Float(used) = metric.value { + pool.used_gb = Some(used); + } + } else if metric.name.contains("_total_gb") { + if let MetricValue::Float(total) = metric.value { + pool.total_gb = Some(total); + } + } else if metric.name.contains("_temperature") { + if let Some(drive_name) = self.extract_drive_name(&metric.name) { + // Find existing drive or create new one + let drive_exists = pool.drives.iter().any(|d| d.name == drive_name); + if !drive_exists { + pool.drives.push(StorageDrive { + name: drive_name.clone(), + temperature: None, + wear_percent: None, + status: Status::Unknown, + }); + } + + if let Some(drive) = pool.drives.iter_mut().find(|d| d.name == drive_name) { + if let MetricValue::Float(temp) = metric.value { + drive.temperature = Some(temp); + drive.status = metric.status.clone(); + } + } + } + } else if metric.name.contains("_wear_percent") { + if let Some(drive_name) = self.extract_drive_name(&metric.name) { + // Find existing drive or create new one + let drive_exists = pool.drives.iter().any(|d| d.name == drive_name); + if !drive_exists { + pool.drives.push(StorageDrive { + name: drive_name.clone(), + temperature: None, + wear_percent: None, + status: Status::Unknown, + }); + } + + if let Some(drive) = pool.drives.iter_mut().find(|d| d.name == drive_name) { + if let MetricValue::Float(wear) = metric.value { + drive.wear_percent = Some(wear); + drive.status = metric.status.clone(); + } + } + } + } + } + } + } + + self.storage_pools = pools.into_values().collect(); + } + + /// Extract pool name from disk metric name + fn extract_pool_name(&self, metric_name: &str) -> Option { + if let Some(captures) = metric_name.strip_prefix("disk_") { + if let Some(pos) = captures.find('_') { + return Some(captures[..pos].to_string()); + } + } + None + } + + /// Extract drive name from disk metric name + fn extract_drive_name(&self, metric_name: &str) -> Option { + // Pattern: disk_pool_drive_metric + let parts: Vec<&str> = metric_name.split('_').collect(); + if parts.len() >= 3 && parts[0] == "disk" { + return Some(parts[2].to_string()); + } + None + } + + /// Render storage section with tree structure + fn render_storage(&self) -> Vec { + let mut lines = Vec::new(); + + lines.push(Line::from(vec![ + Span::styled("Storage:", Typography::widget_title()) + ])); + + for pool in &self.storage_pools { + // Pool header line + let usage_text = match (pool.usage_percent, pool.used_gb, pool.total_gb) { + (Some(pct), Some(used), Some(total)) => { + format!("{:.0}% {:.1}GB/{:.1}GB", pct, used, total) + } + _ => "—% —GB/—GB".to_string(), + }; + + let pool_spans = StatusIcons::create_status_spans( + pool.status.clone(), + &format!("{} ({}):", pool.name, pool.pool_type) + ); + lines.push(Line::from(pool_spans)); + + // Drive lines with tree structure + for (i, drive) in pool.drives.iter().enumerate() { + let tree_symbol = if i == pool.drives.len() - 1 { "└─" } else { "├─" }; + + let mut drive_info = Vec::new(); + if let Some(temp) = drive.temperature { + drive_info.push(format!("T: {:.0}C", temp)); + } + if let Some(wear) = drive.wear_percent { + drive_info.push(format!("W: {:.0}%", wear)); + } + let drive_text = if drive_info.is_empty() { + drive.name.clone() + } else { + format!("{} {}", drive.name, drive_info.join(" • ")) + }; + + let mut drive_spans = vec![ + Span::raw(" "), + Span::raw(tree_symbol), + Span::raw(" "), + ]; + drive_spans.extend(StatusIcons::create_status_spans(drive.status.clone(), &drive_text)); + lines.push(Line::from(drive_spans)); + } + + // Usage line + if pool.usage_percent.is_some() { + let tree_symbol = "└─"; + let mut usage_spans = vec![ + Span::raw(" "), + Span::raw(tree_symbol), + Span::raw(" "), + ]; + usage_spans.extend(StatusIcons::create_status_spans(pool.status.clone(), &usage_text)); + lines.push(Line::from(usage_spans)); + } + } + + lines + } +} + +impl Widget for SystemWidget { + fn update_from_metrics(&mut self, metrics: &[&Metric]) { + self.has_data = !metrics.is_empty(); + + for metric in metrics { + match metric.name.as_str() { + // NixOS metrics + "system_nixos_version" => { + if let MetricValue::String(version) = &metric.value { + self.nixos_version = Some(version.clone()); + } + } + "system_nixos_build_date" => { + if let MetricValue::String(date) = &metric.value { + self.nixos_build_date = Some(date.clone()); + } + } + "system_active_users" => { + if let MetricValue::String(users) = &metric.value { + self.active_users = Some(users.clone()); + } + } + + // CPU metrics + "cpu_load_1min" => { + if let MetricValue::Float(load) = metric.value { + self.cpu_load_1min = Some(load); + self.cpu_status = metric.status.clone(); + } + } + "cpu_load_5min" => { + if let MetricValue::Float(load) = metric.value { + self.cpu_load_5min = Some(load); + } + } + "cpu_load_15min" => { + if let MetricValue::Float(load) = metric.value { + self.cpu_load_15min = Some(load); + } + } + "cpu_frequency_mhz" => { + if let MetricValue::Float(freq) = metric.value { + self.cpu_frequency = Some(freq); + } + } + + // Memory metrics + "memory_usage_percent" => { + if let MetricValue::Float(usage) = metric.value { + self.memory_usage_percent = Some(usage); + self.memory_status = metric.status.clone(); + } + } + "memory_used_gb" => { + if let MetricValue::Float(used) = metric.value { + self.memory_used_gb = Some(used); + } + } + "memory_total_gb" => { + if let MetricValue::Float(total) = metric.value { + self.memory_total_gb = Some(total); + } + } + "disk_tmp_usage_percent" => { + if let MetricValue::Float(usage) = metric.value { + self.tmp_usage_percent = Some(usage); + } + } + "disk_tmp_used_gb" => { + if let MetricValue::Float(used) = metric.value { + self.tmp_used_gb = Some(used); + } + } + "disk_tmp_total_gb" => { + if let MetricValue::Float(total) = metric.value { + self.tmp_total_gb = Some(total); + } + } + _ => {} + } + } + + // Update storage from all disk metrics + self.update_storage_from_metrics(metrics); + } + + fn render(&mut self, frame: &mut Frame, area: Rect) { + let mut lines = Vec::new(); + + // NixOS section + lines.push(Line::from(vec![ + Span::styled("NixOS:", Typography::widget_title()) + ])); + + let version_text = self.nixos_version.as_deref().unwrap_or("unknown"); + lines.push(Line::from(vec![ + Span::styled(format!("Version: {}", version_text), Typography::secondary()) + ])); + + let users_text = self.active_users.as_deref().unwrap_or("unknown"); + lines.push(Line::from(vec![ + Span::styled(format!("Active users: {}", users_text), Typography::secondary()) + ])); + + // CPU section + lines.push(Line::from(vec![ + Span::styled("CPU:", Typography::widget_title()) + ])); + + let load_text = self.format_cpu_load(); + let freq_text = self.format_cpu_frequency(); + let cpu_spans = StatusIcons::create_status_spans( + self.cpu_status.clone(), + &format!("Load: {} • {}", load_text, freq_text) + ); + lines.push(Line::from(cpu_spans)); + + // RAM section + lines.push(Line::from(vec![ + Span::styled("RAM:", Typography::widget_title()) + ])); + + let memory_text = self.format_memory_usage(); + let memory_spans = StatusIcons::create_status_spans( + self.memory_status.clone(), + &format!("Usage: {}", memory_text) + ); + lines.push(Line::from(memory_spans)); + + let tmp_text = self.format_tmp_usage(); + let tmp_spans = StatusIcons::create_status_spans( + self.memory_status.clone(), + &format!("/tmp: {}", tmp_text) + ); + lines.push(Line::from(tmp_spans)); + + // Storage section with tree structure + lines.extend(self.render_storage()); + + let paragraph = Paragraph::new(Text::from(lines)) + .block(Block::default().borders(Borders::ALL).title("System")); + + frame.render_widget(paragraph, area); + } +} \ No newline at end of file