Add service_type field to separate data from presentation

Changes:
- Add service_type field to SubServiceData: 'nginx_site', 'container', 'image'
- Agent sends pure data without display formatting
- Dashboard checks service_type to decide presentation
- Docker images now display without status icon (service_type='image')
- Remove unused image_size_str from docker images tuple

Clean separation: agent provides data, dashboard handles display logic.
This commit is contained in:
Christoffer Martinsson 2025-11-27 18:09:20 +01:00
parent ff2b43827a
commit 2f94a4b853
4 changed files with 72 additions and 37 deletions

6
Cargo.lock generated
View File

@ -279,7 +279,7 @@ checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d"
[[package]] [[package]]
name = "cm-dashboard" name = "cm-dashboard"
version = "0.1.188" version = "0.1.189"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"chrono", "chrono",
@ -301,7 +301,7 @@ dependencies = [
[[package]] [[package]]
name = "cm-dashboard-agent" name = "cm-dashboard-agent"
version = "0.1.188" version = "0.1.189"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",
@ -324,7 +324,7 @@ dependencies = [
[[package]] [[package]]
name = "cm-dashboard-shared" name = "cm-dashboard-shared"
version = "0.1.188" version = "0.1.189"
dependencies = [ dependencies = [
"chrono", "chrono",
"serde", "serde",

View File

@ -113,6 +113,7 @@ impl SystemdCollector {
name: site_name.clone(), name: site_name.clone(),
service_status: self.calculate_service_status(&site_name, &site_status), service_status: self.calculate_service_status(&site_name, &site_status),
metrics, metrics,
service_type: "nginx_site".to_string(),
}); });
} }
} }
@ -128,12 +129,13 @@ impl SystemdCollector {
name: container_name.clone(), name: container_name.clone(),
service_status: self.calculate_service_status(&container_name, &container_status), service_status: self.calculate_service_status(&container_name, &container_status),
metrics, metrics,
service_type: "container".to_string(),
}); });
} }
// Add Docker images // Add Docker images
let docker_images = self.get_docker_images(); let docker_images = self.get_docker_images();
for (image_name, image_status, image_size_str, image_size_mb) in docker_images { for (image_name, image_status, image_size_mb) in docker_images {
let mut metrics = Vec::new(); let mut metrics = Vec::new();
metrics.push(SubServiceMetric { metrics.push(SubServiceMetric {
label: "size".to_string(), label: "size".to_string(),
@ -142,9 +144,10 @@ impl SystemdCollector {
}); });
sub_services.push(SubServiceData { sub_services.push(SubServiceData {
name: format!("{} ({})", image_name, image_size_str), name: image_name.to_string(),
service_status: self.calculate_service_status(&image_name, &image_status), service_status: self.calculate_service_status(&image_name, &image_status),
metrics, metrics,
service_type: "image".to_string(),
}); });
} }
} }
@ -824,7 +827,7 @@ impl SystemdCollector {
} }
/// Get docker images as sub-services /// Get docker images as sub-services
fn get_docker_images(&self) -> Vec<(String, String, String, f32)> { fn get_docker_images(&self) -> Vec<(String, String, f32)> {
let mut images = Vec::new(); let mut images = Vec::new();
// Check if docker is available (cm-agent user is in docker group) with 3 second timeout // Check if docker is available (cm-agent user is in docker group) with 3 second timeout
let output = Command::new("timeout") let output = Command::new("timeout")
@ -865,9 +868,8 @@ impl SystemdCollector {
let size_mb = self.parse_docker_size(size_str); let size_mb = self.parse_docker_size(size_str);
images.push(( images.push((
format!("I {}", image_name), image_name.to_string(),
"inactive".to_string(), // Images are informational - use inactive for neutral display "inactive".to_string(), // Images are informational - use inactive for neutral display
size_str.to_string(),
size_mb size_mb
)); ));
} }

View File

@ -32,6 +32,7 @@ struct ServiceInfo {
disk_gb: Option<f32>, disk_gb: Option<f32>,
metrics: Vec<(String, f32, Option<String>)>, // (label, value, unit) metrics: Vec<(String, f32, Option<String>)>, // (label, value, unit)
widget_status: Status, widget_status: Status,
service_type: String, // "nginx_site", "container", "image", or empty for parent services
} }
impl ServicesWidget { impl ServicesWidget {
@ -179,6 +180,30 @@ impl ServicesWidget {
}; };
let tree_symbol = if is_last { "└─" } else { "├─" }; let tree_symbol = if is_last { "└─" } else { "├─" };
// Docker images don't have status icons
if info.service_type == "image" {
vec![
// Indentation and tree prefix
ratatui::text::Span::styled(
format!(" {} ", tree_symbol),
Typography::tree(),
),
// Service name (no icon for images)
ratatui::text::Span::styled(
format!("{:<18} ", short_name),
Style::default()
.fg(Theme::secondary_text())
.bg(Theme::background()),
),
// Status/metrics text
ratatui::text::Span::styled(
status_str,
Style::default()
.fg(Theme::secondary_text())
.bg(Theme::background()),
),
]
} else {
vec![ vec![
// Indentation and tree prefix // Indentation and tree prefix
ratatui::text::Span::styled( ratatui::text::Span::styled(
@ -206,6 +231,7 @@ impl ServicesWidget {
), ),
] ]
} }
}
/// Move selection up /// Move selection up
pub fn select_previous(&mut self) { pub fn select_previous(&mut self) {
@ -282,6 +308,7 @@ impl Widget for ServicesWidget {
disk_gb: Some(service.disk_gb), disk_gb: Some(service.disk_gb),
metrics: Vec::new(), // Parent services don't have custom metrics metrics: Vec::new(), // Parent services don't have custom metrics
widget_status: service.service_status, widget_status: service.service_status,
service_type: String::new(), // Parent services have no type
}; };
self.parent_services.insert(service.name.clone(), parent_info); self.parent_services.insert(service.name.clone(), parent_info);
@ -299,6 +326,7 @@ impl Widget for ServicesWidget {
disk_gb: None, // Not used for sub-services disk_gb: None, // Not used for sub-services
metrics, metrics,
widget_status: sub_service.service_status, widget_status: sub_service.service_status,
service_type: sub_service.service_type.clone(),
}; };
sub_list.push((sub_service.name.clone(), sub_info)); sub_list.push((sub_service.name.clone(), sub_info));
} }
@ -342,6 +370,7 @@ impl ServicesWidget {
disk_gb: None, disk_gb: None,
metrics: Vec::new(), metrics: Vec::new(),
widget_status: Status::Unknown, widget_status: Status::Unknown,
service_type: String::new(),
}); });
if metric.name.ends_with("_status") { if metric.name.ends_with("_status") {
@ -377,6 +406,7 @@ impl ServicesWidget {
disk_gb: None, disk_gb: None,
metrics: Vec::new(), metrics: Vec::new(),
widget_status: Status::Unknown, widget_status: Status::Unknown,
service_type: String::new(), // Unknown type in legacy path
}, },
)); ));
&mut sub_service_list.last_mut().unwrap().1 &mut sub_service_list.last_mut().unwrap().1

View File

@ -149,6 +149,9 @@ pub struct SubServiceData {
pub name: String, pub name: String,
pub service_status: Status, pub service_status: Status,
pub metrics: Vec<SubServiceMetric>, pub metrics: Vec<SubServiceMetric>,
/// Type of sub-service: "nginx_site", "container", "image"
#[serde(default)]
pub service_type: String,
} }
/// Individual metric for a sub-service /// Individual metric for a sub-service