Christoffer Martinsson bab387c74d Refactor services widget with unified system metrics display
- Rename alerts widget to hosts widget for clarity
- Add sub_service field to ServiceInfo for display differentiation
- Integrate system metrics (CPU load, memory, temperature, disk) as service rows
- Convert nginx sites to individual sub-service rows with tree structure
- Remove nginx site checkmarks - status now shown via row indicators
- Update dashboard layout to display system and service data together
- Maintain description lines for connection counts and service details

Services widget now shows:
- System metrics as regular service rows with status
- Nginx sites as sub-services with ├─/└─ tree formatting
- Regular services with full resource data and descriptions
- Unified status indication across all row types
2025-10-13 08:10:38 +02:00

297 lines
9.1 KiB
Rust

use chrono::{DateTime, Utc};
use ratatui::layout::Rect;
use ratatui::Frame;
use crate::app::{HostDisplayData, ConnectionStatus};
// Removed: evaluate_performance and PerfSeverity no longer needed
use crate::ui::widget::{render_widget_data, WidgetData, WidgetStatus, StatusLevel};
pub fn render(frame: &mut Frame, hosts: &[HostDisplayData], area: Rect) {
let (severity, _ok_count, _warn_count, _fail_count) = classify_hosts(hosts);
let title = "Hosts".to_string();
let widget_status = match severity {
HostSeverity::Critical => StatusLevel::Error,
HostSeverity::Warning => StatusLevel::Warning,
HostSeverity::Healthy => StatusLevel::Ok,
HostSeverity::Unknown => StatusLevel::Unknown,
};
let mut data = WidgetData::new(
title,
Some(WidgetStatus::new(widget_status)),
vec!["Host".to_string(), "Status".to_string(), "Timestamp".to_string()]
);
if hosts.is_empty() {
data.add_row(
None,
vec![],
vec![
"No hosts configured".to_string(),
"".to_string(),
"".to_string(),
],
);
} else {
for host in hosts {
let (status_text, severity, _emphasize) = host_status(host);
let status_level = match severity {
HostSeverity::Critical => StatusLevel::Error,
HostSeverity::Warning => StatusLevel::Warning,
HostSeverity::Healthy => StatusLevel::Ok,
HostSeverity::Unknown => StatusLevel::Unknown,
};
let update = latest_timestamp(host)
.map(|ts| ts.format("%Y-%m-%d %H:%M:%S").to_string())
.unwrap_or_else(|| "".to_string());
data.add_row(
Some(WidgetStatus::new(status_level)),
vec![],
vec![
host.name.clone(),
status_text,
update,
],
);
}
}
render_widget_data(frame, area, data);
}
#[derive(Copy, Clone, Eq, PartialEq)]
enum HostSeverity {
Healthy,
Warning,
Critical,
Unknown,
}
fn classify_hosts(hosts: &[HostDisplayData]) -> (HostSeverity, usize, usize, usize) {
let mut ok = 0;
let mut warn = 0;
let mut fail = 0;
for host in hosts {
let severity = host_severity(host);
match severity {
HostSeverity::Healthy => ok += 1,
HostSeverity::Warning => warn += 1,
HostSeverity::Critical => fail += 1,
HostSeverity::Unknown => warn += 1,
}
}
let highest = if fail > 0 {
HostSeverity::Critical
} else if warn > 0 {
HostSeverity::Warning
} else if ok > 0 {
HostSeverity::Healthy
} else {
HostSeverity::Unknown
};
(highest, ok, warn, fail)
}
fn host_severity(host: &HostDisplayData) -> HostSeverity {
// Check connection status first
match host.connection_status {
ConnectionStatus::Error => return HostSeverity::Critical,
ConnectionStatus::Timeout => return HostSeverity::Warning,
ConnectionStatus::Unknown => return HostSeverity::Unknown,
ConnectionStatus::Connected => {}, // Continue with other checks
}
if host.last_error.is_some() {
return HostSeverity::Critical;
}
if let Some(smart) = host.smart.as_ref() {
if smart.summary.critical > 0 {
return HostSeverity::Critical;
}
if smart.summary.warning > 0 || !smart.issues.is_empty() {
return HostSeverity::Warning;
}
}
if let Some(services) = host.services.as_ref() {
if services.summary.failed > 0 {
return HostSeverity::Critical;
}
if services.summary.degraded > 0 {
return HostSeverity::Warning;
}
// TODO: Update to use agent-provided system statuses instead of evaluate_performance
// let (perf_severity, _) = evaluate_performance(&services.summary);
// match perf_severity {
// PerfSeverity::Critical => return HostSeverity::Critical,
// PerfSeverity::Warning => return HostSeverity::Warning,
// PerfSeverity::Ok => {}
// }
}
if let Some(backup) = host.backup.as_ref() {
match backup.overall_status.as_str() {
"critical" => return HostSeverity::Critical,
"warning" => return HostSeverity::Warning,
_ => {}
}
}
if host.smart.is_none() && host.services.is_none() && host.backup.is_none() {
HostSeverity::Unknown
} else {
HostSeverity::Healthy
}
}
fn host_status(host: &HostDisplayData) -> (String, HostSeverity, bool) {
// Check connection status first
match host.connection_status {
ConnectionStatus::Error => {
let msg = if let Some(error) = &host.last_error {
format!("Connection error: {}", error)
} else {
"Connection error".to_string()
};
return (msg, HostSeverity::Critical, true);
},
ConnectionStatus::Timeout => {
let msg = if let Some(error) = &host.last_error {
format!("Keep-alive timeout: {}", error)
} else {
"Keep-alive timeout".to_string()
};
return (msg, HostSeverity::Warning, true);
},
ConnectionStatus::Unknown => {
return ("No data received".to_string(), HostSeverity::Unknown, true);
},
ConnectionStatus::Connected => {}, // Continue with other checks
}
if let Some(error) = &host.last_error {
return (format!("error: {}", error), HostSeverity::Critical, true);
}
if let Some(smart) = host.smart.as_ref() {
if smart.summary.critical > 0 {
return (
"critical: SMART critical".to_string(),
HostSeverity::Critical,
true,
);
}
if let Some(issue) = smart.issues.first() {
return (format!("warning: {}", issue), HostSeverity::Warning, true);
}
}
if let Some(services) = host.services.as_ref() {
if services.summary.failed > 0 {
return (
format!("critical: {} failed svc", services.summary.failed),
HostSeverity::Critical,
true,
);
}
if services.summary.degraded > 0 {
return (
format!("warning: {} degraded svc", services.summary.degraded),
HostSeverity::Warning,
true,
);
}
// TODO: Update to use agent-provided system statuses instead of evaluate_performance
// let (perf_severity, reason) = evaluate_performance(&services.summary);
// if let Some(reason_text) = reason {
// match perf_severity {
// PerfSeverity::Critical => {
// return (
// format!("critical: {}", reason_text),
// HostSeverity::Critical,
// true,
// );
// }
// PerfSeverity::Warning => {
// return (
// format!("warning: {}", reason_text),
// HostSeverity::Warning,
// true,
// );
// }
// PerfSeverity::Ok => {}
// }
// }
}
if let Some(backup) = host.backup.as_ref() {
match backup.overall_status.as_str() {
"critical" => {
return (
"critical: backup failed".to_string(),
HostSeverity::Critical,
true,
);
}
"warning" => {
return (
"warning: backup warning".to_string(),
HostSeverity::Warning,
true,
);
}
_ => {}
}
}
if host.smart.is_none() && host.services.is_none() && host.backup.is_none() {
let status = if host.last_success.is_none() {
"pending: awaiting metrics"
} else {
"pending: no recent data"
};
return (status.to_string(), HostSeverity::Warning, false);
}
("ok".to_string(), HostSeverity::Healthy, false)
}
fn latest_timestamp(host: &HostDisplayData) -> Option<DateTime<Utc>> {
let mut latest = host.last_success;
if let Some(smart) = host.smart.as_ref() {
latest = Some(match latest {
Some(current) => current.max(smart.timestamp),
None => smart.timestamp,
});
}
if let Some(services) = host.services.as_ref() {
latest = Some(match latest {
Some(current) => current.max(services.timestamp),
None => services.timestamp,
});
}
if let Some(backup) = host.backup.as_ref() {
latest = Some(match latest {
Some(current) => current.max(backup.timestamp),
None => backup.timestamp,
});
}
latest
}