use axum::extract::State; use axum::response::Json; use axum::routing::get; use axum::Router; use serde_json::{json, Value}; use std::sync::Arc; use std::time::Instant; use tokio::sync::watch; pub struct HealthState { pub started_at: Instant, pub cycle_count: watch::Receiver, pub last_cycle_ok: watch::Receiver, pub db_path: String, pub nip46_client: Option>, } pub async fn start_health_server(port: u16, state: Arc) -> anyhow::Result<()> { let app = Router::new() .route("/health", get(health_handler)) .route("/api/mirror-health", get(health_handler)) .with_state(state); let listener = tokio::net::TcpListener::bind(format!("127.0.0.1:{}", port)).await?; tracing::info!(port, "health server listening"); axum::serve(listener, app).await?; Ok(()) } async fn health_handler(State(state): State>) -> Json { let uptime = state.started_at.elapsed(); let cycle_count = *state.cycle_count.borrow(); let last_ok = *state.last_cycle_ok.borrow(); let nip46_sessions = if let Some(ref client) = state.nip46_client { let statuses = client.get_status().await; statuses .into_iter() .map(|s| { json!({ "npub": s.npub, "connected": s.connected, "pairing_uri": s.pairing_uri, }) }) .collect::>() } else { vec![] }; Json(json!({ "status": if last_ok || cycle_count == 0 { "ok" } else { "degraded" }, "uptime_secs": uptime.as_secs(), "cycle_count": cycle_count, "last_cycle_ok": last_ok, "nip46": nip46_sessions, })) }