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
This commit is contained in:
2025-10-13 08:10:38 +02:00
parent c68ccf023e
commit bab387c74d
7 changed files with 419 additions and 89 deletions

View File

@@ -116,6 +116,8 @@ pub struct ServiceInfo {
pub disk_used_gb: f32,
#[serde(default)]
pub description: Option<Vec<String>>,
#[serde(default)]
pub sub_service: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]

View File

@@ -6,7 +6,7 @@ use ratatui::Frame;
use crate::app::App;
use super::{alerts, backup, services, storage, system};
use super::{hosts, backup, services, storage, system};
pub fn render(frame: &mut Frame, app: &App) {
let host_summaries = app.host_display_data();
@@ -56,7 +56,7 @@ pub fn render(frame: &mut Frame, app: &App) {
backup::render(frame, primary_host.as_ref(), left_widgets[2]);
services::render(frame, primary_host.as_ref(), services_area);
alerts::render(frame, &host_summaries, left_side[1]);
hosts::render(frame, &host_summaries, left_side[1]);
if app.help_visible() {
render_help(frame, size);

View File

@@ -9,13 +9,13 @@ use crate::ui::widget::{render_widget_data, WidgetData, WidgetStatus, StatusLeve
pub fn render(frame: &mut Frame, hosts: &[HostDisplayData], area: Rect) {
let (severity, _ok_count, _warn_count, _fail_count) = classify_hosts(hosts);
let title = "Alerts".to_string();
let title = "Hosts".to_string();
let widget_status = match severity {
AlertSeverity::Critical => StatusLevel::Error,
AlertSeverity::Warning => StatusLevel::Warning,
AlertSeverity::Healthy => StatusLevel::Ok,
AlertSeverity::Unknown => StatusLevel::Unknown,
HostSeverity::Critical => StatusLevel::Error,
HostSeverity::Warning => StatusLevel::Warning,
HostSeverity::Healthy => StatusLevel::Ok,
HostSeverity::Unknown => StatusLevel::Unknown,
};
let mut data = WidgetData::new(
@@ -38,10 +38,10 @@ pub fn render(frame: &mut Frame, hosts: &[HostDisplayData], area: Rect) {
for host in hosts {
let (status_text, severity, _emphasize) = host_status(host);
let status_level = match severity {
AlertSeverity::Critical => StatusLevel::Error,
AlertSeverity::Warning => StatusLevel::Warning,
AlertSeverity::Healthy => StatusLevel::Ok,
AlertSeverity::Unknown => StatusLevel::Unknown,
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())
@@ -63,14 +63,14 @@ pub fn render(frame: &mut Frame, hosts: &[HostDisplayData], area: Rect) {
}
#[derive(Copy, Clone, Eq, PartialEq)]
enum AlertSeverity {
enum HostSeverity {
Healthy,
Warning,
Critical,
Unknown,
}
fn classify_hosts(hosts: &[HostDisplayData]) -> (AlertSeverity, usize, usize, usize) {
fn classify_hosts(hosts: &[HostDisplayData]) -> (HostSeverity, usize, usize, usize) {
let mut ok = 0;
let mut warn = 0;
let mut fail = 0;
@@ -78,81 +78,81 @@ fn classify_hosts(hosts: &[HostDisplayData]) -> (AlertSeverity, usize, usize, us
for host in hosts {
let severity = host_severity(host);
match severity {
AlertSeverity::Healthy => ok += 1,
AlertSeverity::Warning => warn += 1,
AlertSeverity::Critical => fail += 1,
AlertSeverity::Unknown => warn += 1,
HostSeverity::Healthy => ok += 1,
HostSeverity::Warning => warn += 1,
HostSeverity::Critical => fail += 1,
HostSeverity::Unknown => warn += 1,
}
}
let highest = if fail > 0 {
AlertSeverity::Critical
HostSeverity::Critical
} else if warn > 0 {
AlertSeverity::Warning
HostSeverity::Warning
} else if ok > 0 {
AlertSeverity::Healthy
HostSeverity::Healthy
} else {
AlertSeverity::Unknown
HostSeverity::Unknown
};
(highest, ok, warn, fail)
}
fn host_severity(host: &HostDisplayData) -> AlertSeverity {
fn host_severity(host: &HostDisplayData) -> HostSeverity {
// Check connection status first
match host.connection_status {
ConnectionStatus::Error => return AlertSeverity::Critical,
ConnectionStatus::Timeout => return AlertSeverity::Warning,
ConnectionStatus::Unknown => return AlertSeverity::Unknown,
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 AlertSeverity::Critical;
return HostSeverity::Critical;
}
if let Some(smart) = host.smart.as_ref() {
if smart.summary.critical > 0 {
return AlertSeverity::Critical;
return HostSeverity::Critical;
}
if smart.summary.warning > 0 || !smart.issues.is_empty() {
return AlertSeverity::Warning;
return HostSeverity::Warning;
}
}
if let Some(services) = host.services.as_ref() {
if services.summary.failed > 0 {
return AlertSeverity::Critical;
return HostSeverity::Critical;
}
if services.summary.degraded > 0 {
return AlertSeverity::Warning;
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 AlertSeverity::Critical,
// PerfSeverity::Warning => return AlertSeverity::Warning,
// 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 AlertSeverity::Critical,
"warning" => return AlertSeverity::Warning,
"critical" => return HostSeverity::Critical,
"warning" => return HostSeverity::Warning,
_ => {}
}
}
if host.smart.is_none() && host.services.is_none() && host.backup.is_none() {
AlertSeverity::Unknown
HostSeverity::Unknown
} else {
AlertSeverity::Healthy
HostSeverity::Healthy
}
}
fn host_status(host: &HostDisplayData) -> (String, AlertSeverity, bool) {
fn host_status(host: &HostDisplayData) -> (String, HostSeverity, bool) {
// Check connection status first
match host.connection_status {
ConnectionStatus::Error => {
@@ -161,7 +161,7 @@ fn host_status(host: &HostDisplayData) -> (String, AlertSeverity, bool) {
} else {
"Connection error".to_string()
};
return (msg, AlertSeverity::Critical, true);
return (msg, HostSeverity::Critical, true);
},
ConnectionStatus::Timeout => {
let msg = if let Some(error) = &host.last_error {
@@ -169,28 +169,28 @@ fn host_status(host: &HostDisplayData) -> (String, AlertSeverity, bool) {
} else {
"Keep-alive timeout".to_string()
};
return (msg, AlertSeverity::Warning, true);
return (msg, HostSeverity::Warning, true);
},
ConnectionStatus::Unknown => {
return ("No data received".to_string(), AlertSeverity::Unknown, true);
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), AlertSeverity::Critical, true);
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(),
AlertSeverity::Critical,
HostSeverity::Critical,
true,
);
}
if let Some(issue) = smart.issues.first() {
return (format!("warning: {}", issue), AlertSeverity::Warning, true);
return (format!("warning: {}", issue), HostSeverity::Warning, true);
}
}
@@ -198,14 +198,14 @@ fn host_status(host: &HostDisplayData) -> (String, AlertSeverity, bool) {
if services.summary.failed > 0 {
return (
format!("critical: {} failed svc", services.summary.failed),
AlertSeverity::Critical,
HostSeverity::Critical,
true,
);
}
if services.summary.degraded > 0 {
return (
format!("warning: {} degraded svc", services.summary.degraded),
AlertSeverity::Warning,
HostSeverity::Warning,
true,
);
}
@@ -217,14 +217,14 @@ fn host_status(host: &HostDisplayData) -> (String, AlertSeverity, bool) {
// PerfSeverity::Critical => {
// return (
// format!("critical: {}", reason_text),
// AlertSeverity::Critical,
// HostSeverity::Critical,
// true,
// );
// }
// PerfSeverity::Warning => {
// return (
// format!("warning: {}", reason_text),
// AlertSeverity::Warning,
// HostSeverity::Warning,
// true,
// );
// }
@@ -238,14 +238,14 @@ fn host_status(host: &HostDisplayData) -> (String, AlertSeverity, bool) {
"critical" => {
return (
"critical: backup failed".to_string(),
AlertSeverity::Critical,
HostSeverity::Critical,
true,
);
}
"warning" => {
return (
"warning: backup warning".to_string(),
AlertSeverity::Warning,
HostSeverity::Warning,
true,
);
}
@@ -260,10 +260,10 @@ fn host_status(host: &HostDisplayData) -> (String, AlertSeverity, bool) {
"pending: no recent data"
};
return (status.to_string(), AlertSeverity::Warning, false);
return (status.to_string(), HostSeverity::Warning, false);
}
("ok".to_string(), AlertSeverity::Healthy, false)
("ok".to_string(), HostSeverity::Healthy, false)
}

View File

@@ -1,4 +1,4 @@
pub mod alerts;
pub mod hosts;
pub mod backup;
pub mod dashboard;
pub mod services;

View File

@@ -91,16 +91,31 @@ fn render_metrics(
vec![]
};
data.add_row(
Some(WidgetStatus::new(status_level)),
description,
vec![
svc.name.clone(),
format_memory_value(svc.memory_used_mb, svc.memory_quota_mb),
format_cpu_value(svc.cpu_percent),
format_disk_value(svc.disk_used_gb),
],
);
if svc.sub_service {
// Sub-services only show name and status, no memory/CPU/disk data
data.add_row(
Some(WidgetStatus::new(status_level)),
description,
vec![
svc.name.clone(),
"".to_string(),
"".to_string(),
"".to_string(),
],
);
} else {
// Regular services show all columns
data.add_row(
Some(WidgetStatus::new(status_level)),
description,
vec![
svc.name.clone(),
format_memory_value(svc.memory_used_mb, svc.memory_quota_mb),
format_cpu_value(svc.cpu_percent),
format_disk_value(svc.disk_used_gb),
],
);
}
}
render_widget_data(frame, area, data);