From 338c4457a575bab90e61c30998944ae6d667ada4 Mon Sep 17 00:00:00 2001 From: Christoffer Martinsson Date: Tue, 21 Oct 2025 19:48:55 +0200 Subject: [PATCH] Remove legacy notification code and fix all warnings --- agent/src/notifications/mod.rs | 252 ++++----------------------------- agent/src/status/mod.rs | 31 +--- dashboard/src/ui/mod.rs | 2 - 3 files changed, 36 insertions(+), 249 deletions(-) diff --git a/agent/src/notifications/mod.rs b/agent/src/notifications/mod.rs index 63bdfe8..c5194b8 100644 --- a/agent/src/notifications/mod.rs +++ b/agent/src/notifications/mod.rs @@ -1,254 +1,64 @@ -use cm_dashboard_shared::Status; -use std::collections::HashMap; -use std::fs; -use std::path::Path; -use tracing::{debug, info, error, warn}; -use chrono::{DateTime, Utc}; -use chrono_tz::Europe::Stockholm; -use lettre::{Message, SmtpTransport, Transport}; -use serde::{Serialize, Deserialize}; - use crate::config::NotificationConfig; +use anyhow::Result; +use chrono::Utc; +use lettre::transport::smtp::SmtpTransport; +use lettre::{Message, Transport}; +use tracing::{debug, error, info}; -/// Persisted status data -#[derive(Debug, Clone, Serialize, Deserialize)] -struct PersistedStatus { - metric_statuses: HashMap, - metric_details: HashMap, -} - -/// Manages status change tracking and notifications +/// Manages notifications pub struct NotificationManager { config: NotificationConfig, - hostname: String, - metric_statuses: HashMap, - metric_details: HashMap, // Store details for warning/critical states - status_file_path: String, -} - -/// Status change information -#[derive(Debug, Clone)] -pub struct StatusChange { - pub metric_name: String, - pub old_status: Status, - pub new_status: Status, - pub timestamp: DateTime, - pub details: Option, } impl NotificationManager { - pub fn new(config: &NotificationConfig, hostname: &str) -> Result { - info!("Initializing notification manager for {}", hostname); - - let status_file_path = "/var/lib/cm-dashboard/last-status.json".to_string(); - - // Create directory if it doesn't exist - if let Some(parent) = Path::new(&status_file_path).parent() { - if let Err(e) = fs::create_dir_all(parent) { - warn!("Failed to create status directory {}: {}", parent.display(), e); - } - } - - // Load previous status from disk - let (metric_statuses, metric_details) = Self::load_status(&status_file_path); - + pub fn new(config: &NotificationConfig, _hostname: &str) -> Result { Ok(Self { config: config.clone(), - hostname: hostname.to_string(), - metric_statuses, - metric_details, - status_file_path, }) } - - /// Send notification for status change - pub async fn send_status_change_notification( - &mut self, - mut status_change: StatusChange, - metric: &cm_dashboard_shared::Metric, - ) -> Result<(), anyhow::Error> { + pub async fn send_direct_email(&mut self, subject: &str, body: &str) -> Result<()> { if !self.config.enabled { return Ok(()); } - // Only notify on transitions to warning/critical, or recovery to ok - let should_send = match (status_change.old_status, status_change.new_status) { - (_, Status::Warning) | (_, Status::Critical) => true, - (Status::Warning | Status::Critical | Status::Unknown, Status::Ok) => true, - _ => false, - }; - - if !should_send { - return Ok(()); - } - - // Check maintenance mode if self.is_maintenance_mode() { - debug!( - "Maintenance mode active, suppressing notification for {}", - status_change.metric_name - ); + debug!("Maintenance mode active, suppressing email notification"); return Ok(()); } + let hostname = gethostname::gethostname() + .to_string_lossy() + .to_string(); - // Add metric details to status change - status_change.details = Some(self.format_metric_details(metric)); - - // For recovery notifications, include original problem details - let is_recovery = status_change.new_status == Status::Ok; - - if is_recovery { - if let Some(old_details) = self.metric_details.get(&status_change.metric_name) { - status_change.details = Some(format!( - "Recovered from: {}\nCurrent status: {}", - old_details, - status_change.details.unwrap_or_default() - )); - } - // Clear stored details after recovery - self.metric_details.remove(&status_change.metric_name); - } else if status_change.new_status == Status::Warning || status_change.new_status == Status::Critical { - // Store details for warning/critical states - if let Some(ref details) = status_change.details { - self.metric_details.insert(status_change.metric_name.clone(), details.clone()); - } - } - - // Save status after updating details - self.save_status(); - - // Send the actual email - if let Err(e) = self.send_email(&status_change).await { - error!("Failed to send notification email: {}", e); - } else { - info!( - "Sent notification: {} {:?} → {:?}", - status_change.metric_name, status_change.old_status, status_change.new_status - ); - } - - - Ok(()) - } - - /// Check if maintenance mode is active - fn is_maintenance_mode(&self) -> bool { - std::fs::metadata("/tmp/cm-maintenance").is_ok() - } - - - /// Format metric details for notification - fn format_metric_details(&self, metric: &cm_dashboard_shared::Metric) -> String { - format!("Value: {}", metric.value.as_string()) - } - - /// Format email subject - fn format_subject(&self, change: &StatusChange) -> String { - let urgency = match change.new_status { - Status::Critical => "🔴 CRITICAL", - Status::Warning => "🟡 WARNING", - Status::Ok => "✅ RESOLVED", - Status::Pending => "âŗ PENDING", - Status::Unknown => "â„šī¸ STATUS", - }; - - format!("{}: {} on {}", urgency, change.metric_name, self.hostname) - } - - /// Format email body - fn format_body(&self, change: &StatusChange) -> String { - let mut body = format!( - "Status Change Alert\n\ - \n\ - Host: {}\n\ - Metric: {}\n\ - Status Change: {:?} → {:?}\n\ - Time: {}", - self.hostname, - change.metric_name, - change.old_status, - change.new_status, - change.timestamp.with_timezone(&Stockholm).format("%Y-%m-%d %H:%M:%S CET/CEST") + let from_email = self.config.from_email.replace("{hostname}", &hostname); + + let email_body = format!( + "{}\n\n--\nCM Dashboard Agent\nGenerated at {}", + body, + Utc::now().format("%Y-%m-%d %H:%M:%S %Z") ); - if let Some(details) = &change.details { - body.push_str(&format!("\n\nDetails:\n{}", details)); - } - - body.push_str(&format!( - "\n\n--\n\ - CM Dashboard Agent\n\ - Generated at {}", - Utc::now().with_timezone(&Stockholm).format("%Y-%m-%d %H:%M:%S CET/CEST") - )); - - body - } - - /// Send email notification - async fn send_email(&self, change: &StatusChange) -> Result<(), Box> { - let subject = self.format_subject(change); - let body = self.format_body(change); - - // Replace {hostname} placeholder in from_email - let from_email = self.config.from_email.replace("{hostname}", &self.hostname); - let email = Message::builder() .from(from_email.parse()?) .to(self.config.to_email.parse()?) .subject(subject) - .body(body)?; + .body(email_body)?; - let mailer = SmtpTransport::builder_dangerous(&self.config.smtp_host) - .port(self.config.smtp_port) - .build(); + let mailer = SmtpTransport::unencrypted_localhost(); + + match mailer.send(&email) { + Ok(_) => info!("Direct email sent successfully: {}", subject), + Err(e) => { + error!("Failed to send email: {}", e); + return Err(e.into()); + } + } - mailer.send(&email)?; Ok(()) } - - /// Load status from disk - fn load_status(file_path: &str) -> (HashMap, HashMap) { - match fs::read_to_string(file_path) { - Ok(content) => { - match serde_json::from_str::(&content) { - Ok(persisted) => { - info!("Loaded {} metric statuses from {}", persisted.metric_statuses.len(), file_path); - (persisted.metric_statuses, persisted.metric_details) - } - Err(e) => { - warn!("Failed to parse status file {}: {}", file_path, e); - (HashMap::new(), HashMap::new()) - } - } - } - Err(_) => { - info!("No previous status file found at {}, starting fresh", file_path); - (HashMap::new(), HashMap::new()) - } - } + fn is_maintenance_mode(&self) -> bool { + std::fs::metadata("/tmp/cm-maintenance").is_ok() } - - /// Save status to disk - fn save_status(&self) { - let persisted = PersistedStatus { - metric_statuses: self.metric_statuses.clone(), - metric_details: self.metric_details.clone(), - }; - - match serde_json::to_string_pretty(&persisted) { - Ok(content) => { - if let Err(e) = fs::write(&self.status_file_path, content) { - warn!("Failed to save status to {}: {}", self.status_file_path, e); - } - } - Err(e) => { - warn!("Failed to serialize status: {}", e); - } - } - } - -} +} \ No newline at end of file diff --git a/agent/src/status/mod.rs b/agent/src/status/mod.rs index b5ed9fd..18a6d17 100644 --- a/agent/src/status/mod.rs +++ b/agent/src/status/mod.rs @@ -3,7 +3,6 @@ use std::collections::HashMap; use std::time::Instant; use tracing::{debug, info, error}; use serde::{Deserialize, Serialize}; -use crate::notifications::StatusChange; use chrono::Utc; #[derive(Debug, Clone, Serialize, Deserialize)] @@ -32,7 +31,6 @@ pub struct StatusChangeSummary { pub initial_status: Status, pub final_status: Status, pub change_count: usize, - pub significant_change: bool, // true if needs notification } #[derive(Debug, Clone)] @@ -191,7 +189,7 @@ impl HostStatusManager { info!("Sending aggregated notification for {} service changes", aggregated.service_summaries.len()); // Send aggregated notification - if let Err(e) = self.send_aggregated_notification(&aggregated, notification_manager).await { + if let Err(e) = self.send_aggregated_email(&aggregated, notification_manager).await { error!("Failed to send aggregated notification: {}", e); } } else { @@ -220,7 +218,6 @@ impl HostStatusManager { initial_status: *initial_status, final_status: *final_status, change_count: *change_count, - significant_change, }); } @@ -251,22 +248,11 @@ impl HostStatusManager { } } - /// Send aggregated notification email - async fn send_aggregated_notification( + async fn send_aggregated_email( &self, aggregated: &AggregatedStatusChanges, notification_manager: &mut crate::notifications::NotificationManager, ) -> Result<(), Box> { - // Create a summary status change for the notification system - let summary_change = StatusChange { - metric_name: "host_status_summary".to_string(), - old_status: aggregated.host_status_initial, - new_status: aggregated.host_status_final, - timestamp: Utc::now(), - details: Some(self.format_aggregated_details(aggregated)), - }; - - // Create a descriptive summary based on change types let mut summary_parts = Vec::new(); let critical_count = aggregated.service_summaries.iter().filter(|s| s.final_status == Status::Critical).count(); let warning_count = aggregated.service_summaries.iter().filter(|s| s.final_status == Status::Warning).count(); @@ -288,17 +274,10 @@ impl HostStatusManager { summary_parts.join(", ") }; - // Create a dummy metric for the notification - let summary_metric = Metric { - name: "host_status_summary".to_string(), - value: cm_dashboard_shared::MetricValue::String(summary_text), - status: aggregated.host_status_final, - timestamp: Utc::now().timestamp() as u64, - description: Some("Aggregated status summary".to_string()), - unit: None, - }; + let subject = format!("Status Alert: {}", summary_text); + let body = self.format_aggregated_details(aggregated); - notification_manager.send_status_change_notification(summary_change, &summary_metric).await.map_err(|e| e.into()) + notification_manager.send_direct_email(&subject, &body).await.map_err(|e| e.into()) } /// Format details for aggregated notification diff --git a/dashboard/src/ui/mod.rs b/dashboard/src/ui/mod.rs index 0e79e95..fa7aa7f 100644 --- a/dashboard/src/ui/mod.rs +++ b/dashboard/src/ui/mod.rs @@ -367,10 +367,8 @@ impl TuiApp { let mut has_warning = false; let mut has_pending = false; let mut ok_count = 0; - let mut total_count = 0; for metric in &metrics { - total_count += 1; match metric.status { Status::Critical => has_critical = true, Status::Warning => has_warning = true,