Remove unused code and eliminate compiler warnings
- Remove unused fields from CommandStatus variants - Clean up unused methods and unused collector fields - Fix lifetime syntax warning in SystemWidget - Delete unused cache module completely - Remove redundant render methods from widgets All agent and dashboard warnings eliminated while preserving panel switching and scrolling functionality.
This commit is contained in:
parent
8dd943e8f1
commit
4b54a59e35
@ -86,9 +86,9 @@ impl Agent {
|
||||
}
|
||||
}
|
||||
_ = transmission_interval.tick() => {
|
||||
// Send all cached metrics via ZMQ every 1 second
|
||||
if let Err(e) = self.broadcast_all_cached_metrics().await {
|
||||
error!("Failed to broadcast cached metrics: {}", e);
|
||||
// Send all metrics via ZMQ every 1 second
|
||||
if let Err(e) = self.broadcast_all_metrics().await {
|
||||
error!("Failed to broadcast metrics: {}", e);
|
||||
}
|
||||
}
|
||||
_ = notification_interval.tick() => {
|
||||
@ -152,34 +152,34 @@ impl Agent {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn broadcast_all_cached_metrics(&mut self) -> Result<()> {
|
||||
debug!("Broadcasting all cached metrics via ZMQ");
|
||||
async fn broadcast_all_metrics(&mut self) -> Result<()> {
|
||||
debug!("Broadcasting all metrics via ZMQ");
|
||||
|
||||
// Get all cached metrics from the metric manager
|
||||
let mut cached_metrics = self.metric_manager.get_all_cached_metrics().await?;
|
||||
// Get all current metrics from collectors
|
||||
let mut metrics = self.metric_manager.collect_all_metrics().await?;
|
||||
|
||||
// Add the host status summary metric from status manager
|
||||
let host_status_metric = self.host_status_manager.get_host_status_metric();
|
||||
cached_metrics.push(host_status_metric);
|
||||
metrics.push(host_status_metric);
|
||||
|
||||
if cached_metrics.is_empty() {
|
||||
debug!("No cached metrics to broadcast");
|
||||
if metrics.is_empty() {
|
||||
debug!("No metrics to broadcast");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
debug!("Broadcasting {} cached metrics (including host status summary)", cached_metrics.len());
|
||||
debug!("Broadcasting {} metrics (including host status summary)", metrics.len());
|
||||
|
||||
// Create and send message with all cached data
|
||||
let message = MetricMessage::new(self.hostname.clone(), cached_metrics);
|
||||
// Create and send message with all current data
|
||||
let message = MetricMessage::new(self.hostname.clone(), metrics);
|
||||
self.zmq_handler.publish_metrics(&message).await?;
|
||||
|
||||
debug!("Cached metrics broadcasted successfully");
|
||||
debug!("Metrics broadcasted successfully");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn process_metrics(&mut self, metrics: &[Metric]) {
|
||||
for metric in metrics {
|
||||
self.host_status_manager.process_metric(metric, &mut self.notification_manager, self.metric_manager.get_cache_manager()).await;
|
||||
self.host_status_manager.process_metric(metric, &mut self.notification_manager).await;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
10
agent/src/cache/cached_metric.rs
vendored
10
agent/src/cache/cached_metric.rs
vendored
@ -1,10 +0,0 @@
|
||||
use cm_dashboard_shared::Metric;
|
||||
use std::time::Instant;
|
||||
|
||||
/// A cached metric with metadata
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CachedMetric {
|
||||
pub metric: Metric,
|
||||
pub collected_at: Instant,
|
||||
pub access_count: u64,
|
||||
}
|
||||
33
agent/src/cache/manager.rs
vendored
33
agent/src/cache/manager.rs
vendored
@ -1,33 +0,0 @@
|
||||
use super::ConfigurableCache;
|
||||
use cm_dashboard_shared::{CacheConfig, Metric};
|
||||
use std::sync::Arc;
|
||||
use tracing::info;
|
||||
|
||||
/// Manages metric caching with background tasks
|
||||
pub struct MetricCacheManager {
|
||||
cache: Arc<ConfigurableCache>,
|
||||
}
|
||||
|
||||
impl MetricCacheManager {
|
||||
pub fn new(config: CacheConfig) -> Self {
|
||||
let cache = Arc::new(ConfigurableCache::new(config.clone()));
|
||||
|
||||
Self { cache }
|
||||
}
|
||||
|
||||
/// Start background cache management tasks
|
||||
pub async fn start_background_tasks(&self) {
|
||||
// Temporarily disabled to isolate CPU usage issue
|
||||
info!("Cache manager background tasks disabled for debugging");
|
||||
}
|
||||
|
||||
/// Store metric in cache
|
||||
pub async fn cache_metric(&self, metric: Metric) {
|
||||
self.cache.store_metric(metric).await;
|
||||
}
|
||||
|
||||
/// Get all cached metrics (including expired ones) for broadcasting
|
||||
pub async fn get_all_cached_metrics(&self) -> Vec<Metric> {
|
||||
self.cache.get_all_cached_metrics().await
|
||||
}
|
||||
}
|
||||
94
agent/src/cache/mod.rs
vendored
94
agent/src/cache/mod.rs
vendored
@ -1,94 +0,0 @@
|
||||
use cm_dashboard_shared::{CacheConfig, Metric};
|
||||
use std::collections::HashMap;
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::RwLock;
|
||||
use tracing::{info, warn, debug};
|
||||
|
||||
/// Simple persistent cache for metrics
|
||||
pub struct SimpleCache {
|
||||
metrics: RwLock<HashMap<String, Metric>>,
|
||||
persist_path: String,
|
||||
}
|
||||
|
||||
impl SimpleCache {
|
||||
pub fn new(config: CacheConfig) -> Self {
|
||||
let cache = Self {
|
||||
metrics: RwLock::new(HashMap::new()),
|
||||
persist_path: config.persist_path,
|
||||
};
|
||||
|
||||
// Clear cache file on startup to ensure fresh data
|
||||
cache.clear_cache_file();
|
||||
cache
|
||||
}
|
||||
|
||||
/// Store metric in cache
|
||||
pub async fn store_metric(&self, metric: Metric) {
|
||||
let mut metrics = self.metrics.write().await;
|
||||
metrics.insert(metric.name.clone(), metric);
|
||||
}
|
||||
|
||||
/// Get all cached metrics
|
||||
pub async fn get_all_cached_metrics(&self) -> Vec<Metric> {
|
||||
let metrics = self.metrics.read().await;
|
||||
metrics.values().cloned().collect()
|
||||
}
|
||||
|
||||
/// Save cache to disk
|
||||
pub async fn save_to_disk(&self) {
|
||||
// Cache persistence disabled to prevent stale data issues during debugging
|
||||
debug!("Cache persistence disabled - not saving to disk");
|
||||
}
|
||||
|
||||
/// Load cache from disk (DISABLED)
|
||||
fn load_from_disk(&self) {
|
||||
// Cache loading disabled to prevent stale data issues during debugging
|
||||
info!("Cache loading disabled - starting with fresh cache");
|
||||
}
|
||||
|
||||
/// Clear cache file on startup to ensure fresh data
|
||||
fn clear_cache_file(&self) {
|
||||
if Path::new(&self.persist_path).exists() {
|
||||
match fs::remove_file(&self.persist_path) {
|
||||
Ok(_) => info!("Cleared cache file {} on startup", self.persist_path),
|
||||
Err(e) => warn!("Failed to clear cache file {}: {}", self.persist_path, e),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct MetricCacheManager {
|
||||
cache: Arc<SimpleCache>,
|
||||
}
|
||||
|
||||
impl MetricCacheManager {
|
||||
pub fn new(config: CacheConfig) -> Self {
|
||||
Self {
|
||||
cache: Arc::new(SimpleCache::new(config)),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn store_metric(&self, metric: Metric) {
|
||||
self.cache.store_metric(metric).await;
|
||||
}
|
||||
|
||||
pub async fn cache_metric(&self, metric: Metric) {
|
||||
self.store_metric(metric).await;
|
||||
}
|
||||
|
||||
pub async fn start_background_tasks(&self) {
|
||||
// No background tasks needed for simple cache
|
||||
}
|
||||
|
||||
pub async fn get_all_cached_metrics(&self) -> Result<Vec<Metric>, anyhow::Error> {
|
||||
Ok(self.cache.get_all_cached_metrics().await)
|
||||
}
|
||||
|
||||
pub async fn save_to_disk(&self) {
|
||||
self.cache.save_to_disk().await;
|
||||
}
|
||||
}
|
||||
@ -107,9 +107,6 @@ impl BackupCollector {
|
||||
|
||||
#[async_trait]
|
||||
impl Collector for BackupCollector {
|
||||
fn name(&self) -> &str {
|
||||
"backup"
|
||||
}
|
||||
|
||||
async fn collect(&self, _status_tracker: &mut StatusTracker) -> Result<Vec<Metric>, CollectorError> {
|
||||
let backup_status_option = self.read_backup_status().await?;
|
||||
|
||||
@ -15,7 +15,6 @@ use crate::config::CpuConfig;
|
||||
/// - No process spawning
|
||||
/// - <0.1ms collection time target
|
||||
pub struct CpuCollector {
|
||||
name: String,
|
||||
load_thresholds: HysteresisThresholds,
|
||||
temperature_thresholds: HysteresisThresholds,
|
||||
}
|
||||
@ -34,7 +33,6 @@ impl CpuCollector {
|
||||
);
|
||||
|
||||
Self {
|
||||
name: "cpu".to_string(),
|
||||
load_thresholds,
|
||||
temperature_thresholds,
|
||||
}
|
||||
@ -197,9 +195,6 @@ impl CpuCollector {
|
||||
|
||||
#[async_trait]
|
||||
impl Collector for CpuCollector {
|
||||
fn name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
|
||||
async fn collect(&self, status_tracker: &mut StatusTracker) -> Result<Vec<Metric>, CollectorError> {
|
||||
debug!("Collecting CPU metrics");
|
||||
|
||||
@ -325,33 +325,6 @@ impl DiskCollector {
|
||||
Some(device_name.to_string())
|
||||
}
|
||||
|
||||
/// Get directory size using du command (efficient for single directory)
|
||||
fn get_directory_size(&self, path: &str) -> Result<u64> {
|
||||
let output = Command::new("du")
|
||||
.arg("-s")
|
||||
.arg("--block-size=1")
|
||||
.arg(path)
|
||||
.output()?;
|
||||
|
||||
// du returns success even with permission denied warnings in stderr
|
||||
// We only care if the command completely failed or produced no stdout
|
||||
let output_str = String::from_utf8(output.stdout)?;
|
||||
|
||||
if output_str.trim().is_empty() {
|
||||
return Err(anyhow::anyhow!(
|
||||
"du command produced no output for {}",
|
||||
path
|
||||
));
|
||||
}
|
||||
|
||||
let size_str = output_str
|
||||
.split_whitespace()
|
||||
.next()
|
||||
.ok_or_else(|| anyhow::anyhow!("Failed to parse du output"))?;
|
||||
|
||||
let size_bytes = size_str.parse::<u64>()?;
|
||||
Ok(size_bytes)
|
||||
}
|
||||
|
||||
/// Get filesystem info using df command
|
||||
fn get_filesystem_info(&self, path: &str) -> Result<(u64, u64)> {
|
||||
@ -382,23 +355,6 @@ impl DiskCollector {
|
||||
Ok((total_bytes, used_bytes))
|
||||
}
|
||||
|
||||
/// Calculate status based on usage percentage
|
||||
fn calculate_usage_status(&self, used_bytes: u64, total_bytes: u64) -> Status {
|
||||
if total_bytes == 0 {
|
||||
return Status::Unknown;
|
||||
}
|
||||
|
||||
let usage_percent = (used_bytes as f64 / total_bytes as f64) * 100.0;
|
||||
|
||||
// Thresholds for disk usage
|
||||
if usage_percent >= 95.0 {
|
||||
Status::Critical
|
||||
} else if usage_percent >= 85.0 {
|
||||
Status::Warning
|
||||
} else {
|
||||
Status::Ok
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse size string (e.g., "120G", "45M") to GB value
|
||||
fn parse_size_to_gb(&self, size_str: &str) -> f32 {
|
||||
@ -435,9 +391,6 @@ impl DiskCollector {
|
||||
|
||||
#[async_trait]
|
||||
impl Collector for DiskCollector {
|
||||
fn name(&self) -> &str {
|
||||
"disk"
|
||||
}
|
||||
|
||||
async fn collect(&self, status_tracker: &mut StatusTracker) -> Result<Vec<Metric>, CollectorError> {
|
||||
let start_time = Instant::now();
|
||||
|
||||
@ -15,7 +15,6 @@ use crate::config::MemoryConfig;
|
||||
/// - No regex or complex parsing
|
||||
/// - <0.1ms collection time target
|
||||
pub struct MemoryCollector {
|
||||
name: String,
|
||||
usage_thresholds: HysteresisThresholds,
|
||||
}
|
||||
|
||||
@ -42,7 +41,6 @@ impl MemoryCollector {
|
||||
);
|
||||
|
||||
Self {
|
||||
name: "memory".to_string(),
|
||||
usage_thresholds,
|
||||
}
|
||||
}
|
||||
@ -284,9 +282,6 @@ impl MemoryCollector {
|
||||
|
||||
#[async_trait]
|
||||
impl Collector for MemoryCollector {
|
||||
fn name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
|
||||
async fn collect(&self, status_tracker: &mut StatusTracker) -> Result<Vec<Metric>, CollectorError> {
|
||||
debug!("Collecting memory metrics");
|
||||
|
||||
@ -16,9 +16,6 @@ pub use error::CollectorError;
|
||||
/// Base trait for all collectors with extreme efficiency requirements
|
||||
#[async_trait]
|
||||
pub trait Collector: Send + Sync {
|
||||
/// Name of this collector
|
||||
fn name(&self) -> &str;
|
||||
|
||||
/// Collect all metrics this collector provides
|
||||
async fn collect(&self, status_tracker: &mut StatusTracker) -> Result<Vec<Metric>, CollectorError>;
|
||||
|
||||
|
||||
@ -12,12 +12,11 @@ use crate::config::NixOSConfig;
|
||||
/// - NixOS version and build information
|
||||
/// - Currently active/logged in users
|
||||
pub struct NixOSCollector {
|
||||
config: NixOSConfig,
|
||||
}
|
||||
|
||||
impl NixOSCollector {
|
||||
pub fn new(config: NixOSConfig) -> Self {
|
||||
Self { config }
|
||||
pub fn new(_config: NixOSConfig) -> Self {
|
||||
Self {}
|
||||
}
|
||||
|
||||
/// Get NixOS build information
|
||||
@ -116,9 +115,6 @@ impl NixOSCollector {
|
||||
|
||||
#[async_trait]
|
||||
impl Collector for NixOSCollector {
|
||||
fn name(&self) -> &str {
|
||||
"nixos"
|
||||
}
|
||||
|
||||
async fn collect(&self, _status_tracker: &mut StatusTracker) -> Result<Vec<Metric>, CollectorError> {
|
||||
debug!("Collecting NixOS system information");
|
||||
|
||||
@ -42,7 +42,6 @@ struct ServiceStatusInfo {
|
||||
load_state: String,
|
||||
active_state: String,
|
||||
sub_state: String,
|
||||
description: String,
|
||||
}
|
||||
|
||||
impl SystemdCollector {
|
||||
@ -170,18 +169,12 @@ impl SystemdCollector {
|
||||
let load_state = fields.get(1).unwrap_or(&"unknown").to_string();
|
||||
let active_state = fields.get(2).unwrap_or(&"unknown").to_string();
|
||||
let sub_state = fields.get(3).unwrap_or(&"unknown").to_string();
|
||||
let description = if fields.len() > 4 {
|
||||
fields[4..].join(" ")
|
||||
} else {
|
||||
"".to_string()
|
||||
};
|
||||
|
||||
// Cache the status information
|
||||
status_cache.insert(service_name.to_string(), ServiceStatusInfo {
|
||||
load_state: load_state.clone(),
|
||||
active_state: active_state.clone(),
|
||||
sub_state: sub_state.clone(),
|
||||
description,
|
||||
});
|
||||
|
||||
all_service_names.insert(service_name.to_string());
|
||||
@ -432,9 +425,6 @@ impl SystemdCollector {
|
||||
|
||||
#[async_trait]
|
||||
impl Collector for SystemdCollector {
|
||||
fn name(&self) -> &str {
|
||||
"systemd"
|
||||
}
|
||||
|
||||
async fn collect(&self, _status_tracker: &mut StatusTracker) -> Result<Vec<Metric>, CollectorError> {
|
||||
let start_time = Instant::now();
|
||||
|
||||
@ -4,7 +4,6 @@ use tracing::{error, info};
|
||||
use tracing_subscriber::EnvFilter;
|
||||
|
||||
mod agent;
|
||||
mod cache;
|
||||
mod collectors;
|
||||
mod communication;
|
||||
mod config;
|
||||
|
||||
@ -1,26 +1,21 @@
|
||||
use anyhow::Result;
|
||||
use cm_dashboard_shared::{Metric, StatusTracker};
|
||||
use std::collections::HashMap;
|
||||
use std::time::Instant;
|
||||
use tracing::{debug, error, info};
|
||||
use tracing::{error, info};
|
||||
|
||||
use crate::cache::MetricCacheManager;
|
||||
use crate::collectors::{
|
||||
backup::BackupCollector, cpu::CpuCollector, disk::DiskCollector, memory::MemoryCollector,
|
||||
nixos::NixOSCollector, systemd::SystemdCollector, Collector,
|
||||
};
|
||||
use crate::config::{AgentConfig, CollectorConfig};
|
||||
|
||||
/// Manages all metric collectors with intelligent caching
|
||||
/// Manages all metric collectors
|
||||
pub struct MetricCollectionManager {
|
||||
collectors: Vec<Box<dyn Collector>>,
|
||||
cache_manager: MetricCacheManager,
|
||||
last_collection_times: HashMap<String, Instant>,
|
||||
status_tracker: StatusTracker,
|
||||
}
|
||||
|
||||
impl MetricCollectionManager {
|
||||
pub async fn new(config: &CollectorConfig, agent_config: &AgentConfig) -> Result<Self> {
|
||||
pub async fn new(config: &CollectorConfig, _agent_config: &AgentConfig) -> Result<Self> {
|
||||
let mut collectors: Vec<Box<dyn Collector>> = Vec::new();
|
||||
|
||||
// Benchmark mode - only enable specific collector based on env var
|
||||
@ -109,153 +104,37 @@ impl MetricCollectionManager {
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize cache manager with configuration
|
||||
let cache_manager = MetricCacheManager::new(agent_config.cache.clone());
|
||||
|
||||
// Start background cache tasks
|
||||
cache_manager.start_background_tasks().await;
|
||||
|
||||
info!(
|
||||
"Metric collection manager initialized with {} collectors and caching enabled",
|
||||
"Metric collection manager initialized with {} collectors",
|
||||
collectors.len()
|
||||
);
|
||||
|
||||
Ok(Self {
|
||||
collectors,
|
||||
cache_manager,
|
||||
last_collection_times: HashMap::new(),
|
||||
status_tracker: StatusTracker::new(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Force collection from ALL collectors immediately (used at startup)
|
||||
pub async fn collect_all_metrics_force(&mut self) -> Result<Vec<Metric>> {
|
||||
let mut all_metrics = Vec::new();
|
||||
let now = Instant::now();
|
||||
|
||||
info!(
|
||||
"Force collecting from ALL {} collectors for startup",
|
||||
self.collectors.len()
|
||||
);
|
||||
|
||||
// Force collection from every collector regardless of intervals
|
||||
for collector in &self.collectors {
|
||||
let collector_name = collector.name();
|
||||
|
||||
match collector.collect(&mut self.status_tracker).await {
|
||||
Ok(metrics) => {
|
||||
info!(
|
||||
"Force collected {} metrics from {} collector",
|
||||
metrics.len(),
|
||||
collector_name
|
||||
);
|
||||
|
||||
// Cache all new metrics
|
||||
for metric in &metrics {
|
||||
self.cache_manager.cache_metric(metric.clone()).await;
|
||||
}
|
||||
|
||||
all_metrics.extend(metrics);
|
||||
self.last_collection_times
|
||||
.insert(collector_name.to_string(), now);
|
||||
}
|
||||
Err(e) => {
|
||||
error!(
|
||||
"Collector '{}' failed during force collection: {}",
|
||||
collector_name, e
|
||||
);
|
||||
// Continue with other collectors even if one fails
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
info!(
|
||||
"Force collection completed: {} total metrics cached",
|
||||
all_metrics.len()
|
||||
);
|
||||
Ok(all_metrics)
|
||||
self.collect_all_metrics().await
|
||||
}
|
||||
|
||||
/// Collect metrics from all collectors with intelligent caching
|
||||
/// Collect metrics from all collectors
|
||||
pub async fn collect_all_metrics(&mut self) -> Result<Vec<Metric>> {
|
||||
let mut all_metrics = Vec::new();
|
||||
let now = Instant::now();
|
||||
|
||||
// Collecting metrics from collectors (debug logging disabled for performance)
|
||||
|
||||
// Keep track of which collector types we're collecting fresh data from
|
||||
let mut collecting_fresh = std::collections::HashSet::new();
|
||||
|
||||
// For each collector, check if we need to collect based on time intervals
|
||||
for collector in &self.collectors {
|
||||
let collector_name = collector.name();
|
||||
|
||||
// Determine cache interval for this collector type based on data volatility
|
||||
let cache_interval_secs = match collector_name {
|
||||
"cpu" | "memory" => 5, // Fast updates for volatile metrics
|
||||
"systemd" => 30, // Service status changes less frequently
|
||||
"disk" => 300, // SMART data changes very slowly (5 minutes)
|
||||
"backup" => 600, // Backup status changes rarely (10 minutes)
|
||||
_ => 30, // Default: moderate frequency
|
||||
};
|
||||
|
||||
let should_collect =
|
||||
if let Some(last_time) = self.last_collection_times.get(collector_name) {
|
||||
now.duration_since(*last_time).as_secs() >= cache_interval_secs
|
||||
} else {
|
||||
true // First collection
|
||||
};
|
||||
|
||||
if should_collect {
|
||||
collecting_fresh.insert(collector_name.to_string());
|
||||
match collector.collect(&mut self.status_tracker).await {
|
||||
Ok(metrics) => {
|
||||
// Collector returned fresh metrics (debug logging disabled for performance)
|
||||
|
||||
// Cache all new metrics
|
||||
for metric in &metrics {
|
||||
self.cache_manager.cache_metric(metric.clone()).await;
|
||||
}
|
||||
|
||||
all_metrics.extend(metrics);
|
||||
self.last_collection_times
|
||||
.insert(collector_name.to_string(), now);
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Collector '{}' failed: {}", collector_name, e);
|
||||
// Continue with other collectors even if one fails
|
||||
}
|
||||
match collector.collect(&mut self.status_tracker).await {
|
||||
Ok(metrics) => {
|
||||
all_metrics.extend(metrics);
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Collector failed: {}", e);
|
||||
}
|
||||
} else {
|
||||
let _elapsed = self
|
||||
.last_collection_times
|
||||
.get(collector_name)
|
||||
.map(|t| now.duration_since(*t).as_secs())
|
||||
.unwrap_or(0);
|
||||
// Collector skipped (debug logging disabled for performance)
|
||||
}
|
||||
}
|
||||
|
||||
// For 2-second intervals, skip cached metrics to avoid duplicates
|
||||
// (Cache system disabled for realtime updates)
|
||||
|
||||
// Collected metrics total (debug logging disabled for performance)
|
||||
Ok(all_metrics)
|
||||
}
|
||||
|
||||
|
||||
/// Get all cached metrics from the cache manager
|
||||
pub async fn get_all_cached_metrics(&self) -> Result<Vec<Metric>> {
|
||||
let cached_metrics = self.cache_manager.get_all_cached_metrics().await?;
|
||||
debug!(
|
||||
"Retrieved {} cached metrics for broadcast",
|
||||
cached_metrics.len()
|
||||
);
|
||||
Ok(cached_metrics)
|
||||
}
|
||||
|
||||
pub fn get_cache_manager(&self) -> &MetricCacheManager {
|
||||
&self.cache_manager
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -70,7 +70,7 @@ impl HostStatusManager {
|
||||
|
||||
/// Update the status of a specific service and recalculate host status
|
||||
/// Updates real-time status and buffers changes for email notifications
|
||||
pub fn update_service_status(&mut self, service: String, status: Status, cache_manager: Option<&crate::cache::MetricCacheManager>) {
|
||||
pub fn update_service_status(&mut self, service: String, status: Status) {
|
||||
if !self.config.enabled {
|
||||
return;
|
||||
}
|
||||
@ -82,14 +82,6 @@ impl HostStatusManager {
|
||||
return;
|
||||
}
|
||||
|
||||
// Save cache when status changes (clone cache manager reference for async)
|
||||
if let Some(cache) = cache_manager {
|
||||
let cache = cache.clone();
|
||||
tokio::spawn(async move {
|
||||
cache.save_to_disk().await;
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize batch if this is the first change
|
||||
if self.batch_start_time.is_none() {
|
||||
self.batch_start_time = Some(Instant::now());
|
||||
@ -169,9 +161,9 @@ impl HostStatusManager {
|
||||
|
||||
|
||||
/// Process a metric - updates status (notifications handled separately via batching)
|
||||
pub async fn process_metric(&mut self, metric: &Metric, _notification_manager: &mut crate::notifications::NotificationManager, cache_manager: &crate::cache::MetricCacheManager) {
|
||||
pub async fn process_metric(&mut self, metric: &Metric, _notification_manager: &mut crate::notifications::NotificationManager) {
|
||||
// Just update status - notifications are handled by process_pending_notifications
|
||||
self.update_service_status(metric.name.clone(), metric.status, Some(cache_manager));
|
||||
self.update_service_status(metric.name.clone(), metric.status);
|
||||
}
|
||||
|
||||
/// Process pending notifications - call this at notification intervals
|
||||
|
||||
@ -332,21 +332,6 @@ impl Dashboard {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get current service status from metrics to determine start/stop action
|
||||
fn get_service_status(&self, hostname: &str, service_name: &str) -> Option<String> {
|
||||
let metrics = self.metric_store.get_metrics_for_host(hostname);
|
||||
|
||||
// Look for systemd service status metric
|
||||
for metric in metrics {
|
||||
if metric.name == format!("systemd_{}_status", service_name) {
|
||||
if let cm_dashboard_shared::MetricValue::String(status) = &metric.value {
|
||||
return Some(status.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Dashboard {
|
||||
|
||||
@ -15,7 +15,7 @@ pub mod widgets;
|
||||
|
||||
use crate::metrics::MetricStore;
|
||||
use cm_dashboard_shared::{Metric, Status};
|
||||
use theme::{Components, Layout as ThemeLayout, StatusIcons, Theme, Typography};
|
||||
use theme::{Components, Layout as ThemeLayout, Theme, Typography};
|
||||
use widgets::{BackupWidget, ServicesWidget, SystemWidget, Widget};
|
||||
|
||||
/// Commands that can be triggered from the UI
|
||||
@ -34,9 +34,7 @@ pub enum CommandStatus {
|
||||
/// Command is executing
|
||||
InProgress { command_type: CommandType, target: String, start_time: std::time::Instant },
|
||||
/// Command completed successfully
|
||||
Success { command_type: CommandType, target: String, duration: std::time::Duration, completed_at: std::time::Instant },
|
||||
/// Command failed
|
||||
Failed { command_type: CommandType, target: String, error: String, failed_at: std::time::Instant },
|
||||
Success { command_type: CommandType, completed_at: std::time::Instant },
|
||||
}
|
||||
|
||||
/// Types of commands for status tracking
|
||||
@ -58,28 +56,6 @@ pub enum PanelType {
|
||||
}
|
||||
|
||||
impl PanelType {
|
||||
/// Get all panel types in order
|
||||
pub fn all() -> [PanelType; 3] {
|
||||
[PanelType::System, PanelType::Services, PanelType::Backup]
|
||||
}
|
||||
|
||||
/// Get the next panel in cycle (System → Services → Backup → System)
|
||||
pub fn next(self) -> PanelType {
|
||||
match self {
|
||||
PanelType::System => PanelType::Services,
|
||||
PanelType::Services => PanelType::Backup,
|
||||
PanelType::Backup => PanelType::System,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the previous panel in cycle (System ← Services ← Backup ← System)
|
||||
pub fn previous(self) -> PanelType {
|
||||
match self {
|
||||
PanelType::System => PanelType::Backup,
|
||||
PanelType::Services => PanelType::System,
|
||||
PanelType::Backup => PanelType::Services,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Widget states for a specific host
|
||||
@ -423,34 +399,7 @@ impl TuiApp {
|
||||
info!("Switched to panel: {:?}", self.focused_panel);
|
||||
}
|
||||
|
||||
/// Switch to previous panel (Shift+Tab in reverse) - only cycles through visible panels
|
||||
pub fn previous_panel(&mut self) {
|
||||
let visible_panels = self.get_visible_panels();
|
||||
if visible_panels.len() <= 1 {
|
||||
return; // Can't switch if only one or no panels visible
|
||||
}
|
||||
|
||||
// Find current panel index in visible panels
|
||||
if let Some(current_index) = visible_panels.iter().position(|&p| p == self.focused_panel) {
|
||||
// Move to previous visible panel
|
||||
let prev_index = if current_index == 0 {
|
||||
visible_panels.len() - 1
|
||||
} else {
|
||||
current_index - 1
|
||||
};
|
||||
self.focused_panel = visible_panels[prev_index];
|
||||
} else {
|
||||
// Current panel not visible, switch to last visible panel
|
||||
self.focused_panel = visible_panels[visible_panels.len() - 1];
|
||||
}
|
||||
|
||||
info!("Switched to panel: {:?}", self.focused_panel);
|
||||
}
|
||||
|
||||
/// Get the currently focused panel
|
||||
pub fn get_focused_panel(&self) -> PanelType {
|
||||
self.focused_panel
|
||||
}
|
||||
|
||||
/// Get the currently selected service name from the services widget
|
||||
fn get_selected_service(&self) -> Option<String> {
|
||||
@ -462,15 +411,6 @@ impl TuiApp {
|
||||
None
|
||||
}
|
||||
|
||||
/// Get command status for current host
|
||||
pub fn get_command_status(&self) -> Option<&CommandStatus> {
|
||||
if let Some(hostname) = &self.current_host {
|
||||
if let Some(host_widgets) = self.host_widgets.get(hostname) {
|
||||
return host_widgets.command_status.as_ref();
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Should quit application
|
||||
pub fn should_quit(&self) -> bool {
|
||||
@ -491,31 +431,15 @@ impl TuiApp {
|
||||
/// Mark command as completed successfully
|
||||
pub fn complete_command(&mut self, hostname: &str) {
|
||||
if let Some(host_widgets) = self.host_widgets.get_mut(hostname) {
|
||||
if let Some(CommandStatus::InProgress { command_type, target, start_time }) = &host_widgets.command_status {
|
||||
let duration = start_time.elapsed();
|
||||
if let Some(CommandStatus::InProgress { command_type, .. }) = &host_widgets.command_status {
|
||||
host_widgets.command_status = Some(CommandStatus::Success {
|
||||
command_type: command_type.clone(),
|
||||
target: target.clone(),
|
||||
duration,
|
||||
completed_at: Instant::now(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Mark command as failed
|
||||
pub fn fail_command(&mut self, hostname: &str, error: String) {
|
||||
if let Some(host_widgets) = self.host_widgets.get_mut(hostname) {
|
||||
if let Some(CommandStatus::InProgress { command_type, target, .. }) = &host_widgets.command_status {
|
||||
host_widgets.command_status = Some(CommandStatus::Failed {
|
||||
command_type: command_type.clone(),
|
||||
target: target.clone(),
|
||||
error,
|
||||
failed_at: Instant::now(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Check for command timeouts and automatically clear them
|
||||
pub fn check_command_timeouts(&mut self) {
|
||||
@ -539,11 +463,6 @@ impl TuiApp {
|
||||
hosts_to_clear.push(hostname.clone());
|
||||
}
|
||||
}
|
||||
else if let Some(CommandStatus::Failed { failed_at, .. }) = &host_widgets.command_status {
|
||||
if now.duration_since(*failed_at) > Duration::from_secs(5) {
|
||||
hosts_to_clear.push(hostname.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clear timed out commands
|
||||
@ -620,14 +539,6 @@ impl TuiApp {
|
||||
}
|
||||
}
|
||||
|
||||
/// Get total count of services for bounds checking
|
||||
fn get_total_services_count(&self, hostname: &str) -> usize {
|
||||
if let Some(host_widgets) = self.host_widgets.get(hostname) {
|
||||
host_widgets.services_widget.get_total_services_count()
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
/// Get list of currently visible panels
|
||||
fn get_visible_panels(&self) -> Vec<PanelType> {
|
||||
@ -759,10 +670,6 @@ impl TuiApp {
|
||||
// Show green checkmark for successful rebuild
|
||||
("✓", Theme::success())
|
||||
}
|
||||
Some(CommandStatus::Failed { command_type: CommandType::SystemRebuild, .. }) => {
|
||||
// Show red X for failed rebuild
|
||||
("✗", Theme::error())
|
||||
}
|
||||
_ => {
|
||||
// Normal status icon based on metrics
|
||||
let host_status = self.calculate_host_status(host, metric_store);
|
||||
@ -928,297 +835,4 @@ impl TuiApp {
|
||||
}
|
||||
}
|
||||
|
||||
fn render_storage_section(&self, frame: &mut Frame, area: Rect, metric_store: &MetricStore) {
|
||||
if area.height < 2 {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(ref hostname) = self.current_host {
|
||||
// Discover storage pools from metrics (look for disk_{pool}_usage_percent patterns)
|
||||
let mut storage_pools: std::collections::HashMap<String, Vec<String>> =
|
||||
std::collections::HashMap::new();
|
||||
|
||||
let all_metrics = metric_store.get_metrics_for_host(hostname);
|
||||
|
||||
// Find storage pools by looking for usage metrics
|
||||
for metric in &all_metrics {
|
||||
if metric.name.starts_with("disk_") && metric.name.ends_with("_usage_percent") {
|
||||
let pool_name = metric.name
|
||||
.strip_prefix("disk_")
|
||||
.and_then(|s| s.strip_suffix("_usage_percent"))
|
||||
.unwrap_or_default()
|
||||
.to_string();
|
||||
|
||||
if !pool_name.is_empty() && pool_name != "tmp" {
|
||||
storage_pools.entry(pool_name.clone()).or_insert_with(Vec::new);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Find individual drives for each pool
|
||||
for metric in &all_metrics {
|
||||
if metric.name.starts_with("disk_") && metric.name.contains("_") && metric.name.ends_with("_health") {
|
||||
// Parse disk_{pool}_{drive}_health format
|
||||
let parts: Vec<&str> = metric.name.split('_').collect();
|
||||
if parts.len() >= 4 && parts[0] == "disk" && parts[parts.len()-1] == "health" {
|
||||
// Extract pool name (everything between "disk_" and "_{drive}_health")
|
||||
let drive_name = parts[parts.len()-2].to_string();
|
||||
let pool_part_end = parts.len() - 2;
|
||||
let pool_name = parts[1..pool_part_end].join("_");
|
||||
|
||||
if let Some(drives) = storage_pools.get_mut(&pool_name) {
|
||||
if !drives.contains(&drive_name) {
|
||||
drives.push(drive_name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if we found any storage pools
|
||||
if storage_pools.is_empty() {
|
||||
// No storage pools found - show error/waiting message
|
||||
let content_chunks = ratatui::layout::Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([Constraint::Length(1), Constraint::Min(0)])
|
||||
.split(area);
|
||||
|
||||
let storage_title = Paragraph::new("Storage:").style(Typography::widget_title());
|
||||
frame.render_widget(storage_title, content_chunks[0]);
|
||||
|
||||
let no_storage_spans =
|
||||
StatusIcons::create_status_spans(Status::Unknown, "No storage pools detected");
|
||||
let no_storage_para = Paragraph::new(ratatui::text::Line::from(no_storage_spans));
|
||||
frame.render_widget(no_storage_para, content_chunks[1]);
|
||||
return;
|
||||
}
|
||||
|
||||
let available_lines = area.height as usize;
|
||||
let mut constraints = Vec::new();
|
||||
let mut pools_to_show = Vec::new();
|
||||
let mut current_line = 0;
|
||||
|
||||
// Sort storage pools by name for consistent ordering
|
||||
let mut sorted_pools: Vec<_> = storage_pools.iter().collect();
|
||||
sorted_pools.sort_by_key(|(pool_name, _)| pool_name.as_str());
|
||||
|
||||
// Add section title if we have pools
|
||||
let mut title_added = false;
|
||||
|
||||
for (pool_name, drives) in sorted_pools {
|
||||
// Calculate lines needed: pool header + drives + usage line (+ section title if first)
|
||||
let section_title_lines = if !title_added { 1 } else { 0 };
|
||||
let lines_for_this_pool = section_title_lines + 1 + drives.len() + 1;
|
||||
|
||||
if current_line + lines_for_this_pool <= available_lines {
|
||||
pools_to_show.push((pool_name.clone(), drives.clone()));
|
||||
|
||||
// Add section title constraint if this is the first pool
|
||||
if !title_added {
|
||||
constraints.push(Constraint::Length(1)); // "Storage:" section title
|
||||
title_added = true;
|
||||
}
|
||||
|
||||
// Add constraints for this pool
|
||||
constraints.push(Constraint::Length(1)); // Pool header with status
|
||||
for _ in 0..drives.len() {
|
||||
constraints.push(Constraint::Length(1)); // Drive line with tree symbol
|
||||
}
|
||||
constraints.push(Constraint::Length(1)); // Usage line with end tree symbol
|
||||
|
||||
current_line += lines_for_this_pool;
|
||||
} else {
|
||||
break; // Can't fit more pools
|
||||
}
|
||||
}
|
||||
|
||||
// Add remaining space if any
|
||||
if constraints.len() < available_lines {
|
||||
constraints.push(Constraint::Min(0));
|
||||
}
|
||||
|
||||
let content_chunks = ratatui::layout::Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints(constraints)
|
||||
.split(area);
|
||||
|
||||
let mut chunk_index = 0;
|
||||
|
||||
// Render "Storage:" section title if we have pools
|
||||
if !pools_to_show.is_empty() {
|
||||
let storage_title = Paragraph::new("Storage:").style(Typography::widget_title());
|
||||
frame.render_widget(storage_title, content_chunks[chunk_index]);
|
||||
chunk_index += 1;
|
||||
}
|
||||
|
||||
// Display each storage pool with tree structure
|
||||
for (pool_name, drives) in &pools_to_show {
|
||||
// Pool header with status icon and type
|
||||
let pool_display_name = if pool_name == "root" {
|
||||
"root".to_string()
|
||||
} else {
|
||||
pool_name.clone()
|
||||
};
|
||||
|
||||
let pool_type = if drives.len() > 1 { "multi-drive" } else { "Single" };
|
||||
|
||||
// Get pool status from usage metric
|
||||
let pool_status = metric_store
|
||||
.get_metric(hostname, &format!("disk_{}_usage_percent", pool_name))
|
||||
.map(|m| m.status)
|
||||
.unwrap_or(Status::Unknown);
|
||||
|
||||
// Create pool header with status icon
|
||||
let pool_status_icon = StatusIcons::get_icon(pool_status);
|
||||
let pool_status_color = Theme::status_color(pool_status);
|
||||
let pool_header_text = format!("{} ({}):", pool_display_name, pool_type);
|
||||
|
||||
let pool_header_spans = vec![
|
||||
ratatui::text::Span::styled(
|
||||
format!("{} ", pool_status_icon),
|
||||
Style::default().fg(pool_status_color),
|
||||
),
|
||||
ratatui::text::Span::styled(
|
||||
pool_header_text,
|
||||
Style::default().fg(Theme::primary_text()),
|
||||
),
|
||||
];
|
||||
let pool_header_para = Paragraph::new(ratatui::text::Line::from(pool_header_spans));
|
||||
frame.render_widget(pool_header_para, content_chunks[chunk_index]);
|
||||
chunk_index += 1;
|
||||
|
||||
// Individual drive lines with tree symbols
|
||||
let mut sorted_drives = drives.clone();
|
||||
sorted_drives.sort();
|
||||
for (_drive_idx, drive_name) in sorted_drives.iter().enumerate() {
|
||||
// Get drive health status
|
||||
let drive_health_metric = metric_store
|
||||
.get_metric(hostname, &format!("disk_{}_{}_health", pool_name, drive_name));
|
||||
let drive_status = drive_health_metric
|
||||
.map(|m| m.status)
|
||||
.unwrap_or(Status::Unknown);
|
||||
|
||||
// Get drive temperature
|
||||
let temp_text = metric_store
|
||||
.get_metric(hostname, &format!("disk_{}_{}_temperature", pool_name, drive_name))
|
||||
.and_then(|m| m.value.as_f32())
|
||||
.map(|temp| format!(" T:{:.0}°C", temp))
|
||||
.unwrap_or_default();
|
||||
|
||||
// Get drive wear level (SSDs)
|
||||
let wear_text = metric_store
|
||||
.get_metric(hostname, &format!("disk_{}_{}_wear_percent", pool_name, drive_name))
|
||||
.and_then(|m| m.value.as_f32())
|
||||
.map(|wear| format!(" W:{:.0}%", wear))
|
||||
.unwrap_or_default();
|
||||
|
||||
// Build drive line with tree symbol
|
||||
let tree_symbol = "├─";
|
||||
let drive_status_icon = StatusIcons::get_icon(drive_status);
|
||||
let drive_status_color = Theme::status_color(drive_status);
|
||||
let drive_text = format!("{}{}{}", drive_name, temp_text, wear_text);
|
||||
|
||||
let drive_spans = vec![
|
||||
ratatui::text::Span::styled(" ", Style::default()), // 2-space indentation
|
||||
ratatui::text::Span::styled(
|
||||
format!("{} ", tree_symbol),
|
||||
Style::default().fg(Theme::muted_text()),
|
||||
),
|
||||
ratatui::text::Span::styled(
|
||||
format!("{} ", drive_status_icon),
|
||||
Style::default().fg(drive_status_color),
|
||||
),
|
||||
ratatui::text::Span::styled(
|
||||
drive_text,
|
||||
Style::default().fg(Theme::primary_text()),
|
||||
),
|
||||
];
|
||||
let drive_para = Paragraph::new(ratatui::text::Line::from(drive_spans));
|
||||
frame.render_widget(drive_para, content_chunks[chunk_index]);
|
||||
chunk_index += 1;
|
||||
}
|
||||
|
||||
// Usage line with end tree symbol and status icon
|
||||
let usage_percent = metric_store
|
||||
.get_metric(hostname, &format!("disk_{}_usage_percent", pool_name))
|
||||
.and_then(|m| m.value.as_f32())
|
||||
.unwrap_or(0.0);
|
||||
|
||||
let used_gb = metric_store
|
||||
.get_metric(hostname, &format!("disk_{}_used_gb", pool_name))
|
||||
.and_then(|m| m.value.as_f32())
|
||||
.unwrap_or(0.0);
|
||||
|
||||
let total_gb = metric_store
|
||||
.get_metric(hostname, &format!("disk_{}_total_gb", pool_name))
|
||||
.and_then(|m| m.value.as_f32())
|
||||
.unwrap_or(0.0);
|
||||
|
||||
let usage_status = metric_store
|
||||
.get_metric(hostname, &format!("disk_{}_usage_percent", pool_name))
|
||||
.map(|m| m.status)
|
||||
.unwrap_or(Status::Unknown);
|
||||
|
||||
// Format usage with proper units
|
||||
let (used_display, total_display, unit) = if total_gb < 1.0 {
|
||||
(used_gb * 1024.0, total_gb * 1024.0, "MB")
|
||||
} else {
|
||||
(used_gb, total_gb, "GB")
|
||||
};
|
||||
|
||||
let end_tree_symbol = "└─";
|
||||
let usage_status_icon = StatusIcons::get_icon(usage_status);
|
||||
let usage_status_color = Theme::status_color(usage_status);
|
||||
let usage_text = format!("{:.1}% {:.1}{}/{:.1}{}",
|
||||
usage_percent, used_display, unit, total_display, unit);
|
||||
|
||||
let usage_spans = vec![
|
||||
ratatui::text::Span::styled(" ", Style::default()), // 2-space indentation
|
||||
ratatui::text::Span::styled(
|
||||
format!("{} ", end_tree_symbol),
|
||||
Style::default().fg(Theme::muted_text()),
|
||||
),
|
||||
ratatui::text::Span::styled(
|
||||
format!("{} ", usage_status_icon),
|
||||
Style::default().fg(usage_status_color),
|
||||
),
|
||||
ratatui::text::Span::styled(
|
||||
usage_text,
|
||||
Style::default().fg(Theme::primary_text()),
|
||||
),
|
||||
];
|
||||
let usage_para = Paragraph::new(ratatui::text::Line::from(usage_spans));
|
||||
frame.render_widget(usage_para, content_chunks[chunk_index]);
|
||||
chunk_index += 1;
|
||||
}
|
||||
|
||||
// Show truncation indicator if we couldn't display all pools
|
||||
if pools_to_show.len() < storage_pools.len() {
|
||||
if let Some(last_chunk) = content_chunks.last() {
|
||||
let truncated_count = storage_pools.len() - pools_to_show.len();
|
||||
let truncated_text = format!(
|
||||
"... and {} more pool{}",
|
||||
truncated_count,
|
||||
if truncated_count == 1 { "" } else { "s" }
|
||||
);
|
||||
let truncated_para = Paragraph::new(truncated_text).style(Typography::muted());
|
||||
frame.render_widget(truncated_para, *last_chunk);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// No host connected
|
||||
let content_chunks = ratatui::layout::Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([Constraint::Length(1), Constraint::Min(0)])
|
||||
.split(area);
|
||||
|
||||
let storage_title = Paragraph::new("Storage:").style(Typography::widget_title());
|
||||
frame.render_widget(storage_title, content_chunks[0]);
|
||||
|
||||
let no_host_spans =
|
||||
StatusIcons::create_status_spans(Status::Unknown, "No host connected");
|
||||
let no_host_para = Paragraph::new(ratatui::text::Line::from(no_host_spans));
|
||||
frame.render_widget(no_host_para, content_chunks[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -226,10 +226,6 @@ impl Layout {
|
||||
/// System vs backup split (equal)
|
||||
pub const SYSTEM_PANEL_HEIGHT: u16 = 50;
|
||||
pub const BACKUP_PANEL_HEIGHT: u16 = 50;
|
||||
/// System panel CPU section height
|
||||
pub const CPU_SECTION_HEIGHT: u16 = 2;
|
||||
/// System panel memory section height
|
||||
pub const MEMORY_SECTION_HEIGHT: u16 = 3;
|
||||
}
|
||||
|
||||
/// Typography system
|
||||
|
||||
@ -81,38 +81,7 @@ impl BackupWidget {
|
||||
|
||||
|
||||
|
||||
/// Format timestamp for display
|
||||
fn format_last_run(&self) -> String {
|
||||
match self.last_run_timestamp {
|
||||
Some(timestamp) => {
|
||||
let duration = chrono::Utc::now().timestamp() - timestamp;
|
||||
if duration < 3600 {
|
||||
format!("{}m ago", duration / 60)
|
||||
} else if duration < 86400 {
|
||||
format!("{}h ago", duration / 3600)
|
||||
} else {
|
||||
format!("{}d ago", duration / 86400)
|
||||
}
|
||||
}
|
||||
None => "—".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Format disk usage in format "usedGB/totalGB"
|
||||
fn format_repo_size(&self) -> String {
|
||||
match (self.backup_disk_used_gb, self.backup_disk_total_gb) {
|
||||
(Some(used_gb), Some(total_gb)) => {
|
||||
let used_str = Self::format_size_with_proper_units(used_gb);
|
||||
let total_str = Self::format_size_with_proper_units(total_gb);
|
||||
format!("{}/{}", used_str, total_str)
|
||||
}
|
||||
(Some(used_gb), None) => {
|
||||
// Fallback to just used size if total not available
|
||||
Self::format_size_with_proper_units(used_gb)
|
||||
}
|
||||
_ => "—".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Format size with proper units (xxxkB/MB/GB/TB)
|
||||
fn format_size_with_proper_units(size_gb: f32) -> String {
|
||||
@ -137,23 +106,7 @@ impl BackupWidget {
|
||||
}
|
||||
}
|
||||
|
||||
/// Format product name display
|
||||
fn format_product_name(&self) -> String {
|
||||
if let Some(ref product_name) = self.backup_disk_product_name {
|
||||
format!("P/N: {}", product_name)
|
||||
} else {
|
||||
"P/N: Unknown".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// Format serial number display
|
||||
fn format_serial_number(&self) -> String {
|
||||
if let Some(ref serial) = self.backup_disk_serial_number {
|
||||
format!("S/N: {}", serial)
|
||||
} else {
|
||||
"S/N: Unknown".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// Extract service name from metric name (e.g., "backup_service_gitea_status" -> "gitea")
|
||||
fn extract_service_name(metric_name: &str) -> Option<String> {
|
||||
@ -324,9 +277,6 @@ impl Widget for BackupWidget {
|
||||
}
|
||||
}
|
||||
|
||||
fn render(&mut self, frame: &mut Frame, area: Rect) {
|
||||
self.render_with_scroll(frame, area, 0);
|
||||
}
|
||||
}
|
||||
|
||||
impl BackupWidget {
|
||||
|
||||
@ -1,139 +1 @@
|
||||
use cm_dashboard_shared::{Metric, Status};
|
||||
use ratatui::{
|
||||
layout::{Constraint, Direction, Layout, Rect},
|
||||
widgets::Paragraph,
|
||||
Frame,
|
||||
};
|
||||
use tracing::debug;
|
||||
|
||||
use super::Widget;
|
||||
use crate::ui::theme::{StatusIcons, Typography};
|
||||
|
||||
/// CPU widget displaying load, temperature, and frequency
|
||||
#[derive(Clone)]
|
||||
pub struct CpuWidget {
|
||||
/// CPU load averages (1, 5, 15 minutes)
|
||||
load_1min: Option<f32>,
|
||||
load_5min: Option<f32>,
|
||||
load_15min: Option<f32>,
|
||||
/// CPU temperature in Celsius
|
||||
temperature: Option<f32>,
|
||||
/// CPU frequency in MHz
|
||||
frequency: Option<f32>,
|
||||
/// Aggregated status
|
||||
status: Status,
|
||||
/// Last update indicator
|
||||
has_data: bool,
|
||||
}
|
||||
|
||||
impl CpuWidget {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
load_1min: None,
|
||||
load_5min: None,
|
||||
load_15min: None,
|
||||
temperature: None,
|
||||
frequency: None,
|
||||
status: Status::Unknown,
|
||||
has_data: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Format load average for display
|
||||
fn format_load(&self) -> String {
|
||||
match (self.load_1min, self.load_5min, self.load_15min) {
|
||||
(Some(l1), Some(l5), Some(l15)) => {
|
||||
format!("{:.2} {:.2} {:.2}", l1, l5, l15)
|
||||
}
|
||||
_ => "— — —".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Format frequency for display
|
||||
fn format_frequency(&self) -> String {
|
||||
match self.frequency {
|
||||
Some(freq) => format!("{:.1} MHz", freq),
|
||||
None => "— MHz".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for CpuWidget {
|
||||
fn update_from_metrics(&mut self, metrics: &[&Metric]) {
|
||||
debug!("CPU widget updating with {} metrics", metrics.len());
|
||||
|
||||
// Reset status aggregation
|
||||
let mut statuses = Vec::new();
|
||||
|
||||
for metric in metrics {
|
||||
match metric.name.as_str() {
|
||||
"cpu_load_1min" => {
|
||||
if let Some(value) = metric.value.as_f32() {
|
||||
self.load_1min = Some(value);
|
||||
statuses.push(metric.status);
|
||||
}
|
||||
}
|
||||
"cpu_load_5min" => {
|
||||
if let Some(value) = metric.value.as_f32() {
|
||||
self.load_5min = Some(value);
|
||||
statuses.push(metric.status);
|
||||
}
|
||||
}
|
||||
"cpu_load_15min" => {
|
||||
if let Some(value) = metric.value.as_f32() {
|
||||
self.load_15min = Some(value);
|
||||
statuses.push(metric.status);
|
||||
}
|
||||
}
|
||||
"cpu_temperature_celsius" => {
|
||||
if let Some(value) = metric.value.as_f32() {
|
||||
self.temperature = Some(value);
|
||||
statuses.push(metric.status);
|
||||
}
|
||||
}
|
||||
"cpu_frequency_mhz" => {
|
||||
if let Some(value) = metric.value.as_f32() {
|
||||
self.frequency = Some(value);
|
||||
statuses.push(metric.status);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
// Aggregate status
|
||||
self.status = if statuses.is_empty() {
|
||||
Status::Unknown
|
||||
} else {
|
||||
Status::aggregate(&statuses)
|
||||
};
|
||||
|
||||
self.has_data = !metrics.is_empty();
|
||||
|
||||
debug!(
|
||||
"CPU widget updated: load={:?}, temp={:?}, freq={:?}, status={:?}",
|
||||
self.load_1min, self.temperature, self.frequency, self.status
|
||||
);
|
||||
}
|
||||
|
||||
fn render(&mut self, frame: &mut Frame, area: Rect) {
|
||||
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 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()
|
||||
}
|
||||
}
|
||||
// This file is intentionally left minimal - CPU functionality is handled by the SystemWidget
|
||||
@ -1,253 +1 @@
|
||||
use cm_dashboard_shared::{Metric, Status};
|
||||
use ratatui::{
|
||||
layout::{Constraint, Direction, Layout, Rect},
|
||||
widgets::Paragraph,
|
||||
Frame,
|
||||
};
|
||||
use tracing::debug;
|
||||
|
||||
use super::Widget;
|
||||
use crate::ui::theme::{StatusIcons, Typography};
|
||||
|
||||
/// Memory widget displaying usage, totals, and swap information
|
||||
#[derive(Clone)]
|
||||
pub struct MemoryWidget {
|
||||
/// Memory usage percentage
|
||||
usage_percent: Option<f32>,
|
||||
/// Total memory in GB
|
||||
total_gb: Option<f32>,
|
||||
/// Used memory in GB
|
||||
used_gb: Option<f32>,
|
||||
/// Available memory in GB
|
||||
available_gb: Option<f32>,
|
||||
/// Total swap in GB
|
||||
swap_total_gb: Option<f32>,
|
||||
/// Used swap in GB
|
||||
swap_used_gb: Option<f32>,
|
||||
/// /tmp directory size in MB
|
||||
tmp_size_mb: Option<f32>,
|
||||
/// /tmp total size in MB
|
||||
tmp_total_mb: Option<f32>,
|
||||
/// /tmp usage percentage
|
||||
tmp_usage_percent: Option<f32>,
|
||||
/// Aggregated status
|
||||
status: Status,
|
||||
/// Last update indicator
|
||||
has_data: bool,
|
||||
}
|
||||
|
||||
impl MemoryWidget {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
usage_percent: None,
|
||||
total_gb: None,
|
||||
used_gb: None,
|
||||
available_gb: None,
|
||||
swap_total_gb: None,
|
||||
swap_used_gb: None,
|
||||
tmp_size_mb: None,
|
||||
tmp_total_mb: None,
|
||||
tmp_usage_percent: None,
|
||||
status: Status::Unknown,
|
||||
has_data: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get memory usage percentage for gauge
|
||||
fn get_memory_percentage(&self) -> u16 {
|
||||
match self.usage_percent {
|
||||
Some(percent) => percent.min(100.0).max(0.0) as u16,
|
||||
None => {
|
||||
// Calculate from used/total if percentage not available
|
||||
match (self.used_gb, self.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
|
||||
}
|
||||
_ => 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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)
|
||||
}
|
||||
(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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for MemoryWidget {
|
||||
fn update_from_metrics(&mut self, metrics: &[&Metric]) {
|
||||
debug!("Memory widget updating with {} metrics", metrics.len());
|
||||
|
||||
// Reset status aggregation
|
||||
let mut statuses = Vec::new();
|
||||
|
||||
for metric in metrics {
|
||||
match metric.name.as_str() {
|
||||
"memory_usage_percent" => {
|
||||
if let Some(value) = metric.value.as_f32() {
|
||||
self.usage_percent = Some(value);
|
||||
statuses.push(metric.status);
|
||||
}
|
||||
}
|
||||
"memory_total_gb" => {
|
||||
if let Some(value) = metric.value.as_f32() {
|
||||
self.total_gb = Some(value);
|
||||
statuses.push(metric.status);
|
||||
}
|
||||
}
|
||||
"memory_used_gb" => {
|
||||
if let Some(value) = metric.value.as_f32() {
|
||||
self.used_gb = Some(value);
|
||||
statuses.push(metric.status);
|
||||
}
|
||||
}
|
||||
"memory_available_gb" => {
|
||||
if let Some(value) = metric.value.as_f32() {
|
||||
self.available_gb = Some(value);
|
||||
statuses.push(metric.status);
|
||||
}
|
||||
}
|
||||
"memory_swap_total_gb" => {
|
||||
if let Some(value) = metric.value.as_f32() {
|
||||
self.swap_total_gb = Some(value);
|
||||
statuses.push(metric.status);
|
||||
}
|
||||
}
|
||||
"memory_swap_used_gb" => {
|
||||
if let Some(value) = metric.value.as_f32() {
|
||||
self.swap_used_gb = Some(value);
|
||||
statuses.push(metric.status);
|
||||
}
|
||||
}
|
||||
"disk_tmp_size_mb" => {
|
||||
if let Some(value) = metric.value.as_f32() {
|
||||
self.tmp_size_mb = Some(value);
|
||||
statuses.push(metric.status);
|
||||
}
|
||||
}
|
||||
"disk_tmp_total_mb" => {
|
||||
if let Some(value) = metric.value.as_f32() {
|
||||
self.tmp_total_mb = Some(value);
|
||||
statuses.push(metric.status);
|
||||
}
|
||||
}
|
||||
"disk_tmp_usage_percent" => {
|
||||
if let Some(value) = metric.value.as_f32() {
|
||||
self.tmp_usage_percent = Some(value);
|
||||
statuses.push(metric.status);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
// Aggregate status
|
||||
self.status = if statuses.is_empty() {
|
||||
Status::Unknown
|
||||
} else {
|
||||
Status::aggregate(&statuses)
|
||||
};
|
||||
|
||||
self.has_data = !metrics.is_empty();
|
||||
|
||||
debug!("Memory widget updated: usage={:?}%, total={:?}GB, swap_total={:?}GB, tmp={:?}/{:?}MB, status={:?}",
|
||||
self.usage_percent, self.total_gb, self.swap_total_gb, self.tmp_size_mb, self.tmp_total_mb, self.status);
|
||||
}
|
||||
|
||||
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("RAM:").style(Typography::widget_title());
|
||||
frame.render_widget(mem_title, content_chunks[0]);
|
||||
|
||||
// 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]);
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for MemoryWidget {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
// This file is intentionally left minimal - Memory functionality is handled by the SystemWidget
|
||||
@ -1,5 +1,4 @@
|
||||
use cm_dashboard_shared::Metric;
|
||||
use ratatui::{layout::Rect, Frame};
|
||||
|
||||
pub mod backup;
|
||||
pub mod cpu;
|
||||
@ -16,6 +15,4 @@ pub trait Widget {
|
||||
/// Update widget with new metrics data
|
||||
fn update_from_metrics(&mut self, metrics: &[&Metric]);
|
||||
|
||||
/// Render the widget to a terminal frame
|
||||
fn render(&mut self, frame: &mut Frame, area: Rect);
|
||||
}
|
||||
|
||||
@ -163,15 +163,6 @@ impl ServicesWidget {
|
||||
(icon.to_string(), info.status.clone(), status_color)
|
||||
}
|
||||
|
||||
/// Create spans for sub-service with icon next to name
|
||||
fn create_sub_service_spans(
|
||||
&self,
|
||||
name: &str,
|
||||
info: &ServiceInfo,
|
||||
is_last: bool,
|
||||
) -> Vec<ratatui::text::Span<'static>> {
|
||||
self.create_sub_service_spans_with_status(name, info, is_last, None)
|
||||
}
|
||||
|
||||
/// Create spans for sub-service with icon next to name, considering command status
|
||||
fn create_sub_service_spans_with_status(
|
||||
@ -432,16 +423,9 @@ impl Widget for ServicesWidget {
|
||||
);
|
||||
}
|
||||
|
||||
fn render(&mut self, frame: &mut Frame, area: Rect) {
|
||||
self.render_with_focus(frame, area, false);
|
||||
}
|
||||
}
|
||||
|
||||
impl ServicesWidget {
|
||||
/// Render with optional focus indicator and scroll support
|
||||
pub fn render_with_focus(&mut self, frame: &mut Frame, area: Rect, is_focused: bool) {
|
||||
self.render_with_focus_and_scroll(frame, area, is_focused, 0);
|
||||
}
|
||||
|
||||
/// Render with focus, scroll, and command status for visual feedback
|
||||
pub fn render_with_command_status(&mut self, frame: &mut Frame, area: Rect, is_focused: bool, scroll_offset: usize, command_status: Option<&CommandStatus>) {
|
||||
@ -635,167 +619,6 @@ impl ServicesWidget {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Render with focus indicator and scroll offset
|
||||
pub fn render_with_focus_and_scroll(&mut self, frame: &mut Frame, area: Rect, is_focused: bool, scroll_offset: usize) {
|
||||
let services_block = if is_focused {
|
||||
Components::focused_widget_block("services")
|
||||
} else {
|
||||
Components::widget_block("services")
|
||||
};
|
||||
let inner_area = services_block.inner(area);
|
||||
frame.render_widget(services_block, area);
|
||||
|
||||
let content_chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([Constraint::Length(1), Constraint::Min(0)])
|
||||
.split(inner_area);
|
||||
|
||||
// Header
|
||||
let header = format!(
|
||||
"{:<25} {:<10} {:<8} {:<8}",
|
||||
"Service:", "Status:", "RAM:", "Disk:"
|
||||
);
|
||||
let header_para = Paragraph::new(header).style(Typography::muted());
|
||||
frame.render_widget(header_para, content_chunks[0]);
|
||||
|
||||
// Check if we have any services to display
|
||||
if self.parent_services.is_empty() && self.sub_services.is_empty() {
|
||||
let empty_text = Paragraph::new("No process data").style(Typography::muted());
|
||||
frame.render_widget(empty_text, content_chunks[1]);
|
||||
return;
|
||||
}
|
||||
|
||||
// Build hierarchical service list for display
|
||||
let mut display_lines: Vec<(String, Status, bool, Option<(ServiceInfo, bool)>)> = Vec::new();
|
||||
|
||||
// Sort parent services alphabetically for consistent order
|
||||
let mut parent_services: Vec<_> = self.parent_services.iter().collect();
|
||||
parent_services.sort_by(|(a, _), (b, _)| a.cmp(b));
|
||||
|
||||
for (parent_name, parent_info) in parent_services {
|
||||
// Add parent service line
|
||||
let parent_line = self.format_parent_service_line(parent_name, parent_info);
|
||||
display_lines.push((parent_line, parent_info.widget_status, false, None)); // false = not sub-service
|
||||
|
||||
// Add sub-services for this parent (if any)
|
||||
if let Some(sub_list) = self.sub_services.get(parent_name) {
|
||||
// Sort sub-services by name for consistent display
|
||||
let mut sorted_subs = sub_list.clone();
|
||||
sorted_subs.sort_by(|(a, _), (b, _)| a.cmp(b));
|
||||
|
||||
for (i, (sub_name, sub_info)) in sorted_subs.iter().enumerate() {
|
||||
let is_last_sub = i == sorted_subs.len() - 1;
|
||||
// Store sub-service info for custom span rendering
|
||||
display_lines.push((
|
||||
sub_name.clone(),
|
||||
sub_info.widget_status,
|
||||
true,
|
||||
Some((sub_info.clone(), is_last_sub)),
|
||||
)); // true = sub-service, with is_last info
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Apply scroll offset and render visible lines
|
||||
let available_lines = content_chunks[1].height as usize;
|
||||
let total_lines = display_lines.len();
|
||||
|
||||
// Calculate scroll boundaries
|
||||
let max_scroll = if total_lines > available_lines {
|
||||
total_lines - available_lines
|
||||
} else {
|
||||
total_lines.saturating_sub(1)
|
||||
};
|
||||
let effective_scroll = scroll_offset.min(max_scroll);
|
||||
|
||||
// Get visible lines after scrolling
|
||||
let visible_lines: Vec<_> = display_lines
|
||||
.iter()
|
||||
.skip(effective_scroll)
|
||||
.take(available_lines)
|
||||
.collect();
|
||||
|
||||
let lines_to_show = visible_lines.len();
|
||||
|
||||
if lines_to_show > 0 {
|
||||
let service_chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints(vec![Constraint::Length(1); lines_to_show])
|
||||
.split(content_chunks[1]);
|
||||
|
||||
for (i, (line_text, line_status, is_sub, sub_info)) in visible_lines.iter().enumerate()
|
||||
{
|
||||
let actual_index = effective_scroll + i; // Real index in the full list
|
||||
|
||||
// Only parent services can be selected - calculate parent service index
|
||||
let is_selected = if !*is_sub {
|
||||
// This is a parent service - count how many parent services came before this one
|
||||
let parent_index = self.calculate_parent_service_index(&actual_index);
|
||||
parent_index == self.selected_index
|
||||
} else {
|
||||
false // Sub-services are never selected
|
||||
};
|
||||
|
||||
let mut spans = if *is_sub && sub_info.is_some() {
|
||||
// Use custom sub-service span creation
|
||||
let (service_info, is_last) = sub_info.as_ref().unwrap();
|
||||
self.create_sub_service_spans(line_text, service_info, *is_last)
|
||||
} else {
|
||||
// Use regular status spans for parent services
|
||||
StatusIcons::create_status_spans(*line_status, line_text)
|
||||
};
|
||||
|
||||
// Apply selection highlighting to parent services only, preserving status icon color
|
||||
// Only show selection when Services panel is focused
|
||||
if is_selected && !*is_sub && is_focused {
|
||||
for (i, span) in spans.iter_mut().enumerate() {
|
||||
if i == 0 {
|
||||
// First span is the status icon - preserve its color
|
||||
span.style = span.style.bg(Theme::highlight());
|
||||
} else {
|
||||
// Other spans (text) get full selection highlighting
|
||||
span.style = span.style
|
||||
.bg(Theme::highlight())
|
||||
.fg(Theme::background());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let service_para = Paragraph::new(ratatui::text::Line::from(spans));
|
||||
|
||||
frame.render_widget(service_para, service_chunks[i]);
|
||||
}
|
||||
}
|
||||
|
||||
// Show scroll indicator if there are more services than we can display
|
||||
if total_lines > available_lines {
|
||||
let hidden_above = effective_scroll;
|
||||
let hidden_below = total_lines.saturating_sub(effective_scroll + available_lines);
|
||||
|
||||
if hidden_above > 0 || hidden_below > 0 {
|
||||
let scroll_text = if hidden_above > 0 && hidden_below > 0 {
|
||||
format!("... {} above, {} below", hidden_above, hidden_below)
|
||||
} else if hidden_above > 0 {
|
||||
format!("... {} more above", hidden_above)
|
||||
} else {
|
||||
format!("... {} more below", hidden_below)
|
||||
};
|
||||
|
||||
if available_lines > 0 && lines_to_show > 0 {
|
||||
let last_line_area = Rect {
|
||||
x: content_chunks[1].x,
|
||||
y: content_chunks[1].y + (lines_to_show - 1) as u16,
|
||||
width: content_chunks[1].width,
|
||||
height: 1,
|
||||
};
|
||||
|
||||
let scroll_para = Paragraph::new(scroll_text).style(Typography::muted());
|
||||
frame.render_widget(scroll_para, last_line_area);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ServicesWidget {
|
||||
|
||||
@ -249,7 +249,7 @@ impl SystemWidget {
|
||||
}
|
||||
|
||||
/// Render storage section with tree structure
|
||||
fn render_storage(&self) -> Vec<Line> {
|
||||
fn render_storage(&self) -> Vec<Line<'_>> {
|
||||
let mut lines = Vec::new();
|
||||
|
||||
for pool in &self.storage_pools {
|
||||
@ -410,9 +410,6 @@ impl Widget for SystemWidget {
|
||||
self.update_storage_from_metrics(metrics);
|
||||
}
|
||||
|
||||
fn render(&mut self, frame: &mut Frame, area: Rect) {
|
||||
self.render_with_scroll(frame, area, 0);
|
||||
}
|
||||
}
|
||||
|
||||
impl SystemWidget {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user