diff --git a/dashboard/src/ui/widgets/backup.rs b/dashboard/src/ui/widgets/backup.rs index 82ba22f..552de97 100644 --- a/dashboard/src/ui/widgets/backup.rs +++ b/dashboard/src/ui/widgets/backup.rs @@ -1,6 +1,6 @@ use cm_dashboard_shared::{Metric, Status}; use ratatui::{ - layout::{Constraint, Direction, Layout, Rect}, + layout::Rect, widgets::Paragraph, Frame, }; @@ -340,138 +340,103 @@ impl Widget for BackupWidget { } fn render(&mut self, frame: &mut Frame, area: Rect) { - // Split area into header and services list - let chunks = Layout::default() - .direction(Direction::Vertical) - .constraints([ - Constraint::Length(6), // Header with "Latest backup" title, status, P/N, and S/N - Constraint::Min(0), // Service list - ]) - .split(area); + let mut lines = Vec::new(); - // Render backup overview - self.render_backup_overview(frame, chunks[0]); + // Latest backup section + lines.push(ratatui::text::Line::from(vec![ + ratatui::text::Span::styled("Latest backup:", Typography::widget_title()) + ])); + + // Timestamp with status icon + let timestamp_text = if let Some(timestamp) = self.last_run_timestamp { + self.format_timestamp(timestamp) + } else { + "Unknown".to_string() + }; + let timestamp_spans = StatusIcons::create_status_spans( + self.overall_status, + ×tamp_text + ); + lines.push(ratatui::text::Line::from(timestamp_spans)); - // Render services list - self.render_services_list(frame, chunks[1]); + // Duration as sub-item + if let Some(duration) = self.duration_seconds { + let duration_text = self.format_duration(duration); + lines.push(ratatui::text::Line::from(vec![ + ratatui::text::Span::raw(" └─ "), + ratatui::text::Span::styled(format!("Duration: {}", duration_text), Typography::secondary()) + ])); + } + + // Disk section + lines.push(ratatui::text::Line::from(vec![ + ratatui::text::Span::styled("Disk:", Typography::widget_title()) + ])); + + // Disk product name with status + if let Some(product) = &self.backup_disk_product_name { + let disk_spans = StatusIcons::create_status_spans( + Status::Ok, // Assuming disk is OK if we have data + product + ); + lines.push(ratatui::text::Line::from(disk_spans)); + + // Serial number as sub-item + if let Some(serial) = &self.backup_disk_serial_number { + lines.push(ratatui::text::Line::from(vec![ + ratatui::text::Span::raw(" ├─ "), + ratatui::text::Span::styled(format!("S/N: {}", serial), Typography::secondary()) + ])); + } + + // Usage as sub-item + if let (Some(used), Some(total)) = (self.backup_disk_used_gb, self.backup_disk_total_gb) { + let used_str = Self::format_size_with_proper_units(used); + let total_str = Self::format_size_with_proper_units(total); + lines.push(ratatui::text::Line::from(vec![ + ratatui::text::Span::raw(" └─ "), + ratatui::text::Span::styled(format!("Usage: {}/{}", used_str, total_str), Typography::secondary()) + ])); + } + } + + // Repos section + lines.push(ratatui::text::Line::from(vec![ + ratatui::text::Span::styled("Repos:", Typography::widget_title()) + ])); + + // Repository list + for service in &self.service_metrics { + if let (Some(archives), Some(size_gb)) = (service.archive_count, service.repo_size_gb) { + let size_str = Self::format_size_with_proper_units(size_gb); + let repo_text = format!("{} ({}) {}", service.name, archives, size_str); + let repo_spans = StatusIcons::create_status_spans(service.status, &repo_text); + lines.push(ratatui::text::Line::from(repo_spans)); + } + } + + let paragraph = Paragraph::new(ratatui::text::Text::from(lines)); + frame.render_widget(paragraph, area); } } impl BackupWidget { - /// Render backup overview section - fn render_backup_overview(&self, frame: &mut Frame, area: Rect) { - let content_chunks = Layout::default() - .direction(Direction::Vertical) - .constraints([ - Constraint::Length(1), // "Latest backup" header - Constraint::Length(1), // Status line - Constraint::Length(1), // Duration and last run - Constraint::Length(1), // Repository size - Constraint::Length(1), // Product name - Constraint::Length(1), // Serial number - Constraint::Min(0), // Remaining space - ]) - .split(area); - - // "Latest backup" header - let header_para = Paragraph::new("Latest backup:").style(Typography::widget_title()); - frame.render_widget(header_para, content_chunks[0]); - - // Status line - let status_text = format!( - "Status: {}", - match self.overall_status { - Status::Ok => "OK", - Status::Pending => "Pending", - Status::Warning => "Warning", - Status::Critical => "Failed", - Status::Unknown => "Unknown", - } - ); - let status_spans = StatusIcons::create_status_spans(self.overall_status, &status_text); - let status_para = Paragraph::new(ratatui::text::Line::from(status_spans)); - frame.render_widget(status_para, content_chunks[1]); - - // Duration and last run - let time_text = format!( - "Duration: {} • Last: {}", - self.format_duration(), - self.format_last_run() - ); - let time_para = Paragraph::new(time_text).style(Typography::secondary()); - frame.render_widget(time_para, content_chunks[2]); - - // Repository size - let size_text = format!("Disk usage: {}", self.format_repo_size()); - let size_para = Paragraph::new(size_text).style(Typography::secondary()); - frame.render_widget(size_para, content_chunks[3]); - - // Product name - let product_text = self.format_product_name(); - let product_para = Paragraph::new(product_text).style(Typography::secondary()); - frame.render_widget(product_para, content_chunks[4]); - - // Serial number - let serial_text = self.format_serial_number(); - let serial_para = Paragraph::new(serial_text).style(Typography::secondary()); - frame.render_widget(serial_para, content_chunks[5]); + /// Format timestamp for display + fn format_timestamp(&self, timestamp: i64) -> String { + let datetime = chrono::DateTime::from_timestamp(timestamp, 0) + .unwrap_or_else(|| chrono::Utc::now()); + datetime.format("%Y-%m-%d %H:%M:%S").to_string() } - /// Render services list section - fn render_services_list(&self, frame: &mut Frame, area: Rect) { - if area.height < 1 { - return; - } - - let available_lines = area.height as usize; - let services_to_show = self.service_metrics.iter().take(available_lines); - - let mut y_offset = 0; - for service in services_to_show { - if y_offset >= available_lines { - break; - } - - let service_area = Rect { - x: area.x, - y: area.y + y_offset as u16, - width: area.width, - height: 1, - }; - - let service_info = if let (Some(archives), Some(size_gb)) = - (service.archive_count, service.repo_size_gb) - { - let size_str = Self::format_size_with_proper_units(size_gb); - format!(" {} archives {}", archives, size_str) - } else { - String::new() - }; - - let service_text = format!("{}{}", service.name, service_info); - let service_spans = StatusIcons::create_status_spans(service.status, &service_text); - let service_para = Paragraph::new(ratatui::text::Line::from(service_spans)); - - frame.render_widget(service_para, service_area); - y_offset += 1; - } - - // If there are more services than we can show, indicate that - if self.service_metrics.len() > available_lines { - let more_count = self.service_metrics.len() - available_lines; - if available_lines > 0 { - let last_line_area = Rect { - x: area.x, - y: area.y + (available_lines - 1) as u16, - width: area.width, - height: 1, - }; - - let more_text = format!("... and {} more services", more_count); - let more_para = Paragraph::new(more_text).style(Typography::muted()); - - frame.render_widget(more_para, last_line_area); - } + /// Format duration in seconds to human readable format + fn format_duration(&self, duration_seconds: i64) -> String { + let minutes = duration_seconds / 60; + let seconds = duration_seconds % 60; + + if minutes > 0 { + format!("{}.{}m", minutes, seconds / 6) // Show 1 decimal for minutes + } else { + format!("{}s", seconds) } } } diff --git a/dashboard/src/ui/widgets/system.rs b/dashboard/src/ui/widgets/system.rs index eb8173b..35b2815 100644 --- a/dashboard/src/ui/widgets/system.rs +++ b/dashboard/src/ui/widgets/system.rs @@ -254,7 +254,11 @@ impl SystemWidget { _ => "—% —GB/—GB".to_string(), }; - let pool_label = format!("{} ({}):", pool.mount_point, pool.pool_type); + let pool_label = if pool.pool_type.to_lowercase() == "single" { + format!("{}:", pool.mount_point) + } else { + format!("{} ({}):", pool.mount_point, pool.pool_type) + }; let pool_spans = StatusIcons::create_status_spans( pool.status.clone(), &pool_label