Fix nginx monitoring and services panel alignment

- Add support for both proxied and static nginx sites
- Proxied sites show 'P' prefix and check backend URLs
- Static sites check external HTTPS URLs
- Fix services panel column alignment for main services
- Keep 10-second timeout for all site checks
This commit is contained in:
Christoffer Martinsson 2025-10-20 14:56:26 +02:00
parent 11be496a26
commit 2ccfc4256a
2 changed files with 129 additions and 84 deletions

View File

@ -739,9 +739,8 @@ impl SystemdCollector {
while i < lines.len() { while i < lines.len() {
let line = lines[i].trim(); let line = lines[i].trim();
if line.starts_with("server") && line.contains("{") { if line.starts_with("server") && line.contains("{") {
if let Some(proxy_url) = self.parse_server_block(&lines, &mut i) { if let Some((server_name, proxy_url)) = self.parse_server_block(&lines, &mut i) {
let site_name = proxy_url.replace("http://", "").replace("https://", ""); sites.push((server_name, proxy_url));
sites.push((site_name, proxy_url));
} }
} }
i += 1; i += 1;
@ -752,7 +751,7 @@ impl SystemdCollector {
} }
/// Parse a server block to extract the primary server_name /// Parse a server block to extract the primary server_name
fn parse_server_block(&self, lines: &[&str], start_index: &mut usize) -> Option<String> { fn parse_server_block(&self, lines: &[&str], start_index: &mut usize) -> Option<(String, String)> {
use tracing::debug; use tracing::debug;
let mut server_names = Vec::new(); let mut server_names = Vec::new();
let mut proxy_pass_url = None; let mut proxy_pass_url = None;
@ -806,9 +805,15 @@ impl SystemdCollector {
*start_index = i - 1; *start_index = i - 1;
if let Some(proxy_url) = proxy_pass_url { if !server_names.is_empty() && !has_redirect {
if !has_redirect { if let Some(proxy_url) = proxy_pass_url {
return Some(proxy_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));
} }
} }

View File

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