diff --git a/Cargo.lock b/Cargo.lock index a5b7e28..ca0f5b0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -279,7 +279,7 @@ checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" [[package]] name = "cm-dashboard" -version = "0.1.160" +version = "0.1.161" dependencies = [ "anyhow", "chrono", @@ -301,7 +301,7 @@ dependencies = [ [[package]] name = "cm-dashboard-agent" -version = "0.1.160" +version = "0.1.161" dependencies = [ "anyhow", "async-trait", @@ -324,7 +324,7 @@ dependencies = [ [[package]] name = "cm-dashboard-shared" -version = "0.1.160" +version = "0.1.161" dependencies = [ "chrono", "serde", diff --git a/agent/Cargo.toml b/agent/Cargo.toml index e0b23ec..5cd9b8e 100644 --- a/agent/Cargo.toml +++ b/agent/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cm-dashboard-agent" -version = "0.1.160" +version = "0.1.161" edition = "2021" [dependencies] diff --git a/agent/src/collectors/nixos.rs b/agent/src/collectors/nixos.rs index ec9d246..183811e 100644 --- a/agent/src/collectors/nixos.rs +++ b/agent/src/collectors/nixos.rs @@ -1,5 +1,5 @@ use async_trait::async_trait; -use cm_dashboard_shared::AgentData; +use cm_dashboard_shared::{AgentData, NetworkInterfaceData}; use std::fs; use std::process::Command; use tracing::debug; @@ -32,6 +32,9 @@ impl NixOSCollector { // Set NixOS build/generation information agent_data.build_version = self.get_nixos_generation().await; + // Collect network interfaces + agent_data.system.network.interfaces = self.get_network_interfaces().await; + // Set current timestamp agent_data.timestamp = chrono::Utc::now().timestamp() as u64; @@ -101,6 +104,72 @@ impl NixOSCollector { } } } + + /// Get network interfaces and their IP addresses + async fn get_network_interfaces(&self) -> Vec { + let mut interfaces = Vec::new(); + + // Use ip command with JSON output for easier parsing + match Command::new("ip").args(["-j", "addr"]).output() { + Ok(output) if output.status.success() => { + let json_str = String::from_utf8_lossy(&output.stdout); + + // Parse JSON output + if let Ok(json_data) = serde_json::from_str::(&json_str) { + if let Some(ifaces) = json_data.as_array() { + for iface in ifaces { + let name = iface["ifname"].as_str().unwrap_or("").to_string(); + + // Skip loopback and empty names + if name.is_empty() || name == "lo" { + continue; + } + + let mut ipv4_addresses = Vec::new(); + let mut ipv6_addresses = Vec::new(); + + // Extract IP addresses + if let Some(addr_info) = iface["addr_info"].as_array() { + for addr in addr_info { + if let Some(family) = addr["family"].as_str() { + if let Some(local) = addr["local"].as_str() { + match family { + "inet" => ipv4_addresses.push(local.to_string()), + "inet6" => { + // Skip link-local IPv6 addresses (fe80::) + if !local.starts_with("fe80:") { + ipv6_addresses.push(local.to_string()); + } + } + _ => {} + } + } + } + } + } + + // Only add interfaces that have at least one IP address + if !ipv4_addresses.is_empty() || !ipv6_addresses.is_empty() { + interfaces.push(NetworkInterfaceData { + name, + ipv4_addresses, + ipv6_addresses, + }); + } + } + } + } + } + Err(e) => { + debug!("Failed to execute ip command: {}", e); + } + Ok(output) => { + debug!("ip command failed with status: {}", output.status); + } + } + + interfaces + } } #[async_trait] diff --git a/dashboard/Cargo.toml b/dashboard/Cargo.toml index dd3e9c5..278b36a 100644 --- a/dashboard/Cargo.toml +++ b/dashboard/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cm-dashboard" -version = "0.1.160" +version = "0.1.161" edition = "2021" [dependencies] diff --git a/dashboard/src/ui/widgets/system.rs b/dashboard/src/ui/widgets/system.rs index 32ab695..9e46bf5 100644 --- a/dashboard/src/ui/widgets/system.rs +++ b/dashboard/src/ui/widgets/system.rs @@ -8,13 +8,16 @@ use ratatui::{ use crate::ui::theme::{StatusIcons, Typography}; -/// System widget displaying NixOS info, CPU, RAM, and Storage in unified layout +/// System widget displaying NixOS info, Network, CPU, RAM, and Storage in unified layout #[derive(Clone)] pub struct SystemWidget { // NixOS information nixos_build: Option, agent_hash: Option, - + + // Network interfaces + network_interfaces: Vec, + // CPU metrics cpu_load_1min: Option, cpu_load_5min: Option, @@ -89,6 +92,7 @@ impl SystemWidget { Self { nixos_build: None, agent_hash: None, + network_interfaces: Vec::new(), cpu_load_1min: None, cpu_load_5min: None, cpu_load_15min: None, @@ -164,6 +168,9 @@ impl Widget for SystemWidget { // Extract build version self.nixos_build = agent_data.build_version.clone(); + // Extract network interfaces + self.network_interfaces = agent_data.system.network.interfaces.clone(); + // Extract CPU data directly let cpu = &agent_data.system.cpu; self.cpu_load_1min = Some(cpu.load_1min); @@ -573,8 +580,46 @@ impl SystemWidget { lines } - /// Render system widget - pub fn render(&mut self, frame: &mut Frame, area: Rect, hostname: &str, config: Option<&crate::config::DashboardConfig>) { + /// Render network section for display + fn render_network(&self) -> Vec> { + let mut lines = Vec::new(); + + if self.network_interfaces.is_empty() { + return lines; + } + + for (i, interface) in self.network_interfaces.iter().enumerate() { + let is_last = i == self.network_interfaces.len() - 1; + let tree_symbol = if is_last { " └─ " } else { " ├─ " }; + + // Show interface name + let mut interface_text = format!("{}: ", interface.name); + + // Add IPv4 addresses + if !interface.ipv4_addresses.is_empty() { + interface_text.push_str(&interface.ipv4_addresses.join(", ")); + } + + // Add IPv6 addresses + if !interface.ipv6_addresses.is_empty() { + if !interface.ipv4_addresses.is_empty() { + interface_text.push_str(", "); + } + interface_text.push_str(&interface.ipv6_addresses.join(", ")); + } + + let mut spans = vec![ + Span::styled(tree_symbol, Typography::tree()), + ]; + spans.extend(StatusIcons::create_status_spans(Status::Ok, &interface_text)); + lines.push(Line::from(spans)); + } + + lines + } + + /// 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(); // NixOS section @@ -591,17 +636,16 @@ impl SystemWidget { lines.push(Line::from(vec![ Span::styled(format!("Agent: {}", agent_version_text), Typography::secondary()) ])); - - // Display detected connection IP - if let Some(config) = config { - if let Some(host_details) = config.hosts.get(hostname) { - let detected_ip = host_details.get_connection_ip(hostname); - lines.push(Line::from(vec![ - Span::styled(format!("IP: {}", detected_ip), Typography::secondary()) - ])); - } + + // Network section + if !self.network_interfaces.is_empty() { + lines.push(Line::from(vec![ + Span::styled("Network:", Typography::widget_title()) + ])); + + let network_lines = self.render_network(); + lines.extend(network_lines); } - // CPU section lines.push(Line::from(vec![ diff --git a/shared/Cargo.toml b/shared/Cargo.toml index f4f425d..42a9d79 100644 --- a/shared/Cargo.toml +++ b/shared/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cm-dashboard-shared" -version = "0.1.160" +version = "0.1.161" edition = "2021" [dependencies] diff --git a/shared/src/agent_data.rs b/shared/src/agent_data.rs index 58b560e..c93dc32 100644 --- a/shared/src/agent_data.rs +++ b/shared/src/agent_data.rs @@ -16,11 +16,26 @@ pub struct AgentData { /// System-level monitoring data #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SystemData { + pub network: NetworkData, pub cpu: CpuData, pub memory: MemoryData, pub storage: StorageData, } +/// Network interface monitoring data +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct NetworkData { + pub interfaces: Vec, +} + +/// Individual network interface data +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct NetworkInterfaceData { + pub name: String, + pub ipv4_addresses: Vec, + pub ipv6_addresses: Vec, +} + /// CPU monitoring data #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CpuData { @@ -171,6 +186,9 @@ impl AgentData { build_version: None, timestamp: chrono::Utc::now().timestamp() as u64, system: SystemData { + network: NetworkData { + interfaces: Vec::new(), + }, cpu: CpuData { load_1min: 0.0, load_5min: 0.0,