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, capacity: Option) -> 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 } }