Implement real-time process monitoring and fix UI hardcoded data
This commit addresses several key issues identified during development: Major Changes: - Replace hardcoded top CPU/RAM process display with real system data - Add intelligent process monitoring to CpuCollector using ps command - Fix disk metrics permission issues in systemd collector - Optimize service collection to focus on status, memory, and disk only - Update dashboard widgets to display live process information Process Monitoring Implementation: - Added collect_top_cpu_process() and collect_top_ram_process() methods - Implemented ps-based monitoring with accurate CPU percentages - Added filtering to prevent self-monitoring artifacts (ps commands) - Enhanced error handling and validation for process data - Dashboard now shows realistic values like "claude (PID 2974) 11.0%" Service Collection Optimization: - Removed CPU monitoring from systemd collector for efficiency - Enhanced service directory permission error logging - Simplified services widget to show essential metrics only - Fixed service-to-directory mapping accuracy UI and Dashboard Improvements: - Reorganized dashboard layout with btop-inspired multi-panel design - Updated system panel to include real top CPU/RAM process display - Enhanced widget formatting and data presentation - Removed placeholder/hardcoded data throughout the interface Technical Details: - Updated agent/src/collectors/cpu.rs with process monitoring - Modified dashboard/src/ui/mod.rs for real-time process display - Enhanced systemd collector error handling and disk metrics - Updated CLAUDE.md documentation with implementation details
This commit is contained in:
196
dashboard/src/ui/widgets/cpu.rs
Normal file
196
dashboard/src/ui/widgets/cpu.rs
Normal file
@@ -0,0 +1,196 @@
|
||||
use cm_dashboard_shared::{Metric, MetricValue, Status};
|
||||
use ratatui::{
|
||||
layout::{Constraint, Direction, Layout, Rect},
|
||||
style::{Color, Style},
|
||||
widgets::{Block, Borders, Gauge, Paragraph},
|
||||
text::{Line, Span},
|
||||
Frame,
|
||||
};
|
||||
use tracing::debug;
|
||||
|
||||
use super::Widget;
|
||||
use crate::ui::theme::Theme;
|
||||
|
||||
/// CPU widget displaying load, temperature, and frequency
|
||||
pub struct CpuWidget {
|
||||
/// CPU load averages (1, 5, 15 minutes)
|
||||
load_1min: Option<f32>,
|
||||
load_5min: Option<f32>,
|
||||
load_15min: Option<f32>,
|
||||
/// CPU temperature in Celsius
|
||||
temperature: Option<f32>,
|
||||
/// CPU frequency in MHz
|
||||
frequency: Option<f32>,
|
||||
/// Aggregated status
|
||||
status: Status,
|
||||
/// Last update indicator
|
||||
has_data: bool,
|
||||
}
|
||||
|
||||
impl CpuWidget {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
load_1min: None,
|
||||
load_5min: None,
|
||||
load_15min: None,
|
||||
temperature: None,
|
||||
frequency: None,
|
||||
status: Status::Unknown,
|
||||
has_data: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get status color for display (btop-style)
|
||||
fn get_status_color(&self) -> Color {
|
||||
Theme::status_color(self.status)
|
||||
}
|
||||
|
||||
/// Format load average for display
|
||||
fn format_load(&self) -> String {
|
||||
match (self.load_1min, self.load_5min, self.load_15min) {
|
||||
(Some(l1), Some(l5), Some(l15)) => {
|
||||
format!("{:.2} {:.2} {:.2}", l1, l5, l15)
|
||||
}
|
||||
_ => "— — —".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Format temperature for display
|
||||
fn format_temperature(&self) -> String {
|
||||
match self.temperature {
|
||||
Some(temp) => format!("{:.1}°C", temp),
|
||||
None => "—°C".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Format frequency for display
|
||||
fn format_frequency(&self) -> String {
|
||||
match self.frequency {
|
||||
Some(freq) => format!("{:.1} MHz", freq),
|
||||
None => "— MHz".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get load percentage for gauge (based on load_1min)
|
||||
fn get_load_percentage(&self) -> u16 {
|
||||
match self.load_1min {
|
||||
Some(load) => {
|
||||
// Assume 8-core system, so 100% = load of 8.0
|
||||
let percentage = (load / 8.0 * 100.0).min(100.0).max(0.0);
|
||||
percentage as u16
|
||||
}
|
||||
None => 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create btop-style dotted bar pattern (like real btop)
|
||||
fn create_btop_dotted_bar(&self, percentage: u16, width: usize) -> String {
|
||||
let filled = (width * percentage as usize) / 100;
|
||||
let empty = width.saturating_sub(filled);
|
||||
|
||||
// Real btop uses these patterns:
|
||||
// High usage: ████████ (solid blocks)
|
||||
// Medium usage: :::::::: (colons)
|
||||
// Low usage: ........ (dots)
|
||||
// Empty: (spaces)
|
||||
|
||||
let pattern = if percentage >= 75 {
|
||||
"█" // High usage - solid blocks
|
||||
} else if percentage >= 25 {
|
||||
":" // Medium usage - colons like btop
|
||||
} else if percentage > 0 {
|
||||
"." // Low usage - dots like btop
|
||||
} else {
|
||||
" " // No usage - spaces
|
||||
};
|
||||
|
||||
let filled_chars = pattern.repeat(filled);
|
||||
let empty_chars = " ".repeat(empty);
|
||||
|
||||
filled_chars + &empty_chars
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for CpuWidget {
|
||||
fn update_from_metrics(&mut self, metrics: &[&Metric]) {
|
||||
debug!("CPU widget updating with {} metrics", metrics.len());
|
||||
|
||||
// Reset status aggregation
|
||||
let mut statuses = Vec::new();
|
||||
|
||||
for metric in metrics {
|
||||
match metric.name.as_str() {
|
||||
"cpu_load_1min" => {
|
||||
if let Some(value) = metric.value.as_f32() {
|
||||
self.load_1min = Some(value);
|
||||
statuses.push(metric.status);
|
||||
}
|
||||
}
|
||||
"cpu_load_5min" => {
|
||||
if let Some(value) = metric.value.as_f32() {
|
||||
self.load_5min = Some(value);
|
||||
statuses.push(metric.status);
|
||||
}
|
||||
}
|
||||
"cpu_load_15min" => {
|
||||
if let Some(value) = metric.value.as_f32() {
|
||||
self.load_15min = Some(value);
|
||||
statuses.push(metric.status);
|
||||
}
|
||||
}
|
||||
"cpu_temperature_celsius" => {
|
||||
if let Some(value) = metric.value.as_f32() {
|
||||
self.temperature = Some(value);
|
||||
statuses.push(metric.status);
|
||||
}
|
||||
}
|
||||
"cpu_frequency_mhz" => {
|
||||
if let Some(value) = metric.value.as_f32() {
|
||||
self.frequency = Some(value);
|
||||
statuses.push(metric.status);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
// Aggregate status
|
||||
self.status = if statuses.is_empty() {
|
||||
Status::Unknown
|
||||
} else {
|
||||
Status::aggregate(&statuses)
|
||||
};
|
||||
|
||||
self.has_data = !metrics.is_empty();
|
||||
|
||||
debug!("CPU widget updated: load={:?}, temp={:?}, freq={:?}, status={:?}",
|
||||
self.load_1min, self.temperature, self.frequency, self.status);
|
||||
}
|
||||
|
||||
fn render(&mut self, frame: &mut Frame, area: Rect) {
|
||||
let content_chunks = Layout::default().direction(Direction::Vertical).constraints([Constraint::Length(1), Constraint::Length(1), Constraint::Length(1)]).split(area);
|
||||
let cpu_title = Paragraph::new("CPU:").style(Style::default().fg(Theme::primary_text()).bg(Theme::background()));
|
||||
frame.render_widget(cpu_title, content_chunks[0]);
|
||||
let overall_usage = self.get_load_percentage();
|
||||
let cpu_usage_text = format!("Usage: {} {:>3}%", self.create_btop_dotted_bar(overall_usage, 20), overall_usage);
|
||||
let cpu_usage_para = Paragraph::new(cpu_usage_text).style(Style::default().fg(Theme::cpu_color(overall_usage)).bg(Theme::background()));
|
||||
frame.render_widget(cpu_usage_para, content_chunks[1]);
|
||||
let load_freq_text = format!("Load: {} • {}", self.format_load(), self.format_frequency());
|
||||
let load_freq_para = Paragraph::new(load_freq_text).style(Style::default().fg(Theme::secondary_text()).bg(Theme::background()));
|
||||
frame.render_widget(load_freq_para, content_chunks[2]);
|
||||
}
|
||||
|
||||
fn get_name(&self) -> &str {
|
||||
"CPU"
|
||||
}
|
||||
|
||||
fn has_data(&self) -> bool {
|
||||
self.has_data
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for CpuWidget {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
258
dashboard/src/ui/widgets/memory.rs
Normal file
258
dashboard/src/ui/widgets/memory.rs
Normal file
@@ -0,0 +1,258 @@
|
||||
use cm_dashboard_shared::{Metric, MetricValue, Status};
|
||||
use ratatui::{
|
||||
layout::{Constraint, Direction, Layout, Rect},
|
||||
style::{Color, Style},
|
||||
widgets::{Block, Borders, Gauge, Paragraph},
|
||||
text::{Line, Span},
|
||||
Frame,
|
||||
};
|
||||
use tracing::debug;
|
||||
|
||||
use super::Widget;
|
||||
use crate::ui::theme::Theme;
|
||||
|
||||
/// Memory widget displaying usage, totals, and swap information
|
||||
pub struct MemoryWidget {
|
||||
/// Memory usage percentage
|
||||
usage_percent: Option<f32>,
|
||||
/// Total memory in GB
|
||||
total_gb: Option<f32>,
|
||||
/// Used memory in GB
|
||||
used_gb: Option<f32>,
|
||||
/// Available memory in GB
|
||||
available_gb: Option<f32>,
|
||||
/// Total swap in GB
|
||||
swap_total_gb: Option<f32>,
|
||||
/// Used swap in GB
|
||||
swap_used_gb: Option<f32>,
|
||||
/// /tmp directory size in MB
|
||||
tmp_size_mb: Option<f32>,
|
||||
/// /tmp total size in MB
|
||||
tmp_total_mb: Option<f32>,
|
||||
/// /tmp usage percentage
|
||||
tmp_usage_percent: Option<f32>,
|
||||
/// Aggregated status
|
||||
status: Status,
|
||||
/// Last update indicator
|
||||
has_data: bool,
|
||||
}
|
||||
|
||||
impl MemoryWidget {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
usage_percent: None,
|
||||
total_gb: None,
|
||||
used_gb: None,
|
||||
available_gb: None,
|
||||
swap_total_gb: None,
|
||||
swap_used_gb: None,
|
||||
tmp_size_mb: None,
|
||||
tmp_total_mb: None,
|
||||
tmp_usage_percent: None,
|
||||
status: Status::Unknown,
|
||||
has_data: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get status color for display (btop-style)
|
||||
fn get_status_color(&self) -> Color {
|
||||
Theme::status_color(self.status)
|
||||
}
|
||||
|
||||
/// Format memory usage for display
|
||||
fn format_memory_usage(&self) -> String {
|
||||
match (self.used_gb, self.total_gb) {
|
||||
(Some(used), Some(total)) => {
|
||||
format!("{:.1}/{:.1} GB", used, total)
|
||||
}
|
||||
_ => "—/— GB".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Format swap usage for display
|
||||
fn format_swap_usage(&self) -> String {
|
||||
match (self.swap_used_gb, self.swap_total_gb) {
|
||||
(Some(used), Some(total)) => {
|
||||
if total > 0.0 {
|
||||
format!("{:.1}/{:.1} GB", used, total)
|
||||
} else {
|
||||
"No swap".to_string()
|
||||
}
|
||||
}
|
||||
_ => "—/— GB".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Format /tmp usage for display
|
||||
fn format_tmp_usage(&self) -> String {
|
||||
match (self.tmp_size_mb, self.tmp_total_mb) {
|
||||
(Some(used), Some(total)) => {
|
||||
format!("{:.1}/{:.0} MB", used, total)
|
||||
}
|
||||
_ => "—/— MB".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get memory usage percentage for gauge
|
||||
fn get_memory_percentage(&self) -> u16 {
|
||||
match self.usage_percent {
|
||||
Some(percent) => percent.min(100.0).max(0.0) as u16,
|
||||
None => {
|
||||
// Calculate from used/total if percentage not available
|
||||
match (self.used_gb, self.total_gb) {
|
||||
(Some(used), Some(total)) if total > 0.0 => {
|
||||
let percent = (used / total * 100.0).min(100.0).max(0.0);
|
||||
percent as u16
|
||||
}
|
||||
_ => 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Get swap usage percentage
|
||||
fn get_swap_percentage(&self) -> u16 {
|
||||
match (self.swap_used_gb, self.swap_total_gb) {
|
||||
(Some(used), Some(total)) if total > 0.0 => {
|
||||
let percent = (used / total * 100.0).min(100.0).max(0.0);
|
||||
percent as u16
|
||||
}
|
||||
_ => 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create btop-style dotted bar pattern (same as CPU)
|
||||
fn create_btop_dotted_bar(&self, percentage: u16, width: usize) -> String {
|
||||
let filled = (width * percentage as usize) / 100;
|
||||
let empty = width.saturating_sub(filled);
|
||||
|
||||
// Real btop uses these patterns:
|
||||
// High usage: ████████ (solid blocks)
|
||||
// Medium usage: :::::::: (colons)
|
||||
// Low usage: ........ (dots)
|
||||
// Empty: (spaces)
|
||||
|
||||
let pattern = if percentage >= 75 {
|
||||
"█" // High usage - solid blocks
|
||||
} else if percentage >= 25 {
|
||||
":" // Medium usage - colons like btop
|
||||
} else if percentage > 0 {
|
||||
"." // Low usage - dots like btop
|
||||
} else {
|
||||
" " // No usage - spaces
|
||||
};
|
||||
|
||||
let filled_chars = pattern.repeat(filled);
|
||||
let empty_chars = " ".repeat(empty);
|
||||
|
||||
filled_chars + &empty_chars
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for MemoryWidget {
|
||||
fn update_from_metrics(&mut self, metrics: &[&Metric]) {
|
||||
debug!("Memory widget updating with {} metrics", metrics.len());
|
||||
|
||||
// Reset status aggregation
|
||||
let mut statuses = Vec::new();
|
||||
|
||||
for metric in metrics {
|
||||
match metric.name.as_str() {
|
||||
"memory_usage_percent" => {
|
||||
if let Some(value) = metric.value.as_f32() {
|
||||
self.usage_percent = Some(value);
|
||||
statuses.push(metric.status);
|
||||
}
|
||||
}
|
||||
"memory_total_gb" => {
|
||||
if let Some(value) = metric.value.as_f32() {
|
||||
self.total_gb = Some(value);
|
||||
statuses.push(metric.status);
|
||||
}
|
||||
}
|
||||
"memory_used_gb" => {
|
||||
if let Some(value) = metric.value.as_f32() {
|
||||
self.used_gb = Some(value);
|
||||
statuses.push(metric.status);
|
||||
}
|
||||
}
|
||||
"memory_available_gb" => {
|
||||
if let Some(value) = metric.value.as_f32() {
|
||||
self.available_gb = Some(value);
|
||||
statuses.push(metric.status);
|
||||
}
|
||||
}
|
||||
"memory_swap_total_gb" => {
|
||||
if let Some(value) = metric.value.as_f32() {
|
||||
self.swap_total_gb = Some(value);
|
||||
statuses.push(metric.status);
|
||||
}
|
||||
}
|
||||
"memory_swap_used_gb" => {
|
||||
if let Some(value) = metric.value.as_f32() {
|
||||
self.swap_used_gb = Some(value);
|
||||
statuses.push(metric.status);
|
||||
}
|
||||
}
|
||||
"disk_tmp_size_mb" => {
|
||||
if let Some(value) = metric.value.as_f32() {
|
||||
self.tmp_size_mb = Some(value);
|
||||
statuses.push(metric.status);
|
||||
}
|
||||
}
|
||||
"disk_tmp_total_mb" => {
|
||||
if let Some(value) = metric.value.as_f32() {
|
||||
self.tmp_total_mb = Some(value);
|
||||
statuses.push(metric.status);
|
||||
}
|
||||
}
|
||||
"disk_tmp_usage_percent" => {
|
||||
if let Some(value) = metric.value.as_f32() {
|
||||
self.tmp_usage_percent = Some(value);
|
||||
statuses.push(metric.status);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
// Aggregate status
|
||||
self.status = if statuses.is_empty() {
|
||||
Status::Unknown
|
||||
} else {
|
||||
Status::aggregate(&statuses)
|
||||
};
|
||||
|
||||
self.has_data = !metrics.is_empty();
|
||||
|
||||
debug!("Memory widget updated: usage={:?}%, total={:?}GB, swap_total={:?}GB, tmp={:?}/{:?}MB, status={:?}",
|
||||
self.usage_percent, self.total_gb, self.swap_total_gb, self.tmp_size_mb, self.tmp_total_mb, self.status);
|
||||
}
|
||||
|
||||
fn render(&mut self, frame: &mut Frame, area: Rect) {
|
||||
let content_chunks = Layout::default().direction(Direction::Vertical).constraints([Constraint::Length(1), Constraint::Length(1), Constraint::Length(1)]).split(area);
|
||||
let mem_title = Paragraph::new("Memory:").style(Style::default().fg(Theme::primary_text()).bg(Theme::background()));
|
||||
frame.render_widget(mem_title, content_chunks[0]);
|
||||
let memory_percentage = self.get_memory_percentage();
|
||||
let mem_usage_text = format!("Usage: {} {:>3}%", self.create_btop_dotted_bar(memory_percentage, 20), memory_percentage);
|
||||
let mem_usage_para = Paragraph::new(mem_usage_text).style(Style::default().fg(Theme::memory_color(memory_percentage)).bg(Theme::background()));
|
||||
frame.render_widget(mem_usage_para, content_chunks[1]);
|
||||
let mem_details_text = format!("Used: {} • Total: {}", self.used_gb.map_or("—".to_string(), |v| format!("{:.1}GB", v)), self.total_gb.map_or("—".to_string(), |v| format!("{:.1}GB", v)));
|
||||
let mem_details_para = Paragraph::new(mem_details_text).style(Style::default().fg(Theme::secondary_text()).bg(Theme::background()));
|
||||
frame.render_widget(mem_details_para, content_chunks[2]);
|
||||
}
|
||||
|
||||
fn get_name(&self) -> &str {
|
||||
"Memory"
|
||||
}
|
||||
|
||||
fn has_data(&self) -> bool {
|
||||
self.has_data
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for MemoryWidget {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
25
dashboard/src/ui/widgets/mod.rs
Normal file
25
dashboard/src/ui/widgets/mod.rs
Normal file
@@ -0,0 +1,25 @@
|
||||
use cm_dashboard_shared::Metric;
|
||||
use ratatui::{layout::Rect, Frame};
|
||||
|
||||
pub mod cpu;
|
||||
pub mod memory;
|
||||
pub mod services;
|
||||
|
||||
pub use cpu::CpuWidget;
|
||||
pub use memory::MemoryWidget;
|
||||
pub use services::ServicesWidget;
|
||||
|
||||
/// Widget trait for UI components that display metrics
|
||||
pub trait Widget {
|
||||
/// Update widget with new metrics data
|
||||
fn update_from_metrics(&mut self, metrics: &[&Metric]);
|
||||
|
||||
/// Render the widget to a terminal frame
|
||||
fn render(&mut self, frame: &mut Frame, area: Rect);
|
||||
|
||||
/// Get widget name for display
|
||||
fn get_name(&self) -> &str;
|
||||
|
||||
/// Check if widget has data to display
|
||||
fn has_data(&self) -> bool;
|
||||
}
|
||||
193
dashboard/src/ui/widgets/services.rs
Normal file
193
dashboard/src/ui/widgets/services.rs
Normal file
@@ -0,0 +1,193 @@
|
||||
use cm_dashboard_shared::{Metric, Status};
|
||||
use ratatui::{
|
||||
layout::{Constraint, Direction, Layout, Rect},
|
||||
style::{Color, Style},
|
||||
widgets::{Block, Borders, List, ListItem, Paragraph},
|
||||
Frame,
|
||||
};
|
||||
use std::collections::HashMap;
|
||||
use tracing::debug;
|
||||
|
||||
use super::Widget;
|
||||
use crate::ui::theme::Theme;
|
||||
|
||||
/// Services widget displaying individual systemd service statuses
|
||||
pub struct ServicesWidget {
|
||||
/// Individual service statuses
|
||||
services: HashMap<String, ServiceInfo>,
|
||||
/// Aggregated status
|
||||
status: Status,
|
||||
/// Last update indicator
|
||||
has_data: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct ServiceInfo {
|
||||
status: String,
|
||||
memory_mb: Option<f32>,
|
||||
disk_gb: Option<f32>,
|
||||
widget_status: Status,
|
||||
}
|
||||
|
||||
impl ServicesWidget {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
services: HashMap::new(),
|
||||
status: Status::Unknown,
|
||||
has_data: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get status color for display (btop-style)
|
||||
fn get_status_color(&self) -> Color {
|
||||
Theme::status_color(self.status)
|
||||
}
|
||||
|
||||
/// Extract service name from metric name
|
||||
fn extract_service_name(metric_name: &str) -> Option<String> {
|
||||
if metric_name.starts_with("service_") {
|
||||
if let Some(end_pos) = metric_name.rfind("_status")
|
||||
.or_else(|| metric_name.rfind("_memory_mb"))
|
||||
.or_else(|| metric_name.rfind("_disk_gb")) {
|
||||
let service_name = &metric_name[8..end_pos]; // Remove "service_" prefix
|
||||
return Some(service_name.to_string());
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Format service info for display
|
||||
fn format_service_info(&self, name: &str, info: &ServiceInfo) -> String {
|
||||
let status_icon = match info.widget_status {
|
||||
Status::Ok => "✅",
|
||||
Status::Warning => "⚠️",
|
||||
Status::Critical => "❌",
|
||||
Status::Unknown => "❓",
|
||||
};
|
||||
|
||||
let memory_str = if let Some(memory) = info.memory_mb {
|
||||
format!(" Mem:{:.1}MB", memory)
|
||||
} else {
|
||||
"".to_string()
|
||||
};
|
||||
|
||||
let disk_str = if let Some(disk) = info.disk_gb {
|
||||
format!(" Disk:{:.1}GB", disk)
|
||||
} else {
|
||||
"".to_string()
|
||||
};
|
||||
|
||||
format!("{} {} ({}){}{}", status_icon, name, info.status, memory_str, disk_str)
|
||||
}
|
||||
|
||||
/// Format service info in clean service list format
|
||||
fn format_btop_process_line(&self, name: &str, info: &ServiceInfo, _index: usize) -> 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("0G".to_string(), |d| format!("{:.1}G", d));
|
||||
|
||||
// Truncate long service names to fit layout
|
||||
let short_name = if name.len() > 23 {
|
||||
format!("{}...", &name[..20])
|
||||
} else {
|
||||
name.to_string()
|
||||
};
|
||||
|
||||
// Status with color indicator
|
||||
let status_str = match info.widget_status {
|
||||
Status::Ok => "✅ active",
|
||||
Status::Warning => "⚠️ inactive",
|
||||
Status::Critical => "❌ failed",
|
||||
Status::Unknown => "❓ unknown",
|
||||
};
|
||||
|
||||
format!("{:<25} {:<10} {:<8} {:<8}",
|
||||
short_name,
|
||||
status_str,
|
||||
memory_str,
|
||||
disk_str)
|
||||
}
|
||||
}
|
||||
|
||||
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(service_name) = Self::extract_service_name(&metric.name) {
|
||||
let service_info = self.services.entry(service_name).or_insert(ServiceInfo {
|
||||
status: "unknown".to_string(),
|
||||
memory_mb: None,
|
||||
disk_gb: None,
|
||||
widget_status: Status::Unknown,
|
||||
});
|
||||
|
||||
if metric.name.ends_with("_status") {
|
||||
service_info.status = metric.value.as_string();
|
||||
service_info.widget_status = metric.status;
|
||||
} else if metric.name.ends_with("_memory_mb") {
|
||||
if let Some(memory) = metric.value.as_f32() {
|
||||
service_info.memory_mb = Some(memory);
|
||||
}
|
||||
} else if metric.name.ends_with("_disk_gb") {
|
||||
if let Some(disk) = metric.value.as_f32() {
|
||||
service_info.disk_gb = Some(disk);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Aggregate status from all services
|
||||
let statuses: Vec<Status> = self.services.values()
|
||||
.map(|info| info.widget_status)
|
||||
.collect();
|
||||
|
||||
self.status = if statuses.is_empty() {
|
||||
Status::Unknown
|
||||
} else {
|
||||
Status::aggregate(&statuses)
|
||||
};
|
||||
|
||||
self.has_data = !self.services.is_empty();
|
||||
|
||||
debug!("Services widget updated: {} services, status={:?}",
|
||||
self.services.len(), self.status);
|
||||
}
|
||||
|
||||
fn render(&mut self, frame: &mut Frame, area: Rect) {
|
||||
let services_block = Block::default().title("services").borders(Borders::ALL).style(Style::default().fg(Theme::border()).bg(Theme::background())).title_style(Style::default().fg(Theme::primary_text()));
|
||||
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);
|
||||
let header = format!("{:<25} {:<10} {:<8} {:<8}", "Service:", "Status:", "MemB", "DiskGB");
|
||||
let header_para = Paragraph::new(header).style(Style::default().fg(Theme::muted_text()).bg(Theme::background()));
|
||||
frame.render_widget(header_para, content_chunks[0]);
|
||||
if self.services.is_empty() { let empty_text = Paragraph::new("No process data").style(Style::default().fg(Theme::muted_text()).bg(Theme::background())); frame.render_widget(empty_text, content_chunks[1]); return; }
|
||||
let mut services: Vec<_> = self.services.iter().collect();
|
||||
services.sort_by(|(_, a), (_, b)| b.memory_mb.unwrap_or(0.0).partial_cmp(&a.memory_mb.unwrap_or(0.0)).unwrap_or(std::cmp::Ordering::Equal));
|
||||
let available_lines = content_chunks[1].height as usize;
|
||||
let service_chunks = Layout::default().direction(Direction::Vertical).constraints(vec![Constraint::Length(1); available_lines.min(services.len())]).split(content_chunks[1]);
|
||||
for (i, (name, info)) in services.iter().take(available_lines).enumerate() {
|
||||
let service_line = self.format_btop_process_line(name, info, i);
|
||||
let color = match info.widget_status { Status::Ok => Theme::primary_text(), Status::Warning => Theme::warning(), Status::Critical => Theme::error(), Status::Unknown => Theme::muted_text(), };
|
||||
let service_para = Paragraph::new(service_line).style(Style::default().fg(color).bg(Theme::background()));
|
||||
frame.render_widget(service_para, service_chunks[i]);
|
||||
}
|
||||
}
|
||||
|
||||
fn get_name(&self) -> &str {
|
||||
"Services"
|
||||
}
|
||||
|
||||
fn has_data(&self) -> bool {
|
||||
self.has_data
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ServicesWidget {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user