use axum::extract::State;
use axum::http::{HeaderMap, StatusCode};
use axum::response::{IntoResponse, Response};
use crate::app_state::AppState;
use crate::proxy::{error_response, is_admin_authorised};
fn admin_required() -> Response {
error_response(
StatusCode::UNAUTHORIZED,
"authentication_error",
"admin credential required",
)
}
pub async fn metrics_endpoint(State(state): State<AppState>) -> impl IntoResponse {
let body = crate::metrics::render_prometheus(&state.metrics);
(
StatusCode::OK,
[("content-type", "text/plain; version=0.0.4")],
body,
)
.into_response()
}
pub async fn usage_endpoint(
State(state): State<AppState>,
headers: HeaderMap,
) -> impl IntoResponse {
if !is_admin_authorised(&state, &headers) {
return admin_required();
}
let snap = crate::metrics::usage_snapshot(&state.metrics);
(StatusCode::OK, axum::Json(snap)).into_response()
}
pub async fn accounts_endpoint(
State(state): State<AppState>,
headers: HeaderMap,
) -> impl IntoResponse {
if !is_admin_authorised(&state, &headers) {
return admin_required();
}
let Some(router) = state.account_router.as_ref() else {
return (StatusCode::OK, axum::Json(single_account_view(&state))).into_response();
};
let snap: Vec<serde_json::Value> = router
.health_snapshot_with(Some(&state.subscription_cache))
.into_iter()
.map(|health| {
serde_json::json!({
"name": health.name,
"home": health.home.display().to_string(),
"healthy": health.healthy,
"credential": health.credential.label(),
"used": health.used,
"request_limit": health.request_limit,
"remaining_requests": health.remaining_requests,
"last_error": health.last_error,
"cooldown_remaining_seconds": health.cooldown_remaining.map(|d| d.as_secs()),
})
})
.collect();
(
StatusCode::OK,
axum::Json(serde_json::json!({"accounts": snap})),
)
.into_response()
}
fn single_account_view(state: &AppState) -> serde_json::Value {
let now_ms = chrono::Utc::now().timestamp_millis();
let credentials: Vec<serde_json::Value> = state
.subscription_readers
.iter()
.map(|reader| {
let credential = crate::accounts::credential_state_of(
reader,
"primary",
now_ms,
Some(&state.subscription_cache),
);
serde_json::json!({
"name": reader.provider().to_string(),
"home": reader.home().display().to_string(),
"credential": credential.label(),
"healthy": credential.can_serve(),
})
})
.collect();
serde_json::json!({
"accounts": [],
"credentials": credentials,
"note": "single-account mode (no AccountRouter configured)",
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::subscription::{SubscriptionProvider, SubscriptionReader};
#[test]
fn a_single_account_view_reports_each_provider() {
let dir = tempfile::tempdir().expect("data dir");
let home = tempfile::tempdir().expect("home");
std::fs::write(
home.path().join(".credentials.json"),
serde_json::json!({
"claudeAiOauth": {
"accessToken": "live-access",
"refreshToken": "live-refresh",
"expiresAt": 4_102_444_800_000_i64,
}
})
.to_string(),
)
.expect("plant a live credential");
let mut state = AppState::for_tests(dir.path());
state.subscription_readers = vec![
SubscriptionReader::new(SubscriptionProvider::Claude, home.path()),
SubscriptionReader::new(SubscriptionProvider::Codex, dir.path()),
];
let view = single_account_view(&state);
assert_eq!(
view["accounts"].as_array().map(Vec::len),
Some(0),
"the pool is genuinely empty and keeps its meaning"
);
assert!(
view["note"]
.as_str()
.is_some_and(|n| n.contains("single-account")),
"the server keeps explaining why: {view}"
);
let credentials = view["credentials"].as_array().expect("credentials");
assert_eq!(credentials.len(), 2, "{view}");
assert_eq!(credentials[0]["name"], "claude");
assert_eq!(
credentials[0]["credential"], "ok",
"a live credential must not read as missing: {view}"
);
assert_eq!(credentials[0]["healthy"], true);
assert_eq!(
credentials[1]["credential"], "missing",
"and an absent one must still say so: {view}"
);
assert_eq!(credentials[1]["healthy"], false);
}
}