Compare commits
2 Commits
c48a105c28
...
fb6ee6d7ae
| Author | SHA1 | Date | |
|---|---|---|---|
| fb6ee6d7ae | |||
| a7e237e2ff |
@ -63,27 +63,32 @@ impl NixOSCollector {
|
|||||||
Ok("unknown".to_string())
|
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>> {
|
fn get_config_hash(&self) -> Result<String, Box<dyn std::error::Error>> {
|
||||||
// Get git hash from the cloned nixos-config directory
|
// Read the symlink target of /run/current-system to get nix store path
|
||||||
let config_path = "/var/lib/cm-dashboard/nixos-config";
|
let output = Command::new("readlink")
|
||||||
|
.arg("/run/current-system")
|
||||||
let output = Command::new("git")
|
|
||||||
.args(&["log", "-1", "--format=%h"])
|
|
||||||
.current_dir(config_path)
|
|
||||||
.output()?;
|
.output()?;
|
||||||
|
|
||||||
if !output.status.success() {
|
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() {
|
// Extract hash from nix store path
|
||||||
return Err("Empty git hash output".into());
|
// 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
|
/// Get currently active users
|
||||||
@ -178,7 +183,7 @@ impl Collector for NixOSCollector {
|
|||||||
name: "system_config_hash".to_string(),
|
name: "system_config_hash".to_string(),
|
||||||
value: MetricValue::String(hash),
|
value: MetricValue::String(hash),
|
||||||
unit: None,
|
unit: None,
|
||||||
description: Some("NixOS configuration git hash".to_string()),
|
description: Some("NixOS deployed configuration hash".to_string()),
|
||||||
status: Status::Ok,
|
status: Status::Ok,
|
||||||
timestamp,
|
timestamp,
|
||||||
});
|
});
|
||||||
@ -189,7 +194,7 @@ impl Collector for NixOSCollector {
|
|||||||
name: "system_config_hash".to_string(),
|
name: "system_config_hash".to_string(),
|
||||||
value: MetricValue::String("unknown".to_string()),
|
value: MetricValue::String("unknown".to_string()),
|
||||||
unit: None,
|
unit: None,
|
||||||
description: Some("Config hash (failed to detect)".to_string()),
|
description: Some("Deployed config hash (failed to detect)".to_string()),
|
||||||
status: Status::Unknown,
|
status: Status::Unknown,
|
||||||
timestamp,
|
timestamp,
|
||||||
});
|
});
|
||||||
|
|||||||
@ -7,7 +7,7 @@ use ratatui::{
|
|||||||
Frame,
|
Frame,
|
||||||
};
|
};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::time::Instant;
|
use std::time::{Duration, Instant};
|
||||||
use tracing::info;
|
use tracing::info;
|
||||||
|
|
||||||
pub mod theme;
|
pub mod theme;
|
||||||
@ -34,9 +34,9 @@ pub enum CommandStatus {
|
|||||||
/// Command is executing
|
/// Command is executing
|
||||||
InProgress { command_type: CommandType, target: String, start_time: std::time::Instant },
|
InProgress { command_type: CommandType, target: String, start_time: std::time::Instant },
|
||||||
/// Command completed successfully
|
/// 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
|
/// 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
|
/// Types of commands for status tracking
|
||||||
@ -156,6 +156,12 @@ impl TuiApp {
|
|||||||
|
|
||||||
/// Update widgets with metrics from store (only for current host)
|
/// Update widgets with metrics from store (only for current host)
|
||||||
pub fn update_metrics(&mut self, metric_store: &MetricStore) {
|
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() {
|
if let Some(hostname) = self.current_host.clone() {
|
||||||
// Only update widgets if we have metrics for this host
|
// Only update widgets if we have metrics for this host
|
||||||
let all_metrics = metric_store.get_metrics_for_host(&hostname);
|
let all_metrics = metric_store.get_metrics_for_host(&hostname);
|
||||||
@ -491,10 +497,8 @@ impl TuiApp {
|
|||||||
command_type: command_type.clone(),
|
command_type: command_type.clone(),
|
||||||
target: target.clone(),
|
target: target.clone(),
|
||||||
duration,
|
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(),
|
command_type: command_type.clone(),
|
||||||
target: target.clone(),
|
target: target.clone(),
|
||||||
error,
|
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
|
/// Scroll the focused panel up or down
|
||||||
pub fn scroll_focused_panel(&mut self, direction: i32) {
|
pub fn scroll_focused_panel(&mut self, direction: i32) {
|
||||||
if let Some(hostname) = self.current_host.clone() {
|
if let Some(hostname) = self.current_host.clone() {
|
||||||
@ -682,15 +748,26 @@ impl TuiApp {
|
|||||||
spans.push(Span::styled(" ", Typography::title()));
|
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) {
|
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 {
|
match &host_widgets.command_status {
|
||||||
// Show blue circular arrow during rebuild
|
Some(CommandStatus::InProgress { command_type: CommandType::SystemRebuild, .. }) => {
|
||||||
("↻", Theme::highlight())
|
// Show blue circular arrow during rebuild
|
||||||
} else {
|
("↻", Theme::highlight())
|
||||||
// Normal status icon based on metrics
|
}
|
||||||
let host_status = self.calculate_host_status(host, metric_store);
|
Some(CommandStatus::Success { command_type: CommandType::SystemRebuild, .. }) => {
|
||||||
(StatusIcons::get_icon(host_status), Theme::status_color(host_status))
|
// 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 {
|
} else {
|
||||||
// No host widgets yet, use normal status
|
// No host widgets yet, use normal status
|
||||||
|
|||||||
@ -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
|
/// Get mount point for a pool name
|
||||||
fn get_mount_point_for_pool(&self, pool_name: &str) -> String {
|
fn get_mount_point_for_pool(&self, pool_name: &str) -> String {
|
||||||
match pool_name {
|
match pool_name {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user