Compare commits

...

2 Commits

Author SHA1 Message Date
fb6ee6d7ae Fix config hash to show actual deployed nix store hash
- Replace git commit hash with nix store hash extraction
- Read from /run/current-system symlink target
- Extract first 8 characters of nix store hash: d8ivwiar
- Shows actual deployed configuration, not just source
- Enables proper rebuild completion detection
- Accurate deployment verification
2025-10-25 12:22:17 +02:00
a7e237e2ff Fix rebuild indicator with proper timeout and completion detection
- Add automatic timeout mechanism (5 minutes for rebuilds, 30 seconds for services)
- Implement agent hash change detection for rebuild completion
- Add visual feedback states: blue ↻ (in progress), green ✓ (success), red ✗ (failed)
- Clear status automatically after timeout or completion
- Fix command status lifecycle management
2025-10-25 11:06:36 +02:00
3 changed files with 118 additions and 31 deletions

View File

@ -63,27 +63,32 @@ impl NixOSCollector {
Ok("unknown".to_string())
}
/// Get configuration hash from cloned nixos-config git repository
/// Get configuration hash from deployed nix store system
fn get_config_hash(&self) -> Result<String, Box<dyn std::error::Error>> {
// Get git hash from the cloned nixos-config directory
let config_path = "/var/lib/cm-dashboard/nixos-config";
let output = Command::new("git")
.args(&["log", "-1", "--format=%h"])
.current_dir(config_path)
// Read the symlink target of /run/current-system to get nix store path
let output = Command::new("readlink")
.arg("/run/current-system")
.output()?;
if !output.status.success() {
return Err("git log command failed".into());
return Err("readlink command failed".into());
}
let hash = String::from_utf8_lossy(&output.stdout).trim().to_string();
let binding = String::from_utf8_lossy(&output.stdout);
let store_path = binding.trim();
if hash.is_empty() {
return Err("Empty git hash output".into());
// Extract hash from nix store path
// Format: /nix/store/HASH-nixos-system-HOSTNAME-VERSION
if let Some(hash_part) = store_path.strip_prefix("/nix/store/") {
if let Some(hash) = hash_part.split('-').next() {
if hash.len() >= 8 {
// Return first 8 characters of nix store hash
return Ok(hash[..8].to_string());
}
}
}
Ok(hash)
Err("Could not extract hash from nix store path".into())
}
/// Get currently active users
@ -178,7 +183,7 @@ impl Collector for NixOSCollector {
name: "system_config_hash".to_string(),
value: MetricValue::String(hash),
unit: None,
description: Some("NixOS configuration git hash".to_string()),
description: Some("NixOS deployed configuration hash".to_string()),
status: Status::Ok,
timestamp,
});
@ -189,7 +194,7 @@ impl Collector for NixOSCollector {
name: "system_config_hash".to_string(),
value: MetricValue::String("unknown".to_string()),
unit: None,
description: Some("Config hash (failed to detect)".to_string()),
description: Some("Deployed config hash (failed to detect)".to_string()),
status: Status::Unknown,
timestamp,
});

View File

@ -7,7 +7,7 @@ use ratatui::{
Frame,
};
use std::collections::HashMap;
use std::time::Instant;
use std::time::{Duration, Instant};
use tracing::info;
pub mod theme;
@ -34,9 +34,9 @@ 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 },
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 { command_type: CommandType, target: String, error: String, failed_at: std::time::Instant },
}
/// Types of commands for status tracking
@ -156,6 +156,12 @@ impl TuiApp {
/// Update widgets with metrics from store (only for current host)
pub fn update_metrics(&mut self, metric_store: &MetricStore) {
// Check for command timeouts first
self.check_command_timeouts();
// Check for rebuild completion by agent hash change
self.check_rebuild_completion(metric_store);
if let Some(hostname) = self.current_host.clone() {
// Only update widgets if we have metrics for this host
let all_metrics = metric_store.get_metrics_for_host(&hostname);
@ -491,10 +497,8 @@ impl TuiApp {
command_type: command_type.clone(),
target: target.clone(),
duration,
completed_at: Instant::now(),
});
// Clear success status after 3 seconds
// TODO: Implement timer to clear this
}
}
}
@ -507,14 +511,76 @@ impl TuiApp {
command_type: command_type.clone(),
target: target.clone(),
error,
failed_at: Instant::now(),
});
// Clear error status after 5 seconds
// TODO: Implement timer to clear this
}
}
}
/// Check for command timeouts and automatically clear them
pub fn check_command_timeouts(&mut self) {
let now = Instant::now();
let mut hosts_to_clear = Vec::new();
for (hostname, host_widgets) in &self.host_widgets {
if let Some(CommandStatus::InProgress { command_type, start_time, .. }) = &host_widgets.command_status {
let timeout_duration = match command_type {
CommandType::SystemRebuild => Duration::from_secs(300), // 5 minutes for rebuilds
_ => Duration::from_secs(30), // 30 seconds for service commands
};
if now.duration_since(*start_time) > timeout_duration {
hosts_to_clear.push(hostname.clone());
}
}
// Also clear success/failed status after display time
else if let Some(CommandStatus::Success { completed_at, .. }) = &host_widgets.command_status {
if now.duration_since(*completed_at) > Duration::from_secs(3) {
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
for hostname in hosts_to_clear {
if let Some(host_widgets) = self.host_widgets.get_mut(&hostname) {
host_widgets.command_status = None;
}
}
}
/// Check for rebuild completion by detecting agent hash changes
pub fn check_rebuild_completion(&mut self, metric_store: &MetricStore) {
let mut hosts_to_complete = Vec::new();
for (hostname, host_widgets) in &self.host_widgets {
if let Some(CommandStatus::InProgress { command_type: CommandType::SystemRebuild, .. }) = &host_widgets.command_status {
// Check if agent hash has changed (indicating successful rebuild)
if let Some(agent_hash_metric) = metric_store.get_metric(hostname, "system_agent_hash") {
if let cm_dashboard_shared::MetricValue::String(current_hash) = &agent_hash_metric.value {
// Compare with stored hash (if we have one)
if let Some(stored_hash) = host_widgets.system_widget.get_agent_hash() {
if current_hash != stored_hash {
// Agent hash changed - rebuild completed successfully
hosts_to_complete.push(hostname.clone());
}
}
}
}
}
}
// Mark rebuilds as completed
for hostname in hosts_to_complete {
self.complete_command(&hostname);
}
}
/// Scroll the focused panel up or down
pub fn scroll_focused_panel(&mut self, direction: i32) {
if let Some(hostname) = self.current_host.clone() {
@ -682,15 +748,26 @@ impl TuiApp {
spans.push(Span::styled(" ", Typography::title()));
}
// Check if this host has a SystemRebuild command in progress
// Check if this host has a command status that affects the icon
let (status_icon, status_color) = if let Some(host_widgets) = self.host_widgets.get(host) {
if let Some(CommandStatus::InProgress { command_type: CommandType::SystemRebuild, .. }) = &host_widgets.command_status {
// Show blue circular arrow during rebuild
("", Theme::highlight())
} else {
// Normal status icon based on metrics
let host_status = self.calculate_host_status(host, metric_store);
(StatusIcons::get_icon(host_status), Theme::status_color(host_status))
match &host_widgets.command_status {
Some(CommandStatus::InProgress { command_type: CommandType::SystemRebuild, .. }) => {
// Show blue circular arrow during rebuild
("", Theme::highlight())
}
Some(CommandStatus::Success { command_type: CommandType::SystemRebuild, .. }) => {
// 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);
(StatusIcons::get_icon(host_status), Theme::status_color(host_status))
}
}
} else {
// No host widgets yet, use normal status

View File

@ -128,6 +128,11 @@ impl SystemWidget {
}
}
/// Get the current agent hash for rebuild completion detection
pub fn get_agent_hash(&self) -> Option<&String> {
self.agent_hash.as_ref()
}
/// Get mount point for a pool name
fn get_mount_point_for_pool(&self, pool_name: &str) -> String {
match pool_name {