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 only show name and status, no memory/CPU/disk/sandbox data data.add_row_with_sub_service( Some(WidgetStatus::new(status_level)), description, vec![ svc.name.clone(), "".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 { "0".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 } }