- 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
297 lines
9.1 KiB
Rust
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
|
|
}
|
|
|