//! PoM (Proof of Monitoring) types and helpers for the health dashboard. use serde::Deserialize; /// Deserialized snapshot from PoM's API response. #[derive(Deserialize, Clone)] pub(super) struct PomSnapshotJson { pub status: String, pub checked_at: String, pub response_time_ms: i64, } /// Deserialized incident from PoM's API response. #[derive(Deserialize, Clone)] #[allow(dead_code)] pub(super) struct PomIncidentJson { pub started_at: String, pub ended_at: Option, pub duration_secs: Option, pub from_status: String, pub to_status: String, } /// Deserialized latency stats from PoM's API response. #[derive(Deserialize, Clone)] pub(super) struct PomLatencyJson { pub avg_ms: f64, pub p95_ms: i64, } /// Deserialized route status from PoM's API response. #[derive(Deserialize, Clone)] pub(super) struct PomRouteStatusJson { pub path: String, #[allow(dead_code)] pub status_code: i64, pub ok: bool, #[allow(dead_code)] pub checked_at: String, #[allow(dead_code)] pub response_time_ms: i64, } /// Response from PoM's `GET /api/status/{target}` endpoint. #[derive(Deserialize)] pub(super) struct PomTargetResponse { pub latest: Option, pub recent: Vec, pub uptime_24h: Option, pub uptime_7d: Option, #[serde(default)] pub latency_24h: Option, #[serde(default)] pub current_incident: Option, #[serde(default)] pub incidents: Vec, #[serde(default)] pub route_status: Vec, } /// Fetch external monitoring data from the local PoM API. pub(super) async fn fetch_pom_status() -> Option { let client = reqwest::Client::builder() .timeout(std::time::Duration::from_secs(2)) .build() .ok()?; let mut req = client.get("http://127.0.0.1:9100/api/status/mnw"); if let Ok(token) = std::env::var("POM_API_TOKEN") { req = req.bearer_auth(token); } req.send().await.ok()?.json::().await.ok() } /// Format an RFC3339 timestamp into a shorter display form (e.g. "14:30 UTC" or "Mar 11, 14:30"). pub(super) fn format_pom_timestamp(rfc3339: &str) -> String { chrono::DateTime::parse_from_rfc3339(rfc3339) .map(|dt| { let utc = dt.with_timezone(&chrono::Utc); let now = chrono::Utc::now(); if utc.date_naive() == now.date_naive() { utc.format("%H:%M UTC").to_string() } else { utc.format("%b %d, %H:%M").to_string() } }) .unwrap_or_else(|_| rfc3339.to_string()) } /// Format a duration in seconds as a human-readable string (e.g. "2h 15m", "45m", "3d 1h"). pub(super) fn format_incident_duration(secs: i64) -> String { let days = secs / 86400; let hours = (secs % 86400) / 3600; let minutes = (secs % 3600) / 60; if days > 0 { format!("{}d {}h", days, hours) } else if hours > 0 { format!("{}h {}m", hours, minutes) } else { format!("{}m", minutes.max(1)) } }