//! cors 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 { #[instrument(skip_all)] pub async fn send_cors_failure_alert( &self, target: &str, label: &str, failures: &[crate::types::CorsCheckResult], ) { let alert_key = format!("cors:{target}"); let n = failures.len(); let subject = format!("[PoM] {label}: {n} CORS preflight(s) failing"); let details: Vec = failures .iter() .map(|f| { if let Some(ref err) = f.error { format!(" - {} {} from {}: {err}", f.method, f.url, f.origin) } else { format!( " - {} {} from {}: no CORS headers", f.method, f.url, f.origin ) } }) .collect(); let body = format!( "Target: {label} ({target})\n\ CORS preflight failures:\n{}\n\ Instance: {}\n\ Time: {}\n\n\ Browser-side uploads will silently fail without CORS.\n\n\ - PoM", details.join("\n"), self.instance_name, chrono::Utc::now().to_rfc3339(), ); self.fire_failure( &subject, &body, "high", "pom-cors", Some(target), AlertMeta { key: &alert_key, category: AlertCategory::CorsFailure, from: None, to: None, error: None, }, ) .await; } #[instrument(skip_all)] pub async fn send_cors_recovery_alert(&self, target: &str, label: &str) { let alert_key = format!("cors:{target}"); let subject = format!("[PoM] {label}: CORS preflights recovered"); let body = format!( "Target: {label} ({target})\n\ All CORS preflight checks passing.\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::CorsRecovery, from: None, to: None, error: None, }, ) .await; } }