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:
2025-10-18 18:33:41 +02:00
parent 8a36472a3d
commit 125111ee99
19 changed files with 2788 additions and 1020 deletions

View File

@@ -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};
/// CPU widget displaying load, temperature, and frequency
pub struct CpuWidget {
@@ -40,11 +38,6 @@ impl CpuWidget {
}
}
/// Get status color for display (btop-style)
fn get_status_color(&self) -> Color {
Theme::status_color(self.status)
}
/// Format load average for display
fn format_load(&self) -> String {
match (self.load_1min, self.load_5min, self.load_15min) {
@@ -55,14 +48,6 @@ impl CpuWidget {
}
}
/// Format temperature for display
fn format_temperature(&self) -> String {
match self.temperature {
Some(temp) => format!("{:.1}°C", temp),
None => "—°C".to_string(),
}
}
/// Format frequency for display
fn format_frequency(&self) -> String {
match self.frequency {
@@ -70,45 +55,7 @@ impl CpuWidget {
None => "— MHz".to_string(),
}
}
/// Get load percentage for gauge (based on load_1min)
fn get_load_percentage(&self) -> u16 {
match self.load_1min {
Some(load) => {
// Assume 8-core system, so 100% = load of 8.0
let percentage = (load / 8.0 * 100.0).min(100.0).max(0.0);
percentage as u16
}
None => 0,
}
}
/// Create btop-style dotted bar pattern (like real btop)
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 CpuWidget {
@@ -168,27 +115,16 @@ impl Widget for CpuWidget {
}
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 cpu_title = Paragraph::new("CPU:").style(Style::default().fg(Theme::primary_text()).bg(Theme::background()));
let content_chunks = Layout::default().direction(Direction::Vertical).constraints([Constraint::Length(1), Constraint::Length(1)]).split(area);
let cpu_title = Paragraph::new("CPU:").style(Typography::widget_title());
frame.render_widget(cpu_title, content_chunks[0]);
let overall_usage = self.get_load_percentage();
let cpu_usage_text = format!("Usage: {} {:>3}%", self.create_btop_dotted_bar(overall_usage, 20), overall_usage);
let cpu_usage_para = Paragraph::new(cpu_usage_text).style(Style::default().fg(Theme::cpu_color(overall_usage)).bg(Theme::background()));
frame.render_widget(cpu_usage_para, content_chunks[1]);
let load_freq_text = format!("Load: {}{}", self.format_load(), self.format_frequency());
let load_freq_para = Paragraph::new(load_freq_text).style(Style::default().fg(Theme::secondary_text()).bg(Theme::background()));
frame.render_widget(load_freq_para, content_chunks[2]);
}
fn get_name(&self) -> &str {
"CPU"
}
fn has_data(&self) -> bool {
self.has_data
let load_freq_spans = StatusIcons::create_status_spans(self.status, &format!("Load: {}{}", self.format_load(), self.format_frequency()));
let load_freq_para = Paragraph::new(ratatui::text::Line::from(load_freq_spans));
frame.render_widget(load_freq_para, content_chunks[1]);
}
}
impl Default for CpuWidget {
fn default() -> Self {
Self::new()