Services widget: - Remove SB (sandbox) column and related formatting function - Fix quota formatting to show decimals when needed (1.5G not 1G) - Remove spaces in unit display (128MB not 128 MB) Storage widget: - Change usage format to 23GB (932GB) for better readability Documentation: - Add NixOS configuration update process to CLAUDE.md
143 lines
4.5 KiB
Rust
143 lines
4.5 KiB
Rust
use ratatui::layout::Rect;
|
|
use ratatui::Frame;
|
|
|
|
use crate::app::HostDisplayData;
|
|
use crate::data::metrics::SmartMetrics;
|
|
use crate::ui::widget::{render_placeholder, render_widget_data, status_level_from_agent_status, connection_status_message, WidgetData, 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.smart.as_ref()) {
|
|
(ConnectionStatus::Connected, Some(metrics)) => {
|
|
render_metrics(frame, data, metrics, area);
|
|
}
|
|
(ConnectionStatus::Connected, None) => {
|
|
render_placeholder(
|
|
frame,
|
|
area,
|
|
"Storage",
|
|
&format!("Host {} has no SMART data yet", data.name),
|
|
);
|
|
}
|
|
(status, _) => {
|
|
render_placeholder(
|
|
frame,
|
|
area,
|
|
"Storage",
|
|
&format!("Host {}: {}", data.name, connection_status_message(status, &data.last_error)),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
None => render_placeholder(frame, area, "Storage", "No hosts configured"),
|
|
}
|
|
}
|
|
|
|
fn render_metrics(frame: &mut Frame, _host: &HostDisplayData, metrics: &SmartMetrics, area: Rect) {
|
|
let title = "Storage".to_string();
|
|
|
|
let widget_status = status_level_from_agent_status(Some(&metrics.status));
|
|
|
|
let mut data = WidgetData::new(
|
|
title,
|
|
Some(WidgetStatus::new(widget_status)),
|
|
vec!["Name".to_string(), "Temp".to_string(), "Wear".to_string(), "Usage".to_string()]
|
|
);
|
|
|
|
if metrics.drives.is_empty() {
|
|
data.add_row(
|
|
None,
|
|
vec![],
|
|
vec![
|
|
"No drives reported".to_string(),
|
|
"".to_string(),
|
|
"".to_string(),
|
|
"".to_string(),
|
|
],
|
|
);
|
|
} else {
|
|
for drive in &metrics.drives {
|
|
let status_level = drive_status_level(metrics, &drive.name);
|
|
|
|
// Use agent-provided descriptions (agent is source of truth)
|
|
let mut description = drive.description.clone().unwrap_or_default();
|
|
|
|
// Add drive-specific issues as additional description lines
|
|
for issue in &metrics.issues {
|
|
if issue.to_lowercase().contains(&drive.name.to_lowercase()) {
|
|
description.push(format!("Issue: {}", issue));
|
|
}
|
|
}
|
|
|
|
data.add_row(
|
|
Some(WidgetStatus::new(status_level)),
|
|
description,
|
|
vec![
|
|
drive.name.clone(),
|
|
format_temperature(drive.temperature_c),
|
|
format_percent(drive.wear_level),
|
|
format_usage(drive.used_gb, drive.capacity_gb),
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
render_widget_data(frame, area, data);
|
|
}
|
|
|
|
|
|
fn format_temperature(value: f32) -> String {
|
|
if value.abs() < f32::EPSILON {
|
|
"—".to_string()
|
|
} else {
|
|
format!("{:.0}°C", value)
|
|
}
|
|
}
|
|
|
|
fn format_percent(value: f32) -> String {
|
|
if value.abs() < f32::EPSILON {
|
|
"—".to_string()
|
|
} else {
|
|
format!("{:.0}%", value)
|
|
}
|
|
}
|
|
|
|
|
|
|
|
fn format_usage(used: Option<f32>, capacity: Option<f32>) -> String {
|
|
match (used, capacity) {
|
|
(Some(used_gb), Some(total_gb)) if used_gb > 0.0 && total_gb > 0.0 => {
|
|
format!("{:.0}GB ({:.0}GB)", used_gb, total_gb)
|
|
}
|
|
(Some(used_gb), None) if used_gb > 0.0 => {
|
|
format!("{:.0}GB", used_gb)
|
|
}
|
|
(None, Some(total_gb)) if total_gb > 0.0 => {
|
|
format!("— ({:.0}GB)", total_gb)
|
|
}
|
|
_ => "—".to_string(),
|
|
}
|
|
}
|
|
|
|
fn drive_status_level(metrics: &SmartMetrics, drive_name: &str) -> StatusLevel {
|
|
if metrics.summary.critical > 0
|
|
|| metrics.issues.iter().any(|issue| {
|
|
issue.to_lowercase().contains(&drive_name.to_lowercase())
|
|
&& issue.to_lowercase().contains("fail")
|
|
})
|
|
{
|
|
StatusLevel::Error
|
|
} else if metrics.summary.warning > 0
|
|
|| metrics
|
|
.issues
|
|
.iter()
|
|
.any(|issue| issue.to_lowercase().contains(&drive_name.to_lowercase()))
|
|
{
|
|
StatusLevel::Warning
|
|
} else {
|
|
StatusLevel::Ok
|
|
}
|
|
}
|