use crate::admin::db::{backend_history_30d, backend_uptime_pct};
use crate::admin::state::{with_db, SharedState};
use axum::{extract::State, Json};
use serde::Serialize;
#[derive(Serialize)]
pub struct HistoryDay {
date: String,
status: String,
}
#[derive(Serialize)]
pub struct ProxyUptimeInfo {
started_at: u64,
uptime_pct_30d: f64,
history: Vec<HistoryDay>,
}
#[derive(Serialize)]
pub struct BackendUptimeInfo {
name: String,
status: String,
last_checked_at: Option<i64>,
last_latency_ms: Option<i64>,
uptime_pct_30d: f64,
history: Vec<HistoryDay>,
}
#[derive(Serialize)]
pub struct UptimeResponse {
proxy: ProxyUptimeInfo,
backends: Vec<BackendUptimeInfo>,
}
pub(super) async fn get_uptime(State(shared): State<SharedState>) -> Json<UptimeResponse> {
let started_at = shared
.started_at
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
let backend_names: Vec<String> = {
let cfg = shared
.runtime_config
.read()
.unwrap_or_else(|e| e.into_inner());
cfg.model_mappings.keys().cloned().collect()
};
let response = with_db(&shared.db, move |conn| {
let proxy_history: Vec<HistoryDay> = conn
.prepare(
"SELECT date(checked_at, 'unixepoch') AS day,
MIN(CASE WHEN status='down' THEN 0 ELSE 1 END) AS all_up
FROM health_checks
WHERE checked_at >= strftime('%s','now') - 30*86400
GROUP BY day
ORDER BY day ASC",
)
.ok()
.and_then(|mut stmt| {
stmt.query_map([], |r| {
Ok(HistoryDay {
date: r.get(0)?,
status: if r.get::<_, i64>(1)? == 1 {
"up".to_string()
} else {
"down".to_string()
},
})
})
.ok()
.map(|rows| rows.filter_map(|r| r.ok()).collect())
})
.unwrap_or_default();
let backends: Vec<BackendUptimeInfo> = backend_names
.iter()
.map(|name| {
let uptime_pct = backend_uptime_pct(conn, name).unwrap_or(100.0);
let history = backend_history_30d(conn, name)
.unwrap_or_default()
.into_iter()
.map(|(date, status)| HistoryDay { date, status })
.collect();
let (last_checked_at, last_latency_ms, current_status) = conn
.query_row(
"SELECT checked_at, latency_ms, status FROM health_checks
WHERE backend = ?1 ORDER BY checked_at DESC LIMIT 1",
[name.as_str()],
|r| {
Ok((
r.get::<_, i64>(0)?,
r.get::<_, Option<i64>>(1)?,
r.get::<_, String>(2)?,
))
},
)
.map(|(ts, lat, st)| (Some(ts), lat, st))
.unwrap_or((None, None, "unknown".to_string()));
BackendUptimeInfo {
name: name.clone(),
status: current_status,
last_checked_at,
last_latency_ms,
uptime_pct_30d: uptime_pct,
history,
}
})
.collect();
let down_days = proxy_history.iter().filter(|d| d.status == "down").count();
let total_days = proxy_history.len();
let proxy_uptime_pct = if total_days == 0 {
100.0
} else {
(total_days - down_days) as f64 / total_days as f64 * 100.0
};
UptimeResponse {
proxy: ProxyUptimeInfo {
started_at,
uptime_pct_30d: proxy_uptime_pct,
history: proxy_history,
},
backends,
}
})
.await;
Json(response.unwrap_or_else(|| UptimeResponse {
proxy: ProxyUptimeInfo {
started_at,
uptime_pct_30d: 100.0,
history: vec![],
},
backends: vec![],
}))
}