Implement per-host widget cache for instant host switching

Resolves widget data persistence issue where switching hosts left stale data
from the previous host displayed in widgets.

Key improvements:
- Add Clone derives to all widget structs (CpuWidget, MemoryWidget,
  ServicesWidget, BackupWidget)
- Create HostWidgets struct to cache widget states per hostname
- Update TuiApp with HashMap<String, HostWidgets> for per-host storage
- Fix borrowing issues by cloning hostname before mutable self borrow
- Implement instant widget state restoration when switching hosts

Tab key host switching now displays cached widget data for each host
without stale information persistence between switches.
This commit is contained in:
Christoffer Martinsson 2025-10-18 19:54:08 +02:00
parent 46cc813a68
commit 4b7d08153c
5 changed files with 72 additions and 35 deletions

View File

@ -6,6 +6,7 @@ use ratatui::{
widgets::{Block, Paragraph},
Frame,
};
use std::collections::HashMap;
use std::time::Instant;
use tracing::info;
@ -17,24 +18,43 @@ use crate::metrics::{MetricStore, WidgetType};
use cm_dashboard_shared::{Metric, Status};
use theme::{Theme, Layout as ThemeLayout, Typography, Components, StatusIcons};
/// Widget states for a specific host
#[derive(Clone)]
pub struct HostWidgets {
/// CPU widget state
pub cpu_widget: CpuWidget,
/// Memory widget state
pub memory_widget: MemoryWidget,
/// Services widget state
pub services_widget: ServicesWidget,
/// Backup widget state
pub backup_widget: BackupWidget,
/// Last update time for this host
pub last_update: Option<Instant>,
}
impl HostWidgets {
pub fn new() -> Self {
Self {
cpu_widget: CpuWidget::new(),
memory_widget: MemoryWidget::new(),
services_widget: ServicesWidget::new(),
backup_widget: BackupWidget::new(),
last_update: None,
}
}
}
/// Main TUI application
pub struct TuiApp {
/// CPU widget
cpu_widget: CpuWidget,
/// Memory widget
memory_widget: MemoryWidget,
/// Services widget
services_widget: ServicesWidget,
/// Backup widget
backup_widget: BackupWidget,
/// Widget states per host (hostname -> HostWidgets)
host_widgets: HashMap<String, HostWidgets>,
/// Current active host
current_host: Option<String>,
/// Available hosts
available_hosts: Vec<String>,
/// Host index for navigation
host_index: usize,
/// Last update time
last_update: Option<Instant>,
/// Should quit application
should_quit: bool,
}
@ -42,45 +62,44 @@ pub struct TuiApp {
impl TuiApp {
pub fn new() -> Self {
Self {
cpu_widget: CpuWidget::new(),
memory_widget: MemoryWidget::new(),
services_widget: ServicesWidget::new(),
backup_widget: BackupWidget::new(),
host_widgets: HashMap::new(),
current_host: None,
available_hosts: Vec::new(),
host_index: 0,
last_update: None,
should_quit: false,
}
}
/// Get or create host widgets for the given hostname
fn get_or_create_host_widgets(&mut self, hostname: &str) -> &mut HostWidgets {
self.host_widgets.entry(hostname.to_string()).or_insert_with(HostWidgets::new)
}
/// Update widgets with metrics from store
pub fn update_metrics(&mut self, metric_store: &MetricStore) {
if let Some(ref hostname) = self.current_host {
// Update CPU widget
let cpu_metrics = metric_store.get_metrics_for_widget(hostname, WidgetType::Cpu);
self.cpu_widget.update_from_metrics(&cpu_metrics);
// Update Memory widget
let memory_metrics = metric_store.get_metrics_for_widget(hostname, WidgetType::Memory);
self.memory_widget.update_from_metrics(&memory_metrics);
// Update Services widget - get all metrics that start with "service_"
let all_metrics = metric_store.get_metrics_for_host(hostname);
if let Some(hostname) = self.current_host.clone() {
// Get metrics first while hostname is borrowed
let cpu_metrics = metric_store.get_metrics_for_widget(&hostname, WidgetType::Cpu);
let memory_metrics = metric_store.get_metrics_for_widget(&hostname, WidgetType::Memory);
let all_metrics = metric_store.get_metrics_for_host(&hostname);
let service_metrics: Vec<&Metric> = all_metrics.iter()
.filter(|m| m.name.starts_with("service_"))
.copied()
.collect();
self.services_widget.update_from_metrics(&service_metrics);
// Update Backup widget - get all metrics that start with "backup_"
let all_backup_metrics: Vec<&Metric> = all_metrics.iter()
.filter(|m| m.name.starts_with("backup_"))
.copied()
.collect();
self.backup_widget.update_from_metrics(&all_backup_metrics);
self.last_update = Some(Instant::now());
// Now get host widgets and update them
let host_widgets = self.get_or_create_host_widgets(&hostname);
host_widgets.cpu_widget.update_from_metrics(&cpu_metrics);
host_widgets.memory_widget.update_from_metrics(&memory_metrics);
host_widgets.services_widget.update_from_metrics(&service_metrics);
host_widgets.backup_widget.update_from_metrics(&all_backup_metrics);
host_widgets.last_update = Some(Instant::now());
}
}
@ -184,7 +203,12 @@ impl TuiApp {
// Render new panel layout
self.render_system_panel(frame, left_chunks[0], metric_store);
self.render_backup_panel(frame, left_chunks[1]);
self.services_widget.render(frame, content_chunks[1]); // Services takes full right side
// Render services widget for current host
if let Some(hostname) = self.current_host.clone() {
let host_widgets = self.get_or_create_host_widgets(&hostname);
host_widgets.services_widget.render(frame, content_chunks[1]); // Services takes full right side
}
}
/// Render btop-style minimal title
@ -241,8 +265,12 @@ impl TuiApp {
])
.split(inner_area);
self.cpu_widget.render(frame, content_chunks[0]);
self.memory_widget.render(frame, content_chunks[1]);
// Get current host widgets, create if none exist
if let Some(hostname) = self.current_host.clone() {
let host_widgets = self.get_or_create_host_widgets(&hostname);
host_widgets.cpu_widget.render(frame, content_chunks[0]);
host_widgets.memory_widget.render(frame, content_chunks[1]);
}
self.render_storage_section(frame, content_chunks[2], metric_store);
}
@ -250,7 +278,12 @@ impl TuiApp {
let backup_block = Components::widget_block("backup");
let inner_area = backup_block.inner(area);
frame.render_widget(backup_block, area);
self.backup_widget.render(frame, inner_area);
// Get current host widgets for backup widget
if let Some(hostname) = self.current_host.clone() {
let host_widgets = self.get_or_create_host_widgets(&hostname);
host_widgets.backup_widget.render(frame, inner_area);
}
}

View File

@ -10,6 +10,7 @@ use super::Widget;
use crate::ui::theme::{Theme, Typography, Components, StatusIcons};
/// Backup widget displaying backup status, services, and repository information
#[derive(Clone)]
pub struct BackupWidget {
/// Overall backup status
overall_status: Status,

View File

@ -10,6 +10,7 @@ use super::Widget;
use crate::ui::theme::{Theme, Typography, Components, StatusIcons};
/// CPU widget displaying load, temperature, and frequency
#[derive(Clone)]
pub struct CpuWidget {
/// CPU load averages (1, 5, 15 minutes)
load_1min: Option<f32>,

View File

@ -10,6 +10,7 @@ use super::Widget;
use crate::ui::theme::{Theme, Typography, Components, StatusIcons};
/// Memory widget displaying usage, totals, and swap information
#[derive(Clone)]
pub struct MemoryWidget {
/// Memory usage percentage
usage_percent: Option<f32>,

View File

@ -12,6 +12,7 @@ use crate::ui::theme::{Theme, Typography, Components, StatusIcons};
use ratatui::style::Style;
/// Services widget displaying hierarchical systemd service statuses
#[derive(Clone)]
pub struct ServicesWidget {
/// Parent services (nginx, docker, etc.)
parent_services: HashMap<String, ServiceInfo>,