Implement unified system widget with NixOS info, CPU, RAM, and Storage

- Create NixOS collector for version and active users detection
- Add SystemWidget combining all system information in TODO.md layout
- Replace separate CPU/Memory widgets with unified system display
- Add tree structure for storage with drive temperature/wear info
- Support NixOS version, active users, load averages, memory usage
- Follow exact decimal formatting from specification
This commit is contained in:
2025-10-23 14:01:14 +02:00
parent c99e0bd8ee
commit 39fc9cd22f
9 changed files with 697 additions and 63 deletions

View File

@@ -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;

View File

@@ -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<String>), Box<dyn std::error::Error>> {
// 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<Vec<String>, Box<dyn std::error::Error>> {
let output = Command::new("who").output()?;
if !output.status.success() {
return Err("who command failed".into());
}
let who_output = String::from_utf8_lossy(&output.stdout);
let mut users = std::collections::HashSet::new();
for line in who_output.lines() {
if let Some(username) = line.split_whitespace().next() {
if !username.is_empty() {
users.insert(username.to_string());
}
}
}
Ok(users.into_iter().collect())
}
}
#[async_trait]
impl Collector for NixOSCollector {
fn name(&self) -> &str {
"nixos"
}
async fn collect(&self, _status_tracker: &mut StatusTracker) -> Result<Vec<Metric>, 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)
}
}