use crate::AppState; use tracing::{info, warn, error}; /// Background task for data cleanup and alert processing pub async fn cleanup_task(state: AppState) { let mut interval = tokio::time::interval(tokio::time::Duration::from_secs(3600)); loop { interval.tick().await; // Cleanup old status history if let Err(e) = sqlx::query( "DELETE FROM device_status_history WHERE reported_at < datetime('now', ?)" ) .bind(format!("-{} days", state.config.retention.status_history_days)) .execute(&state.db) .await { error!("Failed to cleanup status history: {}", e); } // Cleanup old USB events if let Err(e) = sqlx::query( "DELETE FROM usb_events WHERE event_time < datetime('now', ?)" ) .bind(format!("-{} days", state.config.retention.usb_events_days)) .execute(&state.db) .await { error!("Failed to cleanup USB events: {}", e); } // Cleanup handled alert records if let Err(e) = sqlx::query( "DELETE FROM alert_records WHERE handled = 1 AND triggered_at < datetime('now', ?)" ) .bind(format!("-{} days", state.config.retention.alert_records_days)) .execute(&state.db) .await { error!("Failed to cleanup alert records: {}", e); } // Mark devices as offline if no heartbeat for 2 minutes if let Err(e) = sqlx::query( "UPDATE devices SET status = 'offline' WHERE status = 'online' AND last_heartbeat < datetime('now', '-2 minutes')" ) .execute(&state.db) .await { error!("Failed to mark stale devices offline: {}", e); } // SQLite WAL checkpoint if let Err(e) = sqlx::query("PRAGMA wal_checkpoint(TRUNCATE)") .execute(&state.db) .await { warn!("WAL checkpoint failed: {}", e); } info!("Cleanup cycle completed"); } } /// Send email notification pub async fn send_email( smtp_config: &crate::config::SmtpConfig, to: &str, subject: &str, body: &str, ) -> anyhow::Result<()> { use lettre::message::header::ContentType; use lettre::{Message, SmtpTransport, Transport}; use lettre::transport::smtp::authentication::Credentials; let email = Message::builder() .from(smtp_config.from.parse()?) .to(to.parse()?) .subject(subject) .header(ContentType::TEXT_HTML) .body(body.to_string())?; let creds = Credentials::new( smtp_config.username.clone(), smtp_config.password.clone(), ); let mailer = SmtpTransport::starttls_relay(&smtp_config.host)? .port(smtp_config.port) .credentials(creds) .build(); mailer.send(&email)?; Ok(()) } /// Shared HTTP client for webhook notifications. /// Lazily initialized once and reused across calls to benefit from connection pooling. static WEBHOOK_CLIENT: std::sync::OnceLock = std::sync::OnceLock::new(); fn webhook_client() -> &'static reqwest::Client { WEBHOOK_CLIENT.get_or_init(|| { reqwest::Client::builder() .timeout(std::time::Duration::from_secs(10)) .build() .unwrap_or_else(|_| reqwest::Client::new()) }) } /// Send webhook notification pub async fn send_webhook(url: &str, payload: &serde_json::Value) -> anyhow::Result<()> { webhook_client().post(url) .json(payload) .send() .await?; Ok(()) }