Testing
This commit is contained in:
@@ -10,7 +10,11 @@ use gethostname::gethostname;
|
||||
use crate::config;
|
||||
use crate::data::config::{AppConfig, DataSourceKind, HostTarget, ZmqConfig};
|
||||
use crate::data::history::MetricsHistory;
|
||||
use crate::data::metrics::{BackupMetrics, ServiceMetrics, SmartMetrics};
|
||||
use crate::data::metrics::{BackupMetrics, ServiceMetrics, SmartMetrics, SystemMetrics};
|
||||
|
||||
// Host connection timeout - if no data received for this duration, mark as timeout
|
||||
// Keep-alive mechanism: agents send data every 5 seconds, timeout after 15 seconds
|
||||
const HOST_CONNECTION_TIMEOUT: Duration = Duration::from_secs(15);
|
||||
|
||||
/// Shared application settings derived from the CLI arguments.
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -32,11 +36,22 @@ impl AppOptions {
|
||||
struct HostRuntimeState {
|
||||
last_success: Option<DateTime<Utc>>,
|
||||
last_error: Option<String>,
|
||||
connection_status: ConnectionStatus,
|
||||
smart: Option<SmartMetrics>,
|
||||
services: Option<ServiceMetrics>,
|
||||
system: Option<SystemMetrics>,
|
||||
backup: Option<BackupMetrics>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub enum ConnectionStatus {
|
||||
#[default]
|
||||
Unknown,
|
||||
Connected,
|
||||
Timeout,
|
||||
Error,
|
||||
}
|
||||
|
||||
/// Top-level application state container.
|
||||
#[derive(Debug)]
|
||||
pub struct App {
|
||||
@@ -100,6 +115,10 @@ impl App {
|
||||
pub fn on_tick(&mut self) {
|
||||
self.tick_count = self.tick_count.saturating_add(1);
|
||||
self.last_tick = Instant::now();
|
||||
|
||||
// Check for host connection timeouts
|
||||
self.check_host_timeouts();
|
||||
|
||||
let host_count = self.hosts.len();
|
||||
let retention = self.history.retention();
|
||||
self.status = format!(
|
||||
@@ -193,8 +212,10 @@ impl App {
|
||||
name: host.name.clone(),
|
||||
last_success: state.last_success.clone(),
|
||||
last_error: state.last_error.clone(),
|
||||
connection_status: state.connection_status.clone(),
|
||||
smart: state.smart.clone(),
|
||||
services: state.services.clone(),
|
||||
system: state.system.clone(),
|
||||
backup: state.backup.clone(),
|
||||
})
|
||||
})
|
||||
@@ -209,8 +230,10 @@ impl App {
|
||||
name: host.name.clone(),
|
||||
last_success: state.last_success.clone(),
|
||||
last_error: state.last_error.clone(),
|
||||
connection_status: state.connection_status.clone(),
|
||||
smart: state.smart.clone(),
|
||||
services: state.services.clone(),
|
||||
system: state.system.clone(),
|
||||
backup: state.backup.clone(),
|
||||
})
|
||||
})
|
||||
@@ -237,6 +260,7 @@ impl App {
|
||||
host,
|
||||
smart,
|
||||
services,
|
||||
system,
|
||||
backup,
|
||||
timestamp,
|
||||
} => {
|
||||
@@ -245,6 +269,7 @@ impl App {
|
||||
let state = self.host_states.entry(host.clone()).or_default();
|
||||
state.last_success = Some(timestamp);
|
||||
state.last_error = None;
|
||||
state.connection_status = ConnectionStatus::Connected;
|
||||
|
||||
if let Some(mut smart_metrics) = smart {
|
||||
if smart_metrics.timestamp != timestamp {
|
||||
@@ -267,6 +292,16 @@ impl App {
|
||||
state.services = Some(snapshot);
|
||||
}
|
||||
|
||||
if let Some(system_metrics) = system {
|
||||
// Convert timestamp format (u64 to DateTime<Utc>)
|
||||
let system_snapshot = SystemMetrics {
|
||||
summary: system_metrics.summary,
|
||||
timestamp: system_metrics.timestamp,
|
||||
};
|
||||
self.history.record_system(system_snapshot.clone());
|
||||
state.system = Some(system_snapshot);
|
||||
}
|
||||
|
||||
if let Some(mut backup_metrics) = backup {
|
||||
if backup_metrics.timestamp != timestamp {
|
||||
backup_metrics.timestamp = timestamp;
|
||||
@@ -291,12 +326,37 @@ impl App {
|
||||
self.ensure_host_entry(&host);
|
||||
let state = self.host_states.entry(host.clone()).or_default();
|
||||
state.last_error = Some(format!("{} at {}", error, timestamp.format("%H:%M:%S")));
|
||||
state.connection_status = ConnectionStatus::Error;
|
||||
|
||||
self.status = format!("Fetch failed • host: {} • {}", host, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn check_host_timeouts(&mut self) {
|
||||
let now = Utc::now();
|
||||
|
||||
for (host_name, state) in self.host_states.iter_mut() {
|
||||
if let Some(last_success) = state.last_success {
|
||||
let duration_since_last = now.signed_duration_since(last_success);
|
||||
|
||||
if duration_since_last > chrono::Duration::from_std(HOST_CONNECTION_TIMEOUT).unwrap() {
|
||||
// Host has timed out (missed keep-alive)
|
||||
if !matches!(state.connection_status, ConnectionStatus::Timeout) {
|
||||
state.connection_status = ConnectionStatus::Timeout;
|
||||
state.last_error = Some(format!("Keep-alive timeout (no data for {}s)", duration_since_last.num_seconds()));
|
||||
}
|
||||
} else {
|
||||
// Host is connected
|
||||
state.connection_status = ConnectionStatus::Connected;
|
||||
}
|
||||
} else {
|
||||
// No data ever received from this host
|
||||
state.connection_status = ConnectionStatus::Unknown;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn help_visible(&self) -> bool {
|
||||
self.show_help
|
||||
}
|
||||
@@ -511,8 +571,10 @@ pub struct HostDisplayData {
|
||||
pub name: String,
|
||||
pub last_success: Option<DateTime<Utc>>,
|
||||
pub last_error: Option<String>,
|
||||
pub connection_status: ConnectionStatus,
|
||||
pub smart: Option<SmartMetrics>,
|
||||
pub services: Option<ServiceMetrics>,
|
||||
pub system: Option<SystemMetrics>,
|
||||
pub backup: Option<BackupMetrics>,
|
||||
}
|
||||
|
||||
@@ -545,6 +607,7 @@ pub enum AppEvent {
|
||||
host: String,
|
||||
smart: Option<SmartMetrics>,
|
||||
services: Option<ServiceMetrics>,
|
||||
system: Option<SystemMetrics>,
|
||||
backup: Option<BackupMetrics>,
|
||||
timestamp: DateTime<Utc>,
|
||||
},
|
||||
|
||||
@@ -5,7 +5,7 @@ use std::time::Duration;
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
|
||||
use crate::data::metrics::{BackupMetrics, ServiceMetrics, SmartMetrics};
|
||||
use crate::data::metrics::{BackupMetrics, ServiceMetrics, SmartMetrics, SystemMetrics};
|
||||
|
||||
/// Ring buffer for retaining recent samples for trend analysis.
|
||||
#[derive(Debug)]
|
||||
@@ -13,6 +13,7 @@ pub struct MetricsHistory {
|
||||
capacity: usize,
|
||||
smart: VecDeque<(DateTime<Utc>, SmartMetrics)>,
|
||||
services: VecDeque<(DateTime<Utc>, ServiceMetrics)>,
|
||||
system: VecDeque<(DateTime<Utc>, SystemMetrics)>,
|
||||
backups: VecDeque<(DateTime<Utc>, BackupMetrics)>,
|
||||
}
|
||||
|
||||
@@ -22,6 +23,7 @@ impl MetricsHistory {
|
||||
capacity,
|
||||
smart: VecDeque::with_capacity(capacity),
|
||||
services: VecDeque::with_capacity(capacity),
|
||||
system: VecDeque::with_capacity(capacity),
|
||||
backups: VecDeque::with_capacity(capacity),
|
||||
}
|
||||
}
|
||||
@@ -36,6 +38,11 @@ impl MetricsHistory {
|
||||
Self::push_with_limit(&mut self.services, entry, self.capacity);
|
||||
}
|
||||
|
||||
pub fn record_system(&mut self, metrics: SystemMetrics) {
|
||||
let entry = (Utc::now(), metrics);
|
||||
Self::push_with_limit(&mut self.system, entry, self.capacity);
|
||||
}
|
||||
|
||||
pub fn record_backup(&mut self, metrics: BackupMetrics) {
|
||||
let entry = (Utc::now(), metrics);
|
||||
Self::push_with_limit(&mut self.backups, entry, self.capacity);
|
||||
|
||||
@@ -32,6 +32,32 @@ pub struct DriveSummary {
|
||||
pub capacity_used_gb: f32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SystemMetrics {
|
||||
pub summary: SystemSummary,
|
||||
pub timestamp: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SystemSummary {
|
||||
pub cpu_load_1: f32,
|
||||
pub cpu_load_5: f32,
|
||||
pub cpu_load_15: f32,
|
||||
#[serde(default)]
|
||||
pub cpu_status: Option<String>,
|
||||
pub memory_used_mb: f32,
|
||||
pub memory_total_mb: f32,
|
||||
pub memory_usage_percent: f32,
|
||||
#[serde(default)]
|
||||
pub memory_status: Option<String>,
|
||||
#[serde(default)]
|
||||
pub cpu_temp_c: Option<f32>,
|
||||
#[serde(default)]
|
||||
pub cpu_temp_status: Option<String>,
|
||||
#[serde(default)]
|
||||
pub cpu_cstate: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ServiceMetrics {
|
||||
pub summary: ServiceSummary,
|
||||
|
||||
@@ -12,7 +12,7 @@ use std::sync::{
|
||||
};
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::data::metrics::{BackupMetrics, ServiceMetrics, SmartMetrics};
|
||||
use crate::data::metrics::{BackupMetrics, ServiceMetrics, SmartMetrics, SystemMetrics};
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use chrono::{TimeZone, Utc};
|
||||
use clap::{ArgAction, Parser, Subcommand};
|
||||
@@ -316,6 +316,7 @@ fn handle_zmq_message(
|
||||
host,
|
||||
smart: Some(metrics),
|
||||
services: None,
|
||||
system: None,
|
||||
backup: None,
|
||||
timestamp,
|
||||
});
|
||||
@@ -335,6 +336,7 @@ fn handle_zmq_message(
|
||||
host,
|
||||
smart: None,
|
||||
services: Some(metrics),
|
||||
system: None,
|
||||
backup: None,
|
||||
timestamp,
|
||||
});
|
||||
@@ -348,12 +350,33 @@ fn handle_zmq_message(
|
||||
});
|
||||
}
|
||||
},
|
||||
AgentType::System => match serde_json::from_value::<SystemMetrics>(payload.clone()) {
|
||||
Ok(metrics) => {
|
||||
let _ = sender.send(AppEvent::MetricsUpdated {
|
||||
host,
|
||||
smart: None,
|
||||
services: None,
|
||||
system: Some(metrics),
|
||||
backup: None,
|
||||
timestamp,
|
||||
});
|
||||
}
|
||||
Err(error) => {
|
||||
warn!(%error, "failed to parse system metrics");
|
||||
let _ = sender.send(AppEvent::MetricsFailed {
|
||||
host,
|
||||
error: format!("system metrics parse error: {error:#}"),
|
||||
timestamp,
|
||||
});
|
||||
}
|
||||
},
|
||||
AgentType::Backup => match serde_json::from_value::<BackupMetrics>(payload.clone()) {
|
||||
Ok(metrics) => {
|
||||
let _ = sender.send(AppEvent::MetricsUpdated {
|
||||
host,
|
||||
smart: None,
|
||||
services: None,
|
||||
system: None,
|
||||
backup: Some(metrics),
|
||||
timestamp,
|
||||
});
|
||||
|
||||
@@ -2,8 +2,8 @@ use chrono::{DateTime, Utc};
|
||||
use ratatui::layout::Rect;
|
||||
use ratatui::Frame;
|
||||
|
||||
use crate::app::HostDisplayData;
|
||||
use crate::ui::system::{evaluate_performance, PerfSeverity};
|
||||
use crate::app::{HostDisplayData, ConnectionStatus};
|
||||
// Removed: evaluate_performance and PerfSeverity no longer needed
|
||||
use crate::ui::widget::{render_widget_data, WidgetData, WidgetStatus, StatusLevel};
|
||||
|
||||
pub fn render(frame: &mut Frame, hosts: &[HostDisplayData], area: Rect) {
|
||||
@@ -99,6 +99,14 @@ fn classify_hosts(hosts: &[HostDisplayData]) -> (AlertSeverity, usize, usize, us
|
||||
}
|
||||
|
||||
fn host_severity(host: &HostDisplayData) -> AlertSeverity {
|
||||
// Check connection status first
|
||||
match host.connection_status {
|
||||
ConnectionStatus::Error => return AlertSeverity::Critical,
|
||||
ConnectionStatus::Timeout => return AlertSeverity::Warning,
|
||||
ConnectionStatus::Unknown => return AlertSeverity::Unknown,
|
||||
ConnectionStatus::Connected => {}, // Continue with other checks
|
||||
}
|
||||
|
||||
if host.last_error.is_some() {
|
||||
return AlertSeverity::Critical;
|
||||
}
|
||||
@@ -120,12 +128,13 @@ fn host_severity(host: &HostDisplayData) -> AlertSeverity {
|
||||
return AlertSeverity::Warning;
|
||||
}
|
||||
|
||||
let (perf_severity, _) = evaluate_performance(&services.summary);
|
||||
match perf_severity {
|
||||
PerfSeverity::Critical => return AlertSeverity::Critical,
|
||||
PerfSeverity::Warning => return AlertSeverity::Warning,
|
||||
PerfSeverity::Ok => {}
|
||||
}
|
||||
// TODO: Update to use agent-provided system statuses instead of evaluate_performance
|
||||
// let (perf_severity, _) = evaluate_performance(&services.summary);
|
||||
// match perf_severity {
|
||||
// PerfSeverity::Critical => return AlertSeverity::Critical,
|
||||
// PerfSeverity::Warning => return AlertSeverity::Warning,
|
||||
// PerfSeverity::Ok => {}
|
||||
// }
|
||||
}
|
||||
|
||||
if let Some(backup) = host.backup.as_ref() {
|
||||
@@ -144,6 +153,30 @@ fn host_severity(host: &HostDisplayData) -> AlertSeverity {
|
||||
}
|
||||
|
||||
fn host_status(host: &HostDisplayData) -> (String, AlertSeverity, bool) {
|
||||
// Check connection status first
|
||||
match host.connection_status {
|
||||
ConnectionStatus::Error => {
|
||||
let msg = if let Some(error) = &host.last_error {
|
||||
format!("Connection error: {}", error)
|
||||
} else {
|
||||
"Connection error".to_string()
|
||||
};
|
||||
return (msg, AlertSeverity::Critical, true);
|
||||
},
|
||||
ConnectionStatus::Timeout => {
|
||||
let msg = if let Some(error) = &host.last_error {
|
||||
format!("Keep-alive timeout: {}", error)
|
||||
} else {
|
||||
"Keep-alive timeout".to_string()
|
||||
};
|
||||
return (msg, AlertSeverity::Warning, true);
|
||||
},
|
||||
ConnectionStatus::Unknown => {
|
||||
return ("No data received".to_string(), AlertSeverity::Unknown, true);
|
||||
},
|
||||
ConnectionStatus::Connected => {}, // Continue with other checks
|
||||
}
|
||||
|
||||
if let Some(error) = &host.last_error {
|
||||
return (format!("error: {}", error), AlertSeverity::Critical, true);
|
||||
}
|
||||
@@ -177,26 +210,27 @@ fn host_status(host: &HostDisplayData) -> (String, AlertSeverity, bool) {
|
||||
);
|
||||
}
|
||||
|
||||
let (perf_severity, reason) = evaluate_performance(&services.summary);
|
||||
if let Some(reason_text) = reason {
|
||||
match perf_severity {
|
||||
PerfSeverity::Critical => {
|
||||
return (
|
||||
format!("critical: {}", reason_text),
|
||||
AlertSeverity::Critical,
|
||||
true,
|
||||
);
|
||||
}
|
||||
PerfSeverity::Warning => {
|
||||
return (
|
||||
format!("warning: {}", reason_text),
|
||||
AlertSeverity::Warning,
|
||||
true,
|
||||
);
|
||||
}
|
||||
PerfSeverity::Ok => {}
|
||||
}
|
||||
}
|
||||
// TODO: Update to use agent-provided system statuses instead of evaluate_performance
|
||||
// let (perf_severity, reason) = evaluate_performance(&services.summary);
|
||||
// if let Some(reason_text) = reason {
|
||||
// match perf_severity {
|
||||
// PerfSeverity::Critical => {
|
||||
// return (
|
||||
// format!("critical: {}", reason_text),
|
||||
// AlertSeverity::Critical,
|
||||
// true,
|
||||
// );
|
||||
// }
|
||||
// PerfSeverity::Warning => {
|
||||
// return (
|
||||
// format!("warning: {}", reason_text),
|
||||
// AlertSeverity::Warning,
|
||||
// true,
|
||||
// );
|
||||
// }
|
||||
// PerfSeverity::Ok => {}
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
if let Some(backup) = host.backup.as_ref() {
|
||||
|
||||
@@ -3,20 +3,32 @@ use ratatui::Frame;
|
||||
|
||||
use crate::app::HostDisplayData;
|
||||
use crate::data::metrics::BackupMetrics;
|
||||
use crate::ui::widget::{render_placeholder, render_widget_data, status_level_from_agent_status, WidgetData, WidgetStatus, StatusLevel};
|
||||
use crate::ui::widget::{render_placeholder, render_widget_data, status_level_from_agent_status, connection_status_message, WidgetData, WidgetStatus, StatusLevel};
|
||||
use crate::app::ConnectionStatus;
|
||||
|
||||
pub fn render(frame: &mut Frame, host: Option<&HostDisplayData>, area: Rect) {
|
||||
match host {
|
||||
Some(data) => {
|
||||
if let Some(metrics) = data.backup.as_ref() {
|
||||
render_metrics(frame, data, metrics, area);
|
||||
} else {
|
||||
render_placeholder(
|
||||
frame,
|
||||
area,
|
||||
"Backups",
|
||||
&format!("Host {} awaiting backup metrics", data.name),
|
||||
);
|
||||
match (&data.connection_status, data.backup.as_ref()) {
|
||||
(ConnectionStatus::Connected, Some(metrics)) => {
|
||||
render_metrics(frame, data, metrics, area);
|
||||
}
|
||||
(ConnectionStatus::Connected, None) => {
|
||||
render_placeholder(
|
||||
frame,
|
||||
area,
|
||||
"Backups",
|
||||
&format!("Host {} awaiting backup metrics", data.name),
|
||||
);
|
||||
}
|
||||
(status, _) => {
|
||||
render_placeholder(
|
||||
frame,
|
||||
area,
|
||||
"Backups",
|
||||
&format!("Host {}: {}", data.name, connection_status_message(status, &data.last_error)),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
None => render_placeholder(frame, area, "Backups", "No hosts configured"),
|
||||
|
||||
@@ -3,20 +3,32 @@ use ratatui::Frame;
|
||||
|
||||
use crate::app::HostDisplayData;
|
||||
use crate::data::metrics::ServiceStatus;
|
||||
use crate::ui::widget::{render_placeholder, render_widget_data, status_level_from_agent_status, WidgetData, WidgetStatus, StatusLevel};
|
||||
use crate::ui::widget::{render_placeholder, render_widget_data, status_level_from_agent_status, connection_status_message, WidgetData, WidgetStatus, StatusLevel};
|
||||
use crate::app::ConnectionStatus;
|
||||
|
||||
pub fn render(frame: &mut Frame, host: Option<&HostDisplayData>, area: Rect) {
|
||||
match host {
|
||||
Some(data) => {
|
||||
if let Some(metrics) = data.services.as_ref() {
|
||||
render_metrics(frame, data, metrics, area);
|
||||
} else {
|
||||
render_placeholder(
|
||||
frame,
|
||||
area,
|
||||
"Services",
|
||||
&format!("Host {} has no service metrics yet", data.name),
|
||||
);
|
||||
match (&data.connection_status, data.services.as_ref()) {
|
||||
(ConnectionStatus::Connected, Some(metrics)) => {
|
||||
render_metrics(frame, data, metrics, area);
|
||||
}
|
||||
(ConnectionStatus::Connected, None) => {
|
||||
render_placeholder(
|
||||
frame,
|
||||
area,
|
||||
"Services",
|
||||
&format!("Host {} has no service metrics yet", data.name),
|
||||
);
|
||||
}
|
||||
(status, _) => {
|
||||
render_placeholder(
|
||||
frame,
|
||||
area,
|
||||
"Services",
|
||||
&format!("Host {}: {}", data.name, connection_status_message(status, &data.last_error)),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
None => render_placeholder(frame, area, "Services", "No hosts configured"),
|
||||
|
||||
@@ -3,20 +3,32 @@ use ratatui::Frame;
|
||||
|
||||
use crate::app::HostDisplayData;
|
||||
use crate::data::metrics::SmartMetrics;
|
||||
use crate::ui::widget::{render_placeholder, render_widget_data, status_level_from_agent_status, WidgetData, WidgetStatus, StatusLevel};
|
||||
use crate::ui::widget::{render_placeholder, render_widget_data, status_level_from_agent_status, connection_status_message, WidgetData, WidgetStatus, StatusLevel};
|
||||
use crate::app::ConnectionStatus;
|
||||
|
||||
pub fn render(frame: &mut Frame, host: Option<&HostDisplayData>, area: Rect) {
|
||||
match host {
|
||||
Some(data) => {
|
||||
if let Some(metrics) = data.smart.as_ref() {
|
||||
render_metrics(frame, data, metrics, area);
|
||||
} else {
|
||||
render_placeholder(
|
||||
frame,
|
||||
area,
|
||||
"Storage",
|
||||
&format!("Host {} has no SMART data yet", data.name),
|
||||
);
|
||||
match (&data.connection_status, data.smart.as_ref()) {
|
||||
(ConnectionStatus::Connected, Some(metrics)) => {
|
||||
render_metrics(frame, data, metrics, area);
|
||||
}
|
||||
(ConnectionStatus::Connected, None) => {
|
||||
render_placeholder(
|
||||
frame,
|
||||
area,
|
||||
"Storage",
|
||||
&format!("Host {} has no SMART data yet", data.name),
|
||||
);
|
||||
}
|
||||
(status, _) => {
|
||||
render_placeholder(
|
||||
frame,
|
||||
area,
|
||||
"Storage",
|
||||
&format!("Host {}: {}", data.name, connection_status_message(status, &data.last_error)),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
None => render_placeholder(frame, area, "Storage", "No hosts configured"),
|
||||
|
||||
@@ -2,24 +2,36 @@ use ratatui::layout::Rect;
|
||||
use ratatui::Frame;
|
||||
|
||||
use crate::app::HostDisplayData;
|
||||
use crate::data::metrics::{ServiceMetrics, ServiceSummary};
|
||||
use crate::data::metrics::SystemMetrics;
|
||||
use crate::ui::widget::{
|
||||
render_placeholder, render_combined_widget_data,
|
||||
status_level_from_agent_status, WidgetDataSet, WidgetStatus, StatusLevel,
|
||||
status_level_from_agent_status, connection_status_message, WidgetDataSet, WidgetStatus, StatusLevel,
|
||||
};
|
||||
use crate::app::ConnectionStatus;
|
||||
|
||||
pub fn render(frame: &mut Frame, host: Option<&HostDisplayData>, area: Rect) {
|
||||
match host {
|
||||
Some(data) => {
|
||||
if let Some(metrics) = data.services.as_ref() {
|
||||
render_metrics(frame, data, metrics, area);
|
||||
} else {
|
||||
render_placeholder(
|
||||
frame,
|
||||
area,
|
||||
"System",
|
||||
&format!("Host {} awaiting service metrics", data.name),
|
||||
);
|
||||
match (&data.connection_status, data.system.as_ref()) {
|
||||
(ConnectionStatus::Connected, Some(metrics)) => {
|
||||
render_metrics(frame, data, metrics, area);
|
||||
}
|
||||
(ConnectionStatus::Connected, None) => {
|
||||
render_placeholder(
|
||||
frame,
|
||||
area,
|
||||
"System",
|
||||
&format!("Host {} awaiting system metrics", data.name),
|
||||
);
|
||||
}
|
||||
(status, _) => {
|
||||
render_placeholder(
|
||||
frame,
|
||||
area,
|
||||
"System",
|
||||
&format!("Host {}: {}", data.name, connection_status_message(status, &data.last_error)),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
None => render_placeholder(frame, area, "System", "No hosts configured"),
|
||||
@@ -29,30 +41,12 @@ pub fn render(frame: &mut Frame, host: Option<&HostDisplayData>, area: Rect) {
|
||||
fn render_metrics(
|
||||
frame: &mut Frame,
|
||||
_host: &HostDisplayData,
|
||||
metrics: &ServiceMetrics,
|
||||
metrics: &SystemMetrics,
|
||||
area: Rect,
|
||||
) {
|
||||
let summary = &metrics.summary;
|
||||
let system_total = if summary.system_memory_total_mb > 0.0 {
|
||||
summary.system_memory_total_mb
|
||||
} else {
|
||||
summary.memory_quota_mb
|
||||
};
|
||||
let system_used = if summary.system_memory_used_mb > 0.0 {
|
||||
summary.system_memory_used_mb
|
||||
} else {
|
||||
summary.memory_used_mb
|
||||
};
|
||||
let _usage_ratio = if system_total > 0.0 {
|
||||
(system_used / system_total) * 100.0
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
||||
let (perf_severity, _reason) = evaluate_performance(summary);
|
||||
// Dashboard should NOT calculate border colors - agent is the source of truth
|
||||
|
||||
// Use agent-calculated statuses instead of dashboard calculations
|
||||
|
||||
// Use agent-calculated statuses
|
||||
let memory_status = status_level_from_agent_status(summary.memory_status.as_ref());
|
||||
let cpu_status = status_level_from_agent_status(summary.cpu_status.as_ref());
|
||||
// Dashboard should NOT calculate colors - agent is the source of truth
|
||||
@@ -62,7 +56,7 @@ fn render_metrics(
|
||||
memory_dataset.add_row(
|
||||
Some(WidgetStatus::new(memory_status)),
|
||||
vec![],
|
||||
vec![format!("{:.1} / {:.1} GB", system_used / 1000.0, system_total / 1000.0)],
|
||||
vec![format!("{:.1} / {:.1} GB", summary.memory_used_mb / 1000.0, summary.memory_total_mb / 1000.0)],
|
||||
);
|
||||
|
||||
// CPU dataset - use agent-calculated status
|
||||
@@ -140,30 +134,24 @@ fn render_metrics(
|
||||
);
|
||||
}
|
||||
|
||||
// GPU dataset
|
||||
// GPU status should come from agent when available
|
||||
let gpu_status = StatusLevel::Unknown; // Default until agent provides gpu_status
|
||||
// GPU dataset - GPU data remains in ServiceMetrics, not SystemMetrics
|
||||
let gpu_status = StatusLevel::Unknown; // GPU not available in SystemMetrics
|
||||
let mut gpu_dataset = WidgetDataSet::new(vec!["GPU load".to_string(), "GPU temp".to_string()], Some(WidgetStatus::new(gpu_status)));
|
||||
gpu_dataset.add_row(
|
||||
Some(WidgetStatus::new(gpu_status)),
|
||||
vec![],
|
||||
vec![
|
||||
summary
|
||||
.gpu_load_percent
|
||||
.map(|value| format_optional_percent(Some(value)))
|
||||
.unwrap_or_else(|| "—".to_string()),
|
||||
summary
|
||||
.gpu_temp_c
|
||||
.map(|value| format_optional_metric(Some(value), "°C"))
|
||||
.unwrap_or_else(|| "—".to_string()),
|
||||
"—".to_string(), // GPU data not in SystemMetrics
|
||||
"—".to_string(), // GPU data not in SystemMetrics
|
||||
],
|
||||
);
|
||||
|
||||
// Determine overall widget status based on worst case
|
||||
let overall_status_level = match perf_severity {
|
||||
PerfSeverity::Critical => StatusLevel::Error,
|
||||
PerfSeverity::Warning => StatusLevel::Warning,
|
||||
PerfSeverity::Ok => StatusLevel::Ok,
|
||||
// Determine overall widget status based on worst case from agent statuses
|
||||
let overall_status_level = match (memory_status, cpu_status) {
|
||||
(StatusLevel::Error, _) | (_, StatusLevel::Error) => StatusLevel::Error,
|
||||
(StatusLevel::Warning, _) | (_, StatusLevel::Warning) => StatusLevel::Warning,
|
||||
(StatusLevel::Ok, StatusLevel::Ok) => StatusLevel::Ok,
|
||||
_ => StatusLevel::Unknown,
|
||||
};
|
||||
let overall_status = Some(WidgetStatus::new(overall_status_level));
|
||||
|
||||
@@ -171,13 +159,6 @@ fn render_metrics(
|
||||
render_combined_widget_data(frame, area, "System".to_string(), overall_status, vec![memory_dataset, cpu_dataset, cstate_dataset, gpu_dataset]);
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub(crate) enum PerfSeverity {
|
||||
Ok,
|
||||
Warning,
|
||||
Critical,
|
||||
}
|
||||
|
||||
fn format_optional_metric(value: Option<f32>, unit: &str) -> String {
|
||||
match value {
|
||||
Some(number) => format!("{:.1}{}", number, unit),
|
||||
@@ -191,62 +172,3 @@ fn format_optional_percent(value: Option<f32>) -> String {
|
||||
None => "—".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub(crate) fn evaluate_performance(summary: &ServiceSummary) -> (PerfSeverity, Option<String>) {
|
||||
let mem_percent = if summary.system_memory_total_mb > 0.0 {
|
||||
(summary.system_memory_used_mb / summary.system_memory_total_mb) * 100.0
|
||||
} else if summary.memory_quota_mb > 0.0 {
|
||||
(summary.memory_used_mb / summary.memory_quota_mb) * 100.0
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
||||
let mut severity = PerfSeverity::Ok;
|
||||
let mut reason: Option<String> = None;
|
||||
|
||||
let mut consider = |level: PerfSeverity, message: String| {
|
||||
if level > severity {
|
||||
severity = level;
|
||||
reason = Some(message);
|
||||
}
|
||||
};
|
||||
|
||||
// Use agent's memory status instead of hardcoded thresholds
|
||||
if let Some(memory_status) = &summary.memory_status {
|
||||
match memory_status.as_str() {
|
||||
"critical" => consider(PerfSeverity::Critical, format!("RAM {:.0}%", mem_percent)),
|
||||
"warning" => consider(PerfSeverity::Warning, format!("RAM {:.0}%", mem_percent)),
|
||||
_ => {} // "ok" - no alert needed
|
||||
}
|
||||
}
|
||||
|
||||
// Use agent's CPU status instead of hardcoded thresholds
|
||||
if let Some(cpu_status) = &summary.cpu_status {
|
||||
match cpu_status.as_str() {
|
||||
"critical" => consider(PerfSeverity::Critical, format!("CPU load {:.2}", summary.cpu_load_5)),
|
||||
"warning" => consider(PerfSeverity::Warning, format!("CPU load {:.2}", summary.cpu_load_5)),
|
||||
_ => {} // "ok" - no alert needed
|
||||
}
|
||||
}
|
||||
|
||||
// Use agent's CPU temperature status instead of hardcoded thresholds
|
||||
if let Some(cpu_temp_status) = &summary.cpu_temp_status {
|
||||
if let Some(temp) = summary.cpu_temp_c {
|
||||
match cpu_temp_status.as_str() {
|
||||
"critical" => consider(PerfSeverity::Critical, format!("CPU temp {:.0}°C", temp)),
|
||||
"warning" => consider(PerfSeverity::Warning, format!("CPU temp {:.0}°C", temp)),
|
||||
_ => {} // "ok" - no alert needed
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: GPU status should come from agent, not calculated here with hardcoded thresholds
|
||||
// For now, remove hardcoded GPU thresholds until agent provides gpu_status
|
||||
|
||||
if severity == PerfSeverity::Ok {
|
||||
(PerfSeverity::Ok, None)
|
||||
} else {
|
||||
(severity, reason)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,6 +37,28 @@ pub fn status_level_from_agent_status(agent_status: Option<&String>) -> StatusLe
|
||||
}
|
||||
}
|
||||
|
||||
pub fn connection_status_message(connection_status: &crate::app::ConnectionStatus, last_error: &Option<String>) -> String {
|
||||
use crate::app::ConnectionStatus;
|
||||
match connection_status {
|
||||
ConnectionStatus::Connected => "Connected".to_string(),
|
||||
ConnectionStatus::Timeout => {
|
||||
if let Some(error) = last_error {
|
||||
format!("Timeout: {}", error)
|
||||
} else {
|
||||
"Keep-alive timeout".to_string()
|
||||
}
|
||||
},
|
||||
ConnectionStatus::Error => {
|
||||
if let Some(error) = last_error {
|
||||
format!("Error: {}", error)
|
||||
} else {
|
||||
"Connection error".to_string()
|
||||
}
|
||||
},
|
||||
ConnectionStatus::Unknown => "No data received".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
pub fn render_placeholder(frame: &mut Frame, area: Rect, title: &str, message: &str) {
|
||||
|
||||
Reference in New Issue
Block a user