//! scan alert/recovery messages. Domain half of the Alerter split; //! shared dispatch/cooldown plumbing lives in the parent module. use tracing::instrument; use super::{AlertCategory, AlertMeta, Alerter}; impl Alerter { /// Fire when the scan pipeline transitions from operational → degraded or /// unreachable. Includes the audit-doc threshold issues that fired. #[instrument(skip_all)] pub async fn send_scan_pipeline_alert( &self, target: &str, label: &str, status: &str, issues: &[String], ) { let alert_key = format!("scan_pipeline:{target}"); let subject = format!("[PoM] {label}: scan pipeline {status}"); let body = format!( "Target: {label} ({target})\n\ Status: {status}\n\ Issues:\n{}\n\ Instance: {}\n\ Time: {}\n\n\ Dashboard: \n\n\ - PoM", issues .iter() .map(|i| format!(" - {i}")) .collect::>() .join("\n"), self.instance_name, chrono::Utc::now().to_rfc3339(), target, ); let priority = if status == "unreachable" { "high" } else { "medium" }; self.fire_failure( &subject, &body, priority, "pom-scan-pipeline", Some(target), AlertMeta { key: &alert_key, category: AlertCategory::ScanPipelineDegraded, from: None, to: Some(status), error: None, }, ) .await; } /// Fire on recovery from a degraded / unreachable scan-pipeline state. #[instrument(skip_all)] pub async fn send_scan_pipeline_recovery(&self, target: &str, label: &str) { let alert_key = format!("scan_pipeline:{target}"); let subject = format!("[PoM] {label}: scan pipeline recovered"); let body = format!( "Target: {label} ({target})\n\ Scan pipeline is operational.\n\ Instance: {}\n\ Time: {}\n\n\ - PoM", self.instance_name, chrono::Utc::now().to_rfc3339(), ); self.fire_recovery( &subject, &body, AlertMeta { key: &alert_key, category: AlertCategory::ScanPipelineRecovery, from: None, to: Some("operational"), error: None, }, ) .await; } }