This commit is contained in:
Christoffer Martinsson 2025-10-13 09:57:43 +02:00
parent d76302e1c4
commit 42aaebf6a7
4 changed files with 160 additions and 152 deletions

View File

@ -149,18 +149,57 @@ impl SystemCollector {
} }
if total_time > 0 && !cstate_times.is_empty() { if total_time > 0 && !cstate_times.is_empty() {
// Sort by time spent (highest first) // Sort by C-state order: POLL, C1, C1E, C3, C6, C7s, C8, C9, C10
cstate_times.sort_by(|a, b| b.1.cmp(&a.1)); cstate_times.sort_by(|a, b| {
let order_a = match a.0.as_str() {
"POLL" => 0,
"C1" => 1,
"C1E" => 2,
"C3" => 3,
"C6" => 4,
"C7s" => 5,
"C8" => 6,
"C9" => 7,
"C10" => 8,
_ => 99,
};
let order_b = match b.0.as_str() {
"POLL" => 0,
"C1" => 1,
"C1E" => 2,
"C3" => 3,
"C6" => 4,
"C7s" => 5,
"C8" => 6,
"C9" => 7,
"C10" => 8,
_ => 99,
};
order_a.cmp(&order_b)
});
// Format all C-states with percentages // Format C-states as description lines (split into two rows for readability)
let mut result = Vec::new(); let mut result = Vec::new();
let mut current_line = Vec::new();
for (name, time) in cstate_times { for (name, time) in cstate_times {
let percent = (time as f32 / total_time as f32) * 100.0; let percent = (time as f32 / total_time as f32) * 100.0;
if percent >= 0.1 { // Only show states with at least 0.1% time if percent >= 0.1 { // Only show states with at least 0.1% time
result.push(format!("{}: {:.1}%", name, percent)); current_line.push(format!("{}: {:.1}%", name, percent));
// Split into two rows when we have 4 items
if current_line.len() == 4 {
result.push(current_line.join(", "));
current_line.clear();
}
} }
} }
// Add remaining items as second line
if !current_line.is_empty() {
result.push(current_line.join(", "));
}
return Some(result); return Some(result);
} }
} }

View File

@ -475,8 +475,26 @@ impl App {
targets.push(HostTarget::from_name(local)); targets.push(HostTarget::from_name(local));
} }
} }
} else if let Some(local) = local_host { } else {
targets.push(HostTarget::from_name(local)); // No config file - use auto-discovery with known CMTEC hosts
let known_hosts = vec![
"cmbox", "labbox", "simonbox", "steambox", "srv01"
];
if let Some(local) = local_host.as_ref() {
targets.push(HostTarget::from_name(local.clone()));
}
// Add all known hosts for auto-discovery
for hostname in known_hosts {
if targets
.iter()
.any(|existing| existing.name.eq_ignore_ascii_case(hostname))
{
continue;
}
targets.push(HostTarget::from_name(hostname.to_string()));
}
} }
if targets.is_empty() { if targets.is_empty() {

View File

@ -49,102 +49,6 @@ fn render_metrics(
// Use agent-calculated statuses // Use agent-calculated statuses
let memory_status = status_level_from_agent_status(summary.memory_status.as_ref()); 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()); let cpu_status = status_level_from_agent_status(summary.cpu_status.as_ref());
// Dashboard should NOT calculate colors - agent is the source of truth
// Memory dataset - use agent-calculated status
let mut memory_dataset = WidgetDataSet::new(vec!["Memory usage".to_string()], Some(WidgetStatus::new(memory_status)));
memory_dataset.add_row(
Some(WidgetStatus::new(memory_status)),
vec![],
vec![format!("{:.1} / {:.1} GB", summary.memory_used_mb / 1000.0, summary.memory_total_mb / 1000.0)],
);
// CPU dataset - use agent-calculated status
let mut cpu_dataset = WidgetDataSet::new(vec!["CPU load".to_string(), "CPU temp".to_string()], Some(WidgetStatus::new(cpu_status)));
cpu_dataset.add_row(
Some(WidgetStatus::new(cpu_status)),
vec![],
vec![
format!("{:.2}{:.2}{:.2}", summary.cpu_load_1, summary.cpu_load_5, summary.cpu_load_15),
format_optional_metric(summary.cpu_temp_c, "°C"),
],
);
// C-state dataset - all C-states as columns in one row, ordered properly
let mut cstate_headers = Vec::new();
let mut cstate_values = Vec::new();
if let Some(cstates) = summary.cpu_cstate.as_ref() {
// Parse all C-states first
let mut parsed_cstates: Vec<(String, String)> = Vec::new();
for cstate_info in cstates {
if let Some((state, percent)) = cstate_info.split_once(": ") {
parsed_cstates.push((state.to_string(), percent.to_string()));
}
}
// Sort by C-state order: POLL, C1, C1E, C3, C6, C7s, C8, C9, C10
parsed_cstates.sort_by(|a, b| {
let order_a = match a.0.as_str() {
"POLL" => 0,
"C1" => 1,
"C1E" => 2,
"C3" => 3,
"C6" => 4,
"C7s" => 5,
"C8" => 6,
"C9" => 7,
"C10" => 8,
_ => 99,
};
let order_b = match b.0.as_str() {
"POLL" => 0,
"C1" => 1,
"C1E" => 2,
"C3" => 3,
"C6" => 4,
"C7s" => 5,
"C8" => 6,
"C9" => 7,
"C10" => 8,
_ => 99,
};
order_a.cmp(&order_b)
});
// Take all available C-states (or limit to fit display)
for (state, percent) in parsed_cstates.into_iter().take(8) {
cstate_headers.push(state);
cstate_values.push(percent);
}
}
let mut cstate_dataset = WidgetDataSet::new(cstate_headers, Some(WidgetStatus::new(StatusLevel::Ok)));
if !cstate_values.is_empty() {
cstate_dataset.add_row(
Some(WidgetStatus::new(StatusLevel::Ok)),
vec![],
cstate_values,
);
} else {
cstate_dataset.add_row(
Some(WidgetStatus::new(StatusLevel::Unknown)),
vec![],
vec!["No data".to_string()],
);
}
// GPU dataset - GPU data remains in ServiceMetrics, not SystemMetrics
let gpu_status = StatusLevel::Unknown; // GPU not available in SystemMetrics
let mut gpu_dataset = WidgetDataSet::new(vec!["GPU load".to_string(), "GPU temp".to_string()], Some(WidgetStatus::new(gpu_status)));
gpu_dataset.add_row(
Some(WidgetStatus::new(gpu_status)),
vec![],
vec![
"".to_string(), // GPU data not in SystemMetrics
"".to_string(), // GPU data not in SystemMetrics
],
);
// Determine overall widget status based on worst case from agent statuses // Determine overall widget status based on worst case from agent statuses
let overall_status_level = match (memory_status, cpu_status) { let overall_status_level = match (memory_status, cpu_status) {
@ -155,8 +59,27 @@ fn render_metrics(
}; };
let overall_status = Some(WidgetStatus::new(overall_status_level)); let overall_status = Some(WidgetStatus::new(overall_status_level));
// Render all datasets in a single combined widget // Single dataset with RAM, CPU load, CPU temp as columns
render_combined_widget_data(frame, area, "System".to_string(), overall_status, vec![memory_dataset, cpu_dataset, cstate_dataset, gpu_dataset]); 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 as description (agent decides what goes in descriptions)
let cstate_description = summary.cpu_cstate.clone().unwrap_or_default();
system_dataset.add_row(
overall_status.clone(),
cstate_description,
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 { fn format_optional_metric(value: Option<f32>, unit: &str) -> String {

View File

@ -286,63 +286,91 @@ fn render_dataset_with_wrapping(frame: &mut Frame, dataset: &WidgetDataSet, inne
break; break;
} }
let mut cells = vec![]; // Check if this is a sub-service - if so, render as full-width row
if row.sub_service.is_some() && col_start == 0 {
// Status cell (only show on first section) // Sub-service: render as full-width spanning row
if col_start == 0 { let is_last_sub_service = is_last_sub_service_in_group(&dataset.rows, row_idx, &row.sub_service);
match &row.status { let tree_char = if is_last_sub_service { "└─" } else { "├─" };
let service_name = row.values.get(0).cloned().unwrap_or_default();
let status_icon = match &row.status {
Some(s) => { Some(s) => {
let color = s.status.to_color(); let color = s.status.to_color();
let icon = s.status.to_icon(); let icon = s.status.to_icon();
cells.push(Cell::from(Line::from(vec![Span::styled( Span::styled(icon.to_string(), Style::default().fg(color))
icon.to_string(),
Style::default().fg(color),
)])));
}, },
None => cells.push(Cell::from("")), None => Span::raw(""),
} };
} else {
cells.push(Cell::from("")); let full_content = format!("{} {}", tree_char, service_name);
} let full_cell = Cell::from(Line::from(vec![
status_icon,
// Data cells for this section Span::raw(" "),
for col_idx in col_start..col_end { Span::styled(full_content, neutral_text_style()),
if let Some(content) = row.values.get(col_idx) { ]));
if content.is_empty() {
cells.push(Cell::from("")); let full_row = Row::new(vec![full_cell]);
} else { let full_constraints = vec![Constraint::Length(inner.width)];
// Check if this is the first column (service name) and if it's a sub-service let full_table = Table::new(vec![full_row])
let display_content = if col_idx == 0 && row.sub_service.is_some() { .widths(&full_constraints)
// Determine if this is the last sub-service for this parent .style(neutral_text_style());
let is_last_sub_service = is_last_sub_service_in_group(&dataset.rows, row_idx, &row.sub_service);
let tree_char = if is_last_sub_service { "└─" } else { "├─" }; frame.render_widget(full_table, Rect {
format!("{} {}", tree_char, content) x: inner.x,
} else { y: current_y,
content.to_string() width: inner.width,
}; height: 1,
});
cells.push(Cell::from(Line::from(vec![Span::styled( } else if row.sub_service.is_none() {
display_content, // Regular service: render with columns as normal
neutral_text_style(), let mut cells = vec![];
)])));
// Status cell (only show on first section)
if col_start == 0 {
match &row.status {
Some(s) => {
let color = s.status.to_color();
let icon = s.status.to_icon();
cells.push(Cell::from(Line::from(vec![Span::styled(
icon.to_string(),
Style::default().fg(color),
)])));
},
None => cells.push(Cell::from("")),
} }
} else { } else {
cells.push(Cell::from("")); cells.push(Cell::from(""));
} }
// Data cells for this section
for col_idx in col_start..col_end {
if let Some(content) = row.values.get(col_idx) {
if content.is_empty() {
cells.push(Cell::from(""));
} else {
cells.push(Cell::from(Line::from(vec![Span::styled(
content.to_string(),
neutral_text_style(),
)])));
}
} else {
cells.push(Cell::from(""));
}
}
let data_row = Row::new(cells);
let data_table = Table::new(vec![data_row])
.widths(&constraints)
.column_spacing(col_spacing)
.style(neutral_text_style());
frame.render_widget(data_table, Rect {
x: inner.x,
y: current_y,
width: inner.width,
height: 1,
});
} }
let data_row = Row::new(cells);
let data_table = Table::new(vec![data_row])
.widths(&constraints)
.column_spacing(col_spacing)
.style(neutral_text_style());
frame.render_widget(data_table, Rect {
x: inner.x,
y: current_y,
width: inner.width,
height: 1,
});
current_y += 1; current_y += 1;
// Render description rows if any exist // Render description rows if any exist