Compare commits

..

5 Commits

Author SHA1 Message Date
783d233319 Add CM Dashboard ASCII logo to rebuild tmux popup
All checks were successful
Build and Release / build-and-release (push) Successful in 2m9s
- Display branded CM Dashboard ASCII logo in green when rebuild starts
- Shows logo immediately when tmux popup opens for better UX
- Includes rebuild target hostname and visual separator
- Enhances rebuild process with professional branding
- Bump version to v0.1.39
2025-10-28 23:12:09 +01:00
6509a2b91a Make nginx site latency thresholds configurable and simplify status logic
All checks were successful
Build and Release / build-and-release (push) Successful in 4m25s
- Replace hardcoded 500ms/2000ms thresholds with configurable nginx_latency_critical_ms
- Simplify status logic to only OK or Critical (no Warning status)
- Add validation for nginx latency threshold configuration
- Re-enable nginx site collection with configurable thresholds
- Resolves issue where sites showed critical at 2000ms despite 30s timeout setting
- Bump version to v0.1.38
2025-10-28 21:24:34 +01:00
52f8c40b86 Fix title bar layout constraints to prevent text disappearing
All checks were successful
Build and Release / build-and-release (push) Successful in 2m12s
- Set fixed width (15 chars) for left side to prevent chunk collapse
- Resolves issue where "cm-dashboard" text would flash and disappear
- Ensures consistent visibility of title text in dynamic status bar
- Bump version to v0.1.37
2025-10-28 18:56:12 +01:00
a86b5ba8f9 Implement dynamic status-based title bar with infrastructure health indicator
All checks were successful
Build and Release / build-and-release (push) Successful in 1m15s
- Title bar background now dynamically changes based on worst-case status across all hosts
- Green: all OK, Yellow: warnings present, Red: critical issues, Blue: pending, Gray: unknown
- Provides immediate visual feedback of overall infrastructure health
- Added 1-character padding on both sides of title bar
- Maintains dark text for visibility against all status background colors
- Bump version to v0.1.36
2025-10-28 18:47:02 +01:00
1b964545be Fix storage display parsing and improve title bar UI
All checks were successful
Build and Release / build-and-release (push) Successful in 1m14s
- Fix disk drive name extraction for mount points with underscores (e.g., /mnt/steampool)
- Replace confusing "1" and "2" drive names with proper device names like "sda1", "sda2"
- Update title bar with blue background and dark text styling
- Right-align host list in title bar while keeping "cm-dashboard" on left
- Bump version to v0.1.35
2025-10-28 18:32:12 +01:00
9 changed files with 126 additions and 37 deletions

6
Cargo.lock generated
View File

@@ -270,7 +270,7 @@ checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d"
[[package]] [[package]]
name = "cm-dashboard" name = "cm-dashboard"
version = "0.1.33" version = "0.1.38"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"chrono", "chrono",
@@ -291,7 +291,7 @@ dependencies = [
[[package]] [[package]]
name = "cm-dashboard-agent" name = "cm-dashboard-agent"
version = "0.1.33" version = "0.1.38"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",
@@ -314,7 +314,7 @@ dependencies = [
[[package]] [[package]]
name = "cm-dashboard-shared" name = "cm-dashboard-shared"
version = "0.1.33" version = "0.1.38"
dependencies = [ dependencies = [
"chrono", "chrono",
"serde", "serde",

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "cm-dashboard-agent" name = "cm-dashboard-agent"
version = "0.1.34" version = "0.1.39"
edition = "2021" edition = "2021"
[dependencies] [dependencies]

View File

@@ -555,10 +555,8 @@ impl SystemdCollector {
for (site_name, url) in &sites { for (site_name, url) in &sites {
match self.check_site_latency(url) { match self.check_site_latency(url) {
Ok(latency_ms) => { Ok(latency_ms) => {
let status = if latency_ms < 500.0 { let status = if latency_ms < self.config.nginx_latency_critical_ms {
Status::Ok Status::Ok
} else if latency_ms < 2000.0 {
Status::Warning
} else { } else {
Status::Critical Status::Critical
}; };

View File

@@ -108,6 +108,7 @@ pub struct SystemdConfig {
pub nginx_check_interval_seconds: u64, pub nginx_check_interval_seconds: u64,
pub http_timeout_seconds: u64, pub http_timeout_seconds: u64,
pub http_connect_timeout_seconds: u64, pub http_connect_timeout_seconds: u64,
pub nginx_latency_critical_ms: f32,
} }

View File

@@ -83,6 +83,13 @@ pub fn validate_config(config: &AgentConfig) -> Result<()> {
} }
} }
// Validate systemd configuration
if config.collectors.systemd.enabled {
if config.collectors.systemd.nginx_latency_critical_ms <= 0.0 {
bail!("Nginx latency critical threshold must be positive");
}
}
// Validate SMTP configuration // Validate SMTP configuration
if config.notifications.enabled { if config.notifications.enabled {
if config.notifications.smtp_host.is_empty() { if config.notifications.smtp_host.is_empty() {

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "cm-dashboard" name = "cm-dashboard"
version = "0.1.34" version = "0.1.39"
edition = "2021" edition = "2021"
[dependencies] [dependencies]

View File

@@ -244,16 +244,31 @@ impl TuiApp {
KeyCode::Char('r') => { KeyCode::Char('r') => {
// System rebuild command - works on any panel for current host // System rebuild command - works on any panel for current host
if let Some(hostname) = self.current_host.clone() { if let Some(hostname) = self.current_host.clone() {
// Launch tmux popup with SSH using config values // Create command that shows CM Dashboard logo and then rebuilds
let ssh_command = format!( let logo_and_rebuild = format!(
"ssh -tt {}@{} 'bash -ic {}'", "echo ''; \
echo -e '\\033[1;32m _____ __ __ _____ _ _ _ \\033[0m'; \
echo -e '\\033[1;32m / ____| \\/ | | __ \\ | | | | | |\\033[0m'; \
echo -e '\\033[1;32m| | | \\ / | | | | | __ _ ___| |__ | |__ ___ __ _ _ __ __| |\\033[0m'; \
echo -e '\\033[1;32m| | | |\\/| | | | | |/ _` / __| \\'_ \\| \\'_ \\ / _ \\ / _` | \\'__/ _` |\\033[0m'; \
echo -e '\\033[1;32m| |____| | | | | |__| | (_| \\__ \\ | | | |_) | (_) | (_| | | | (_| |\\033[0m'; \
echo -e '\\033[1;32m \\_____|_| |_| |_____/ \\__,_|___/_| |_|_.__/ \\___/ \\__,_|_| \\__,_|\\033[0m'; \
echo ''; \
echo -e '\\033[1;33m NixOS System Rebuild\\033[0m'; \
echo -e '\\033[1;32m Target: {}\\033[0m'; \
echo ''; \
echo -e '\\033[1;90m────────────────────────────────────────────────────────────────────────────────\\033[0m'; \
echo ''; \
ssh -tt {}@{} 'bash -ic {}'",
hostname,
self.config.ssh.rebuild_user, self.config.ssh.rebuild_user,
hostname, hostname,
self.config.ssh.rebuild_alias self.config.ssh.rebuild_alias
); );
std::process::Command::new("tmux") std::process::Command::new("tmux")
.arg("display-popup") .arg("display-popup")
.arg(&ssh_command) .arg(&logo_and_rebuild)
.spawn() .spawn()
.ok(); // Ignore errors, tmux will handle them .ok(); // Ignore errors, tmux will handle them
} }
@@ -536,48 +551,87 @@ impl TuiApp {
if self.available_hosts.is_empty() { if self.available_hosts.is_empty() {
let title_text = "cm-dashboard • no hosts discovered"; let title_text = "cm-dashboard • no hosts discovered";
let title = Paragraph::new(title_text).style(Typography::title()); let title = Paragraph::new(title_text)
.style(Style::default().fg(Theme::background()).bg(Theme::status_color(Status::Unknown)));
frame.render_widget(title, area); frame.render_widget(title, area);
return; return;
} }
// Create spans for each host with status indicators // Calculate worst-case status across all hosts
let mut spans = vec![Span::styled("cm-dashboard • ", Typography::title())]; let mut worst_status = Status::Ok;
for host in &self.available_hosts {
let host_status = self.calculate_host_status(host, metric_store);
worst_status = Status::aggregate(&[worst_status, host_status]);
}
// Use the worst status color as background
let background_color = Theme::status_color(worst_status);
// Split the title bar into left and right sections
let chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Length(15), Constraint::Min(0)])
.split(area);
// Left side: "cm-dashboard" text
let left_span = Span::styled(
" cm-dashboard",
Style::default().fg(Theme::background()).bg(background_color)
);
let left_title = Paragraph::new(Line::from(vec![left_span]))
.style(Style::default().bg(background_color));
frame.render_widget(left_title, chunks[0]);
// Right side: hosts with status indicators
let mut host_spans = Vec::new();
for (i, host) in self.available_hosts.iter().enumerate() { for (i, host) in self.available_hosts.iter().enumerate() {
if i > 0 { if i > 0 {
spans.push(Span::styled(" ", Typography::title())); host_spans.push(Span::styled(
" ",
Style::default().fg(Theme::background()).bg(background_color)
));
} }
// Always show normal status icon based on metrics (no command status at host level) // Always show normal status icon based on metrics (no command status at host level)
let host_status = self.calculate_host_status(host, metric_store); let host_status = self.calculate_host_status(host, metric_store);
let (status_icon, status_color) = (StatusIcons::get_icon(host_status), Theme::status_color(host_status)); let status_icon = StatusIcons::get_icon(host_status);
// Add status icon // Add status icon with background color as foreground against status background
spans.push(Span::styled( host_spans.push(Span::styled(
format!("{} ", status_icon), format!("{} ", status_icon),
Style::default().fg(status_color), Style::default().fg(Theme::background()).bg(background_color),
)); ));
if Some(host) == self.current_host.as_ref() { if Some(host) == self.current_host.as_ref() {
// Selected host in bold bright white // Selected host in bold background color against status background
spans.push(Span::styled( host_spans.push(Span::styled(
host.clone(), host.clone(),
Typography::title().add_modifier(Modifier::BOLD), Style::default()
.fg(Theme::background())
.bg(background_color)
.add_modifier(Modifier::BOLD),
)); ));
} else { } else {
// Other hosts in normal style with status color // Other hosts in normal background color against status background
spans.push(Span::styled( host_spans.push(Span::styled(
host.clone(), host.clone(),
Style::default().fg(status_color), Style::default().fg(Theme::background()).bg(background_color),
)); ));
} }
} }
let title_line = Line::from(spans); // Add right padding
let title = Paragraph::new(vec![title_line]); host_spans.push(Span::styled(
" ",
Style::default().fg(Theme::background()).bg(background_color)
));
frame.render_widget(title, area); let host_line = Line::from(host_spans);
let host_title = Paragraph::new(vec![host_line])
.style(Style::default().bg(background_color))
.alignment(ratatui::layout::Alignment::Right);
frame.render_widget(host_title, chunks[1]);
} }
/// Calculate overall status for a host based on its metrics /// Calculate overall status for a host based on its metrics

View File

@@ -230,9 +230,30 @@ impl SystemWidget {
/// Extract pool name from disk metric name /// Extract pool name from disk metric name
fn extract_pool_name(&self, metric_name: &str) -> Option<String> { fn extract_pool_name(&self, metric_name: &str) -> Option<String> {
if let Some(captures) = metric_name.strip_prefix("disk_") { // Pattern: disk_{pool_name}_{drive_name}_{metric_type}
if let Some(pos) = captures.find('_') { // Since pool_name can contain underscores, work backwards from known metric suffixes
return Some(captures[..pos].to_string()); if metric_name.starts_with("disk_") {
// First try drive-specific metrics that have device names
if let Some(suffix_pos) = metric_name.rfind("_temperature")
.or_else(|| metric_name.rfind("_wear_percent"))
.or_else(|| metric_name.rfind("_health")) {
// Find the second-to-last underscore to get pool name
let before_suffix = &metric_name[..suffix_pos];
if let Some(drive_start) = before_suffix.rfind('_') {
return Some(metric_name[5..drive_start].to_string()); // Skip "disk_"
}
}
// For pool-level metrics (usage_percent, used_gb, total_gb), take everything before the metric suffix
else if let Some(suffix_pos) = metric_name.rfind("_usage_percent")
.or_else(|| metric_name.rfind("_used_gb"))
.or_else(|| metric_name.rfind("_total_gb")) {
return Some(metric_name[5..suffix_pos].to_string()); // Skip "disk_"
}
// Fallback to old behavior for unknown patterns
else if let Some(captures) = metric_name.strip_prefix("disk_") {
if let Some(pos) = captures.find('_') {
return Some(captures[..pos].to_string());
}
} }
} }
None None
@@ -240,10 +261,18 @@ impl SystemWidget {
/// Extract drive name from disk metric name /// Extract drive name from disk metric name
fn extract_drive_name(&self, metric_name: &str) -> Option<String> { fn extract_drive_name(&self, metric_name: &str) -> Option<String> {
// Pattern: disk_pool_drive_metric // Pattern: disk_{pool_name}_{drive_name}_{metric_type}
let parts: Vec<&str> = metric_name.split('_').collect(); // Since pool_name can contain underscores, work backwards from known metric suffixes
if parts.len() >= 3 && parts[0] == "disk" { if metric_name.starts_with("disk_") {
return Some(parts[2].to_string()); if let Some(suffix_pos) = metric_name.rfind("_temperature")
.or_else(|| metric_name.rfind("_wear_percent"))
.or_else(|| metric_name.rfind("_health")) {
// Find the second-to-last underscore to get the drive name
let before_suffix = &metric_name[..suffix_pos];
if let Some(drive_start) = before_suffix.rfind('_') {
return Some(before_suffix[drive_start + 1..].to_string());
}
}
} }
None None
} }

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "cm-dashboard-shared" name = "cm-dashboard-shared"
version = "0.1.34" version = "0.1.39"
edition = "2021" edition = "2021"
[dependencies] [dependencies]