use ratatui::layout::Rect; use ratatui::style::Color; use ratatui::Frame; use crate::app::HostDisplayData; use crate::data::metrics::{ServiceStatus, ServiceSummary}; use crate::ui::widget::{render_placeholder, render_widget_data, WidgetData, WidgetStatus, WidgetValue, StatusLevel}; pub fn render(frame: &mut Frame, host: Option<&HostDisplayData>, area: Rect) { match host { Some(data) => { if let Some(metrics) = data.services.as_ref() { render_metrics(frame, data, metrics, area); } else { render_placeholder( frame, area, "Services", &format!("Host {} has no service metrics yet", data.name), ); } } 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 color = summary_color(summary); let title = format!( "Services • ok:{} warn:{} fail:{}", summary.healthy, summary.degraded, summary.failed ); let widget_status = if summary.failed > 0 { StatusLevel::Error } else if summary.degraded > 0 { StatusLevel::Warning } else { StatusLevel::Ok }; let mut data = WidgetData::new( title, Some(WidgetStatus::new(widget_status)), vec!["Service".to_string(), "Memory".to_string(), "Disk".to_string(), "Description".to_string()] ); if metrics.services.is_empty() { data.add_row( None, "", vec![ WidgetValue::new("No services reported"), WidgetValue::new(""), WidgetValue::new(""), WidgetValue::new(""), ], ); render_widget_data(frame, area, data); return; } let mut services = metrics.services.clone(); services.sort_by(|a, b| { status_weight(&a.status) .cmp(&status_weight(&b.status)) .then_with(|| a.name.cmp(&b.name)) }); 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, }; data.add_row( Some(WidgetStatus::new(status_level)), "", vec![ WidgetValue::new(svc.name.clone()), WidgetValue::new(format_memory_value(svc.memory_used_mb, svc.memory_quota_mb)), WidgetValue::new(format_disk_value(svc.disk_used_gb)), WidgetValue::new(svc.description.as_deref().unwrap_or("—")), ], ); } render_widget_data(frame, area, data); } fn status_weight(status: &ServiceStatus) -> i32 { match status { ServiceStatus::Stopped => 0, ServiceStatus::Degraded => 1, ServiceStatus::Restarting => 2, ServiceStatus::Running => 3, } } fn status_symbol(status: &ServiceStatus) -> (&'static str, Color) { match status { ServiceStatus::Running => ("✔", Color::Green), ServiceStatus::Degraded => ("!", Color::Yellow), ServiceStatus::Restarting => ("↻", Color::Yellow), ServiceStatus::Stopped => ("✖", Color::Red), } } fn summary_color(summary: &ServiceSummary) -> Color { if summary.failed > 0 { Color::Red } else if summary.degraded > 0 { Color::Yellow } else { Color::Green } } fn format_memory_value(used: f32, quota: f32) -> String { if quota > 0.05 { format!("{:.1}/{:.1} MiB", used, quota) } else if used > 0.05 { format!("{:.1} MiB", used) } else { "—".to_string() } } fn format_disk_value(used: f32) -> String { if used >= 1.0 { format!("{:.1} GiB", used) } else if used >= 0.001 { // 1 MB or more format!("{:.0} MiB", used * 1024.0) } else if used > 0.0 { format!("<1 MiB") } else { "—".to_string() } }