diff --git a/agent/src/collectors/systemd.rs b/agent/src/collectors/systemd.rs index 8acf9c8..dd23ca0 100644 --- a/agent/src/collectors/systemd.rs +++ b/agent/src/collectors/systemd.rs @@ -739,9 +739,8 @@ impl SystemdCollector { while i < lines.len() { let line = lines[i].trim(); if line.starts_with("server") && line.contains("{") { - if let Some(proxy_url) = self.parse_server_block(&lines, &mut i) { - let site_name = proxy_url.replace("http://", "").replace("https://", ""); - sites.push((site_name, proxy_url)); + if let Some((server_name, proxy_url)) = self.parse_server_block(&lines, &mut i) { + sites.push((server_name, proxy_url)); } } i += 1; @@ -752,7 +751,7 @@ impl SystemdCollector { } /// Parse a server block to extract the primary server_name - fn parse_server_block(&self, lines: &[&str], start_index: &mut usize) -> Option { + fn parse_server_block(&self, lines: &[&str], start_index: &mut usize) -> Option<(String, String)> { use tracing::debug; let mut server_names = Vec::new(); let mut proxy_pass_url = None; @@ -806,9 +805,15 @@ impl SystemdCollector { *start_index = i - 1; - if let Some(proxy_url) = proxy_pass_url { - if !has_redirect { - return Some(proxy_url); + if !server_names.is_empty() && !has_redirect { + if let Some(proxy_url) = proxy_pass_url { + // Site with proxy_pass: check backend, show "P" prefix + let proxied_name = format!("P {}", server_names[0]); + return Some((proxied_name, proxy_url)); + } else { + // Site without proxy_pass: check external HTTPS + let external_url = format!("https://{}", server_names[0]); + return Some((server_names[0].clone(), external_url)); } } diff --git a/dashboard/src/ui/widgets/services.rs b/dashboard/src/ui/widgets/services.rs index 56d00e6..e3b1333 100644 --- a/dashboard/src/ui/widgets/services.rs +++ b/dashboard/src/ui/widgets/services.rs @@ -8,7 +8,7 @@ use std::collections::HashMap; use tracing::debug; use super::Widget; -use crate::ui::theme::{Theme, Typography, Components, StatusIcons}; +use crate::ui::theme::{Components, StatusIcons, Theme, Typography}; use ratatui::style::Style; /// Services widget displaying hierarchical systemd service statuses @@ -42,16 +42,18 @@ impl ServicesWidget { has_data: false, } } - + /// Extract service name and determine if it's a parent or sub-service fn extract_service_info(metric_name: &str) -> Option<(String, Option)> { if metric_name.starts_with("service_") { - if let Some(end_pos) = metric_name.rfind("_status") + if let Some(end_pos) = metric_name + .rfind("_status") .or_else(|| metric_name.rfind("_memory_mb")) .or_else(|| metric_name.rfind("_disk_gb")) - .or_else(|| metric_name.rfind("_latency_ms")) { + .or_else(|| metric_name.rfind("_latency_ms")) + { let service_part = &metric_name[8..end_pos]; // Remove "service_" prefix - + // Check for sub-services patterns if service_part.starts_with("nginx_") { // nginx sub-services: service_nginx_gitea_latency_ms -> ("nginx", "gitea") @@ -69,11 +71,11 @@ impl ServicesWidget { } None } - + /// Format disk size with appropriate units (kB/MB/GB) fn format_disk_size(size_gb: f32) -> String { let size_mb = size_gb * 1024.0; // Convert GB to MB - + if size_mb >= 1024.0 { // Show as GB format!("{:.1}GB", size_gb) @@ -93,41 +95,47 @@ impl ServicesWidget { /// Format parent service line - returns text without icon for span formatting fn format_parent_service_line(&self, name: &str, info: &ServiceInfo) -> String { - let memory_str = info.memory_mb.map_or("0M".to_string(), |m| format!("{:.0}M", m)); - let disk_str = info.disk_gb.map_or("0".to_string(), |d| Self::format_disk_size(d)); - + let memory_str = info + .memory_mb + .map_or("0M".to_string(), |m| format!("{:.0}M", m)); + let disk_str = info + .disk_gb + .map_or("0".to_string(), |d| Self::format_disk_size(d)); + // Truncate long service names to fit layout (account for icon space) let short_name = if name.len() > 22 { format!("{}...", &name[..19]) } else { name.to_string() }; - + // Parent services always show active/inactive status let status_str = match info.widget_status { Status::Ok => "active".to_string(), - Status::Warning => "inactive".to_string(), + Status::Warning => "inactive".to_string(), Status::Critical => "failed".to_string(), Status::Unknown => "unknown".to_string(), }; - - format!("{:<24} {:<10} {:<8} {:<8}", - short_name, - status_str, - memory_str, - disk_str) + + format!( + "{:<23} {:<10} {:<8} {:<8}", + short_name, status_str, memory_str, disk_str + ) } - - + /// Create spans for sub-service with icon next to name - fn create_sub_service_spans(&self, name: &str, info: &ServiceInfo) -> Vec> { + fn create_sub_service_spans( + &self, + name: &str, + info: &ServiceInfo, + ) -> Vec> { // Truncate long sub-service names to fit layout (accounting for indentation) let short_name = if name.len() > 18 { format!("{}...", &name[..15]) } else { name.to_string() }; - + // Sub-services show latency if available, otherwise status let status_str = if let Some(latency) = info.latency_ms { if latency < 0.0 { @@ -138,41 +146,47 @@ impl ServicesWidget { } else { match info.widget_status { Status::Ok => "active".to_string(), - Status::Warning => "inactive".to_string(), + Status::Warning => "inactive".to_string(), Status::Critical => "failed".to_string(), Status::Unknown => "unknown".to_string(), } }; - + let status_color = match info.widget_status { Status::Ok => Theme::success(), Status::Warning => Theme::warning(), Status::Critical => Theme::error(), Status::Unknown => Theme::muted_text(), }; - + let icon = StatusIcons::get_icon(info.widget_status); - + vec![ // Indentation and tree prefix ratatui::text::Span::styled( " ├─ ".to_string(), - Style::default().fg(Theme::secondary_text()).bg(Theme::background()) + Style::default() + .fg(Theme::secondary_text()) + .bg(Theme::background()), ), // Status icon ratatui::text::Span::styled( format!("{} ", icon), - Style::default().fg(status_color).bg(Theme::background()) + Style::default().fg(status_color).bg(Theme::background()), ), // Service name ratatui::text::Span::styled( format!("{:<18} ", short_name), - Style::default().fg(Theme::secondary_text()).bg(Theme::background()) + Style::default() + .fg(Theme::secondary_text()) + .bg(Theme::background()), ), // Status/latency text ratatui::text::Span::styled( status_str, - Style::default().fg(Theme::secondary_text()).bg(Theme::background()) + Style::default() + .fg(Theme::secondary_text()) + .bg(Theme::background()), ), ] } @@ -181,23 +195,26 @@ impl ServicesWidget { impl Widget for ServicesWidget { fn update_from_metrics(&mut self, metrics: &[&Metric]) { debug!("Services widget updating with {} metrics", metrics.len()); - + // Don't clear existing services - preserve data between metric batches - + // Process individual service metrics for metric in metrics { if let Some((parent_service, sub_service)) = Self::extract_service_info(&metric.name) { match sub_service { None => { // Parent service metric - let service_info = self.parent_services.entry(parent_service).or_insert(ServiceInfo { - status: "unknown".to_string(), - memory_mb: None, - disk_gb: None, - latency_ms: None, - widget_status: Status::Unknown, - }); - + let service_info = + self.parent_services + .entry(parent_service) + .or_insert(ServiceInfo { + status: "unknown".to_string(), + memory_mb: None, + disk_gb: None, + latency_ms: None, + widget_status: Status::Unknown, + }); + if metric.name.ends_with("_status") { service_info.status = metric.value.as_string(); service_info.widget_status = metric.status; @@ -213,22 +230,31 @@ impl Widget for ServicesWidget { } Some(sub_name) => { // Sub-service metric - let sub_service_list = self.sub_services.entry(parent_service).or_insert_with(Vec::new); - + let sub_service_list = self + .sub_services + .entry(parent_service) + .or_insert_with(Vec::new); + // Find existing sub-service or create new one - let sub_service_info = if let Some(pos) = sub_service_list.iter().position(|(name, _)| name == &sub_name) { + let sub_service_info = if let Some(pos) = sub_service_list + .iter() + .position(|(name, _)| name == &sub_name) + { &mut sub_service_list[pos].1 } else { - sub_service_list.push((sub_name.clone(), ServiceInfo { - status: "unknown".to_string(), - memory_mb: None, - disk_gb: None, - latency_ms: None, - widget_status: Status::Unknown, - })); + sub_service_list.push(( + sub_name.clone(), + ServiceInfo { + status: "unknown".to_string(), + memory_mb: None, + disk_gb: None, + latency_ms: None, + widget_status: Status::Unknown, + }, + )); &mut sub_service_list.last_mut().unwrap().1 }; - + if metric.name.ends_with("_status") { sub_service_info.status = metric.value.as_string(); sub_service_info.widget_status = metric.status; @@ -250,88 +276,102 @@ impl Widget for ServicesWidget { } } } - + // Aggregate status from all parent and sub-services let mut all_statuses = Vec::new(); - + // Add parent service statuses all_statuses.extend(self.parent_services.values().map(|info| info.widget_status)); - + // Add sub-service statuses for sub_list in self.sub_services.values() { all_statuses.extend(sub_list.iter().map(|(_, info)| info.widget_status)); } - + self.status = if all_statuses.is_empty() { Status::Unknown } else { Status::aggregate(&all_statuses) }; - + self.has_data = !self.parent_services.is_empty() || !self.sub_services.is_empty(); - - debug!("Services widget updated: {} parent services, {} sub-service groups, status={:?}", - self.parent_services.len(), self.sub_services.len(), self.status); + + debug!( + "Services widget updated: {} parent services, {} sub-service groups, status={:?}", + self.parent_services.len(), + self.sub_services.len(), + self.status + ); } - + fn render(&mut self, frame: &mut Frame, area: Rect) { let services_block = Components::widget_block("services"); let inner_area = services_block.inner(area); frame.render_widget(services_block, area); - + let content_chunks = Layout::default() .direction(Direction::Vertical) .constraints([Constraint::Length(1), Constraint::Min(0)]) .split(inner_area); - + // Header - let header = format!("{:<25} {:<10} {:<8} {:<8}", "Service:", "Status:", "RAM:", "Disk:"); + let header = format!( + "{:<25} {:<10} {:<8} {:<8}", + "Service:", "Status:", "RAM:", "Disk:" + ); let header_para = Paragraph::new(header).style(Typography::muted()); frame.render_widget(header_para, content_chunks[0]); - + // Check if we have any services to display if self.parent_services.is_empty() && self.sub_services.is_empty() { let empty_text = Paragraph::new("No process data").style(Typography::muted()); frame.render_widget(empty_text, content_chunks[1]); return; } - + // Build hierarchical service list for display let mut display_lines = Vec::new(); - + // Sort parent services alphabetically for consistent order let mut parent_services: Vec<_> = self.parent_services.iter().collect(); parent_services.sort_by(|(a, _), (b, _)| a.cmp(b)); - + for (parent_name, parent_info) in parent_services { // Add parent service line let parent_line = self.format_parent_service_line(parent_name, parent_info); display_lines.push((parent_line, parent_info.widget_status, false, None)); // false = not sub-service - + // Add sub-services for this parent (if any) if let Some(sub_list) = self.sub_services.get(parent_name) { // Sort sub-services by name for consistent display let mut sorted_subs = sub_list.clone(); sorted_subs.sort_by(|(a, _), (b, _)| a.cmp(b)); - + for (sub_name, sub_info) in sorted_subs { // Store sub-service info for custom span rendering - display_lines.push((sub_name.clone(), sub_info.widget_status, true, Some(sub_info.clone()))); // true = sub-service + display_lines.push(( + sub_name.clone(), + sub_info.widget_status, + true, + Some(sub_info.clone()), + )); // true = sub-service } } } - + // Render all lines within available space let available_lines = content_chunks[1].height as usize; let lines_to_show = available_lines.min(display_lines.len()); - + if lines_to_show > 0 { let service_chunks = Layout::default() .direction(Direction::Vertical) .constraints(vec![Constraint::Length(1); lines_to_show]) .split(content_chunks[1]); - - for (i, (line_text, line_status, is_sub, sub_info)) in display_lines.iter().take(lines_to_show).enumerate() { + + for (i, (line_text, line_status, is_sub, sub_info)) in + display_lines.iter().take(lines_to_show).enumerate() + { let spans = if *is_sub && sub_info.is_some() { // Use custom sub-service span creation self.create_sub_service_spans(line_text, sub_info.as_ref().unwrap()) @@ -343,7 +383,7 @@ impl Widget for ServicesWidget { frame.render_widget(service_para, service_chunks[i]); } } - + // Show indicator if there are more services than we can display if display_lines.len() > available_lines { let more_count = display_lines.len() - available_lines; @@ -354,7 +394,7 @@ impl Widget for ServicesWidget { width: content_chunks[1].width, height: 1, }; - + let more_text = format!("... and {} more services", more_count); let more_para = Paragraph::new(more_text).style(Typography::muted()); frame.render_widget(more_para, last_line_area); @@ -367,4 +407,4 @@ impl Default for ServicesWidget { fn default() -> Self { Self::new() } -} \ No newline at end of file +}