- Remove unreachable descriptions from failed nginx sites - Show complete site URLs instead of truncating at first dot - Implement service-specific disk quotas (docker: 4GB, immich: 4GB, others: 1-2GB) - Truncate process names to show only executable name without full path - Display only highest C-state instead of all C-states for cleaner output - Format system RAM as xxxMB/GB (totalGB) to match services format
202 lines
6.8 KiB
Rust
202 lines
6.8 KiB
Rust
use ratatui::layout::Rect;
|
|
use ratatui::Frame;
|
|
|
|
use crate::app::HostDisplayData;
|
|
use crate::data::metrics::ServiceStatus;
|
|
use crate::ui::widget::{render_placeholder, render_widget_data, status_level_from_agent_status, connection_status_message, WidgetData, WidgetStatus, StatusLevel};
|
|
use crate::app::ConnectionStatus;
|
|
|
|
pub fn render(frame: &mut Frame, host: Option<&HostDisplayData>, area: Rect) {
|
|
match host {
|
|
Some(data) => {
|
|
match (&data.connection_status, data.services.as_ref()) {
|
|
(ConnectionStatus::Connected, Some(metrics)) => {
|
|
render_metrics(frame, data, metrics, area);
|
|
}
|
|
(ConnectionStatus::Connected, None) => {
|
|
render_placeholder(
|
|
frame,
|
|
area,
|
|
"Services",
|
|
&format!("Host {} has no service metrics yet", data.name),
|
|
);
|
|
}
|
|
(status, _) => {
|
|
render_placeholder(
|
|
frame,
|
|
area,
|
|
"Services",
|
|
&format!("Host {}: {}", data.name, connection_status_message(status, &data.last_error)),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
None => render_placeholder(frame, area, "Services", "No hosts configured"),
|
|
}
|
|
}
|
|
|
|
fn render_metrics(
|
|
frame: &mut Frame,
|
|
_host: &HostDisplayData,
|
|
metrics: &crate::data::metrics::ServiceMetrics,
|
|
area: Rect,
|
|
) {
|
|
let summary = &metrics.summary;
|
|
let title = "Services".to_string();
|
|
|
|
// Use agent-calculated services status
|
|
let widget_status = status_level_from_agent_status(summary.services_status.as_ref());
|
|
|
|
let mut data = WidgetData::new(
|
|
title,
|
|
Some(WidgetStatus::new(widget_status)),
|
|
vec!["Service".to_string(), "RAM".to_string(), "CPU".to_string(), "Disk".to_string()]
|
|
);
|
|
|
|
|
|
if metrics.services.is_empty() {
|
|
data.add_row(
|
|
None,
|
|
vec![],
|
|
vec![
|
|
"No services reported".to_string(),
|
|
"".to_string(),
|
|
"".to_string(),
|
|
"".to_string(),
|
|
],
|
|
);
|
|
render_widget_data(frame, area, data);
|
|
return;
|
|
}
|
|
|
|
let mut services = metrics.services.clone();
|
|
services.sort_by(|a, b| {
|
|
// First, determine the primary service name for grouping
|
|
let primary_a = a.sub_service.as_ref().unwrap_or(&a.name);
|
|
let primary_b = b.sub_service.as_ref().unwrap_or(&b.name);
|
|
|
|
// Sort by primary service name first
|
|
match primary_a.cmp(primary_b) {
|
|
std::cmp::Ordering::Equal => {
|
|
// Same primary service, put parent service first, then sub-services alphabetically
|
|
match (a.sub_service.as_ref(), b.sub_service.as_ref()) {
|
|
(None, Some(_)) => std::cmp::Ordering::Less, // Parent comes before sub-services
|
|
(Some(_), None) => std::cmp::Ordering::Greater, // Sub-services come after parent
|
|
_ => a.name.cmp(&b.name), // Both same type, sort by name
|
|
}
|
|
}
|
|
other => other, // Different primary services, sort alphabetically
|
|
}
|
|
});
|
|
|
|
for svc in services {
|
|
let status_level = match svc.status {
|
|
ServiceStatus::Running => StatusLevel::Ok,
|
|
ServiceStatus::Degraded => StatusLevel::Warning,
|
|
ServiceStatus::Restarting => StatusLevel::Warning,
|
|
ServiceStatus::Stopped => StatusLevel::Error,
|
|
};
|
|
|
|
// Service row with optional description(s)
|
|
let description = if let Some(desc_vec) = &svc.description {
|
|
desc_vec.clone()
|
|
} else {
|
|
vec![]
|
|
};
|
|
|
|
if svc.sub_service.is_some() {
|
|
// Sub-services (nginx sites) only show name and status, no memory/CPU/disk data
|
|
// Add latency information for nginx sites if available
|
|
let service_name_with_latency = if let Some(parent) = &svc.sub_service {
|
|
if parent == "nginx" {
|
|
// Use full site name instead of truncating at first dot
|
|
let short_name = &svc.name;
|
|
|
|
match &svc.latency_ms {
|
|
Some(latency) if *latency >= 2000.0 => format!("{} → unreachable", short_name), // Timeout (2s+)
|
|
Some(latency) => format!("{} → {:.0}ms", short_name, latency),
|
|
None => format!("{} → unreachable", short_name), // Connection failed
|
|
}
|
|
} else {
|
|
svc.name.clone()
|
|
}
|
|
} else {
|
|
svc.name.clone()
|
|
};
|
|
|
|
data.add_row_with_sub_service(
|
|
Some(WidgetStatus::new(status_level)),
|
|
description,
|
|
vec![
|
|
service_name_with_latency,
|
|
"".to_string(),
|
|
"".to_string(),
|
|
"".to_string(),
|
|
],
|
|
svc.sub_service.clone(),
|
|
);
|
|
} 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, svc.disk_quota_gb),
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
render_widget_data(frame, area, data);
|
|
}
|
|
|
|
|
|
|
|
fn format_bytes(mb: f32) -> String {
|
|
if mb < 0.1 {
|
|
"<1MB".to_string()
|
|
} else if mb < 1.0 {
|
|
format!("{:.0}kB", mb * 1000.0)
|
|
} else if mb < 1000.0 {
|
|
format!("{:.0}MB", mb)
|
|
} else {
|
|
format!("{:.1}GB", mb / 1000.0)
|
|
}
|
|
}
|
|
|
|
fn format_memory_value(used: f32, quota: f32) -> String {
|
|
let used_value = format_bytes(used);
|
|
|
|
if quota > 0.05 {
|
|
let quota_gb = quota / 1000.0;
|
|
// Format quota without decimals and use GB
|
|
format!("{} ({}GB)", used_value, quota_gb as u32)
|
|
} else {
|
|
used_value
|
|
}
|
|
}
|
|
|
|
fn format_cpu_value(cpu_percent: f32) -> String {
|
|
if cpu_percent >= 0.1 {
|
|
format!("{:.1}%", cpu_percent)
|
|
} else {
|
|
"0.0%".to_string()
|
|
}
|
|
}
|
|
|
|
fn format_disk_value(used: f32, quota: f32) -> String {
|
|
let used_value = format_bytes(used * 1000.0); // Convert GB to MB for format_bytes
|
|
|
|
if quota > 0.05 {
|
|
// Format quota without decimals and use GB (round to nearest GB)
|
|
format!("{} ({}GB)", used_value, quota.round() as u32)
|
|
} else {
|
|
used_value
|
|
}
|
|
}
|
|
|
|
|