Implement comprehensive backup monitoring and fix timestamp issues
- Add BackupCollector for reading TOML status files with disk space metrics - Implement BackupWidget with disk usage display and service status details - Fix backup script disk space parsing by adding missing capture_output=True - Update backup widget to show actual disk usage instead of repository size - Fix timestamp parsing to use backup completion time instead of start time - Resolve timezone issues by using UTC timestamps in backup script - Add disk identification metrics (product name, serial number) to backup status - Enhance UI layout with proper backup monitoring integration
This commit is contained in:
@@ -1,15 +1,13 @@
|
||||
use cm_dashboard_shared::{Metric, MetricValue, Status};
|
||||
use cm_dashboard_shared::{Metric, Status};
|
||||
use ratatui::{
|
||||
layout::{Constraint, Direction, Layout, Rect},
|
||||
style::{Color, Style},
|
||||
widgets::{Block, Borders, Gauge, Paragraph},
|
||||
text::{Line, Span},
|
||||
widgets::Paragraph,
|
||||
Frame,
|
||||
};
|
||||
use tracing::debug;
|
||||
|
||||
use super::Widget;
|
||||
use crate::ui::theme::Theme;
|
||||
use crate::ui::theme::{Theme, Typography, Components, StatusIcons};
|
||||
|
||||
/// Memory widget displaying usage, totals, and swap information
|
||||
pub struct MemoryWidget {
|
||||
@@ -54,44 +52,6 @@ impl MemoryWidget {
|
||||
}
|
||||
}
|
||||
|
||||
/// Get status color for display (btop-style)
|
||||
fn get_status_color(&self) -> Color {
|
||||
Theme::status_color(self.status)
|
||||
}
|
||||
|
||||
/// Format memory usage for display
|
||||
fn format_memory_usage(&self) -> String {
|
||||
match (self.used_gb, self.total_gb) {
|
||||
(Some(used), Some(total)) => {
|
||||
format!("{:.1}/{:.1} GB", used, total)
|
||||
}
|
||||
_ => "—/— GB".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Format swap usage for display
|
||||
fn format_swap_usage(&self) -> String {
|
||||
match (self.swap_used_gb, self.swap_total_gb) {
|
||||
(Some(used), Some(total)) => {
|
||||
if total > 0.0 {
|
||||
format!("{:.1}/{:.1} GB", used, total)
|
||||
} else {
|
||||
"No swap".to_string()
|
||||
}
|
||||
}
|
||||
_ => "—/— GB".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Format /tmp usage for display
|
||||
fn format_tmp_usage(&self) -> String {
|
||||
match (self.tmp_size_mb, self.tmp_total_mb) {
|
||||
(Some(used), Some(total)) => {
|
||||
format!("{:.1}/{:.0} MB", used, total)
|
||||
}
|
||||
_ => "—/— MB".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get memory usage percentage for gauge
|
||||
fn get_memory_percentage(&self) -> u16 {
|
||||
@@ -109,44 +69,66 @@ impl MemoryWidget {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Get swap usage percentage
|
||||
fn get_swap_percentage(&self) -> u16 {
|
||||
match (self.swap_used_gb, self.swap_total_gb) {
|
||||
(Some(used), Some(total)) if total > 0.0 => {
|
||||
let percent = (used / total * 100.0).min(100.0).max(0.0);
|
||||
percent as u16
|
||||
|
||||
/// Format size with proper units (kB/MB/GB)
|
||||
fn format_size_units(size_mb: f32) -> String {
|
||||
if size_mb >= 1024.0 {
|
||||
// Convert to GB
|
||||
let size_gb = size_mb / 1024.0;
|
||||
format!("{:.1}GB", size_gb)
|
||||
} else if size_mb >= 1.0 {
|
||||
// Show as MB
|
||||
format!("{:.0}MB", size_mb)
|
||||
} else if size_mb >= 0.001 {
|
||||
// Convert to kB
|
||||
let size_kb = size_mb * 1024.0;
|
||||
format!("{:.0}kB", size_kb)
|
||||
} else {
|
||||
// Show very small sizes in bytes
|
||||
let size_bytes = size_mb * 1024.0 * 1024.0;
|
||||
format!("{:.0}B", size_bytes)
|
||||
}
|
||||
}
|
||||
|
||||
/// Format /tmp usage as "xx% yyykB/MB/GB/zzzGB"
|
||||
fn format_tmp_usage(&self) -> String {
|
||||
match (self.tmp_usage_percent, self.tmp_size_mb, self.tmp_total_mb) {
|
||||
(Some(percent), Some(used_mb), Some(total_mb)) => {
|
||||
let used_str = Self::format_size_units(used_mb);
|
||||
let total_str = Self::format_size_units(total_mb);
|
||||
format!("{:.1}% {}/{}", percent, used_str, total_str)
|
||||
}
|
||||
_ => 0,
|
||||
(Some(percent), Some(used_mb), None) => {
|
||||
let used_str = Self::format_size_units(used_mb);
|
||||
format!("{:.1}% {}", percent, used_str)
|
||||
}
|
||||
(None, Some(used_mb), Some(total_mb)) => {
|
||||
let used_str = Self::format_size_units(used_mb);
|
||||
let total_str = Self::format_size_units(total_mb);
|
||||
format!("{}/{}", used_str, total_str)
|
||||
}
|
||||
(None, Some(used_mb), None) => {
|
||||
Self::format_size_units(used_mb)
|
||||
}
|
||||
_ => "—".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// Get tmp status based on usage percentage
|
||||
fn get_tmp_status(&self) -> Status {
|
||||
if let Some(tmp_percent) = self.tmp_usage_percent {
|
||||
if tmp_percent >= 90.0 {
|
||||
Status::Critical
|
||||
} else if tmp_percent >= 70.0 {
|
||||
Status::Warning
|
||||
} else {
|
||||
Status::Ok
|
||||
}
|
||||
} else {
|
||||
Status::Unknown
|
||||
}
|
||||
}
|
||||
|
||||
/// Create btop-style dotted bar pattern (same as CPU)
|
||||
fn create_btop_dotted_bar(&self, percentage: u16, width: usize) -> String {
|
||||
let filled = (width * percentage as usize) / 100;
|
||||
let empty = width.saturating_sub(filled);
|
||||
|
||||
// Real btop uses these patterns:
|
||||
// High usage: ████████ (solid blocks)
|
||||
// Medium usage: :::::::: (colons)
|
||||
// Low usage: ........ (dots)
|
||||
// Empty: (spaces)
|
||||
|
||||
let pattern = if percentage >= 75 {
|
||||
"█" // High usage - solid blocks
|
||||
} else if percentage >= 25 {
|
||||
":" // Medium usage - colons like btop
|
||||
} else if percentage > 0 {
|
||||
"." // Low usage - dots like btop
|
||||
} else {
|
||||
" " // No usage - spaces
|
||||
};
|
||||
|
||||
let filled_chars = pattern.repeat(filled);
|
||||
let empty_chars = " ".repeat(empty);
|
||||
|
||||
filled_chars + &empty_chars
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for MemoryWidget {
|
||||
@@ -231,23 +213,22 @@ impl Widget for MemoryWidget {
|
||||
|
||||
fn render(&mut self, frame: &mut Frame, area: Rect) {
|
||||
let content_chunks = Layout::default().direction(Direction::Vertical).constraints([Constraint::Length(1), Constraint::Length(1), Constraint::Length(1)]).split(area);
|
||||
let mem_title = Paragraph::new("Memory:").style(Style::default().fg(Theme::primary_text()).bg(Theme::background()));
|
||||
let mem_title = Paragraph::new("RAM:").style(Typography::widget_title());
|
||||
frame.render_widget(mem_title, content_chunks[0]);
|
||||
let memory_percentage = self.get_memory_percentage();
|
||||
let mem_usage_text = format!("Usage: {} {:>3}%", self.create_btop_dotted_bar(memory_percentage, 20), memory_percentage);
|
||||
let mem_usage_para = Paragraph::new(mem_usage_text).style(Style::default().fg(Theme::memory_color(memory_percentage)).bg(Theme::background()));
|
||||
frame.render_widget(mem_usage_para, content_chunks[1]);
|
||||
let mem_details_text = format!("Used: {} • Total: {}", self.used_gb.map_or("—".to_string(), |v| format!("{:.1}GB", v)), self.total_gb.map_or("—".to_string(), |v| format!("{:.1}GB", v)));
|
||||
let mem_details_para = Paragraph::new(mem_details_text).style(Style::default().fg(Theme::secondary_text()).bg(Theme::background()));
|
||||
frame.render_widget(mem_details_para, content_chunks[2]);
|
||||
}
|
||||
|
||||
fn get_name(&self) -> &str {
|
||||
"Memory"
|
||||
}
|
||||
|
||||
fn has_data(&self) -> bool {
|
||||
self.has_data
|
||||
|
||||
// Format used and total memory with smart units, percentage, and status icon
|
||||
let used_str = self.used_gb.map_or("—".to_string(), |v| Self::format_size_units(v * 1024.0)); // Convert GB to MB for formatting
|
||||
let total_str = self.total_gb.map_or("—".to_string(), |v| Self::format_size_units(v * 1024.0)); // Convert GB to MB for formatting
|
||||
let percentage = self.get_memory_percentage();
|
||||
let mem_details_spans = StatusIcons::create_status_spans(self.status, &format!("Used: {}% {}/{}", percentage, used_str, total_str));
|
||||
let mem_details_para = Paragraph::new(ratatui::text::Line::from(mem_details_spans));
|
||||
frame.render_widget(mem_details_para, content_chunks[1]);
|
||||
|
||||
// /tmp usage line with status icon
|
||||
let tmp_status = self.get_tmp_status();
|
||||
let tmp_spans = StatusIcons::create_status_spans(tmp_status, &format!("tmp: {}", self.format_tmp_usage()));
|
||||
let tmp_para = Paragraph::new(ratatui::text::Line::from(tmp_spans));
|
||||
frame.render_widget(tmp_para, content_chunks[2]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user