Files
cm-dashboard/dashboard/src/ui/system.rs
Christoffer Martinsson f3b6d12f68 Add top CPU and RAM process monitoring to System widget
- Implement get_top_cpu_process() and get_top_ram_process() functions in SystemCollector
- Add top_cpu_process and top_ram_process fields to SystemSummary data structure
- Update System widget to display top processes as description rows
- Show process name and percentage usage for highest CPU and RAM consumers
- Skip kernel threads and filter out processes with minimal usage (<0.1%)
2025-10-14 21:47:52 +02:00

125 lines
4.4 KiB
Rust

use ratatui::layout::Rect;
use ratatui::Frame;
use crate::app::HostDisplayData;
use crate::data::metrics::SystemMetrics;
use crate::ui::widget::{
render_placeholder, render_combined_widget_data,
status_level_from_agent_status, connection_status_message, WidgetDataSet, 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.system.as_ref()) {
(ConnectionStatus::Connected, Some(metrics)) => {
render_metrics(frame, data, metrics, area);
}
(ConnectionStatus::Connected, None) => {
render_placeholder(
frame,
area,
"System",
&format!("Host {} awaiting system metrics", data.name),
);
}
(status, _) => {
render_placeholder(
frame,
area,
"System",
&format!("Host {}: {}", data.name, connection_status_message(status, &data.last_error)),
);
}
}
}
None => render_placeholder(frame, area, "System", "No hosts configured"),
}
}
fn render_metrics(
frame: &mut Frame,
_host: &HostDisplayData,
metrics: &SystemMetrics,
area: Rect,
) {
let summary = &metrics.summary;
// Use agent-calculated statuses
let memory_status = status_level_from_agent_status(summary.memory_status.as_ref());
let cpu_status = status_level_from_agent_status(summary.cpu_status.as_ref());
// Determine overall widget status based on worst case from agent statuses
let overall_status_level = match (memory_status, cpu_status) {
(StatusLevel::Error, _) | (_, StatusLevel::Error) => StatusLevel::Error,
(StatusLevel::Warning, _) | (_, StatusLevel::Warning) => StatusLevel::Warning,
(StatusLevel::Ok, StatusLevel::Ok) => StatusLevel::Ok,
_ => StatusLevel::Unknown,
};
let overall_status = Some(WidgetStatus::new(overall_status_level));
// Single dataset with RAM, CPU load, CPU temp as columns
let mut system_dataset = WidgetDataSet::new(
vec!["RAM usage".to_string(), "CPU load".to_string(), "CPU temp".to_string()],
overall_status.clone()
);
// Use agent-provided C-states and logged-in users as description
let mut description_lines = Vec::new();
// Add C-states with prefix on first line, indent subsequent lines
if let Some(cstates) = &summary.cpu_cstate {
for (i, cstate_line) in cstates.iter().enumerate() {
if i == 0 {
description_lines.push(format!("C-State: {}", cstate_line));
} else {
description_lines.push(format!(" {}", cstate_line));
}
}
}
// Add logged-in users to description
if let Some(users) = &summary.logged_in_users {
if !users.is_empty() {
let user_line = if users.len() == 1 {
format!("Logged in: {}", users[0])
} else {
format!("Logged in: {} users ({})", users.len(), users.join(", "))
};
description_lines.push(user_line);
}
}
// Add top CPU process
if let Some(cpu_proc) = &summary.top_cpu_process {
description_lines.push(format!("Top CPU: {}", cpu_proc));
}
// Add top RAM process
if let Some(ram_proc) = &summary.top_ram_process {
description_lines.push(format!("Top RAM: {}", ram_proc));
}
system_dataset.add_row(
overall_status.clone(),
description_lines,
vec![
format!("{:.1} / {:.1} GB", summary.memory_used_mb / 1000.0, summary.memory_total_mb / 1000.0),
format!("{:.2}{:.2}{:.2}", summary.cpu_load_1, summary.cpu_load_5, summary.cpu_load_15),
format_optional_metric(summary.cpu_temp_c, "°C"),
],
);
// Render single dataset
render_combined_widget_data(frame, area, "System".to_string(), overall_status, vec![system_dataset]);
}
fn format_optional_metric(value: Option<f32>, unit: &str) -> String {
match value {
Some(number) => format!("{:.1}{}", number, unit),
None => "".to_string(),
}
}