use std::sync::OnceLock;
use axum::extract::Query;
use axum::http::{StatusCode, header};
use axum::response::{IntoResponse, Json, Response};
use deadpool_postgres::Pool;
use serde::{Deserialize, Serialize};
use crate::proxy::gateway_identity::GatewayTags;
use super::admin_timeseries::{TimeseriesPoint, fill_gaps};
static USER_POOL: OnceLock<Pool> = OnceLock::new();
pub fn install_pool(pool: Pool) -> bool {
USER_POOL.set(pool).is_ok()
}
const DEFAULT_WINDOW_DAYS: u32 = 30;
const MAX_WINDOW_DAYS: u32 = 365;
const ME_HTML: &str = include_str!("static/me.html");
const ME_CSS: &str = include_str!("static/me.css");
const ME_JS: &str = include_str!("static/me.js");
const BASE_CSS: &str = include_str!("static/admin.css");
const ME_FONTS_CSS: &str = include_str!("static/me-fonts.css");
const FONT_INTER_WOFF2: &[u8] = include_bytes!("../dashboard/static/fonts/inter-variable.woff2");
const FONT_JETBRAINS_WOFF2: &[u8] =
include_bytes!("../dashboard/static/fonts/jetbrains-mono-variable.woff2");
const FONT_SPACE_GROTESK_WOFF2: &[u8] =
include_bytes!("../dashboard/static/fonts/space-grotesk-variable.woff2");
const VENDOR_CHART_JS: &str = include_str!("../dashboard/static/vendor/chart.umd.min.js");
#[must_use]
pub fn is_shell_path(path: &str) -> bool {
path == "/me" || path.starts_with("/me/static/")
}
pub fn router<S: Clone + Send + Sync + 'static>() -> axum::Router<S> {
axum::Router::new()
.route("/me", axum::routing::get(shell))
.route("/me/static/base.css", axum::routing::get(base_css))
.route("/me/static/me.css", axum::routing::get(me_css))
.route("/me/static/me.js", axum::routing::get(me_js))
.route("/me/static/fonts/fonts.css", axum::routing::get(fonts_css))
.route(
"/me/static/vendor/chart.umd.min.js",
axum::routing::get(chart_js),
)
.route("/me/static/fonts/{file}", axum::routing::get(font_file))
.route("/api/me/usage", axum::routing::get(me_usage))
.layer(axum::middleware::from_fn(super::security::security_headers))
}
async fn shell() -> impl IntoResponse {
(
[(header::CONTENT_TYPE, "text/html; charset=utf-8")],
ME_HTML,
)
}
async fn base_css() -> impl IntoResponse {
(
[(header::CONTENT_TYPE, "text/css; charset=utf-8")],
BASE_CSS,
)
}
async fn me_css() -> impl IntoResponse {
([(header::CONTENT_TYPE, "text/css; charset=utf-8")], ME_CSS)
}
async fn me_js() -> impl IntoResponse {
(
[(
header::CONTENT_TYPE,
"application/javascript; charset=utf-8",
)],
ME_JS,
)
}
async fn fonts_css() -> impl IntoResponse {
(
[(header::CONTENT_TYPE, "text/css; charset=utf-8")],
ME_FONTS_CSS,
)
}
async fn chart_js() -> impl IntoResponse {
(
[(
header::CONTENT_TYPE,
"application/javascript; charset=utf-8",
)],
VENDOR_CHART_JS,
)
}
async fn font_file(axum::extract::Path(file): axum::extract::Path<String>) -> Response {
let bytes: &'static [u8] = match file.as_str() {
"inter-variable.woff2" => FONT_INTER_WOFF2,
"jetbrains-mono-variable.woff2" => FONT_JETBRAINS_WOFF2,
"space-grotesk-variable.woff2" => FONT_SPACE_GROTESK_WOFF2,
_ => return StatusCode::NOT_FOUND.into_response(),
};
([(header::CONTENT_TYPE, "font/woff2")], bytes).into_response()
}
#[derive(Debug, Clone, Deserialize)]
pub struct MeQuery {
pub days: Option<u32>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct MeModelRow {
pub model: String,
pub provider: String,
pub requests: i64,
pub input_tokens: i64,
pub output_tokens: i64,
pub cost_usd: f64,
pub saved_usd: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct MeProjectRow {
pub project: String,
pub requests: i64,
pub cost_usd: f64,
pub saved_usd: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct MeToolRow {
pub server_id: String,
pub tool: String,
pub calls: i64,
pub result_tokens: i64,
pub context_cost_usd: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct MeTotals {
pub requests: i64,
pub input_tokens: i64,
pub output_tokens: i64,
pub cost_usd: f64,
pub saved_tokens: i64,
pub saved_usd: f64,
pub reference_cost_usd: f64,
pub routed_requests: i64,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct MeUsageResponse {
pub person: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub team: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub org_label: Option<String>,
pub version: String,
pub from: String,
pub to: String,
pub totals: MeTotals,
pub by_model: Vec<MeModelRow>,
pub by_project: Vec<MeProjectRow>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub tools: Vec<MeToolRow>,
pub days: Vec<TimeseriesPoint>,
}
const ME_TOTALS_SQL: &str = "
SELECT count(*) AS requests,
coalesce(sum(input_tokens), 0)::BIGINT AS input_tokens,
coalesce(sum(output_tokens), 0)::BIGINT AS output_tokens,
coalesce(sum(cost_usd), 0) AS cost_usd,
coalesce(sum(saved_tokens), 0)::BIGINT AS saved_tokens,
coalesce(sum(saved_usd), 0) AS saved_usd,
coalesce(sum(reference_cost_usd), 0) AS reference_cost_usd,
count(*) FILTER (WHERE routed_from IS NOT NULL) AS routed_requests
FROM usage_events
WHERE ts >= $1 AND ts <= $2 AND person = $3";
const ME_BY_MODEL_SQL: &str = "
SELECT model, provider,
count(*) AS requests,
sum(input_tokens)::BIGINT AS input_tokens,
sum(output_tokens)::BIGINT AS output_tokens,
sum(cost_usd) AS cost_usd,
sum(saved_usd) AS saved_usd
FROM usage_events
WHERE ts >= $1 AND ts <= $2 AND person = $3
GROUP BY model, provider
ORDER BY cost_usd DESC";
const ME_BY_PROJECT_SQL: &str = "
SELECT project,
count(*) AS requests,
sum(cost_usd) AS cost_usd,
sum(saved_usd) AS saved_usd
FROM usage_events
WHERE ts >= $1 AND ts <= $2 AND person = $3
GROUP BY project
ORDER BY cost_usd DESC";
const ME_TIMESERIES_SQL: &str = "
SELECT date_trunc('day', ts) AS day,
count(*) AS requests,
coalesce(sum(cost_usd), 0) AS cost_usd,
coalesce(sum(saved_usd), 0) AS saved_usd,
coalesce(sum(reference_cost_usd), 0) AS reference_cost_usd
FROM usage_events
WHERE ts >= $1 AND ts <= $2 AND person = $3
GROUP BY 1
ORDER BY 1";
async fn me_usage(
tags: Option<axum::Extension<GatewayTags>>,
Query(q): Query<MeQuery>,
) -> Response {
let Some(person) = tags.as_ref().and_then(|t| t.person.clone()) else {
return (
StatusCode::FORBIDDEN,
Json(serde_json::json!({
"error": "this view needs a personal gateway key (it identifies you); \
ask your admin for one: lean-ctx gateway keys add --person <you>"
})),
)
.into_response();
};
let Some(pool) = USER_POOL.get() else {
return (
StatusCode::SERVICE_UNAVAILABLE,
Json(serde_json::json!({
"error": "usage store not configured on this gateway (DATABASE_URL unset)"
})),
)
.into_response();
};
let team = tags.and_then(|t| t.0.team);
let days = q
.days
.unwrap_or(DEFAULT_WINDOW_DAYS)
.clamp(1, MAX_WINDOW_DAYS);
let to = chrono::Utc::now();
let from = to - chrono::Duration::days(i64::from(days));
match personal_usage(pool, &person, team, from, to).await {
Ok(resp) => Json(resp).into_response(),
Err(e) => {
tracing::warn!("personal usage query failed: {e:#}");
(
StatusCode::SERVICE_UNAVAILABLE,
Json(serde_json::json!({"error": "usage store unavailable"})),
)
.into_response()
}
}
}
pub async fn personal_usage(
pool: &Pool,
person: &str,
team: Option<String>,
from: chrono::DateTime<chrono::Utc>,
to: chrono::DateTime<chrono::Utc>,
) -> anyhow::Result<MeUsageResponse> {
let client = pool.get().await?;
let t = client
.query_one(ME_TOTALS_SQL, &[&from, &to, &person])
.await?;
let totals = MeTotals {
requests: t.get("requests"),
input_tokens: t.get("input_tokens"),
output_tokens: t.get("output_tokens"),
cost_usd: t.get("cost_usd"),
saved_tokens: t.get("saved_tokens"),
saved_usd: t.get("saved_usd"),
reference_cost_usd: t.get("reference_cost_usd"),
routed_requests: t.get("routed_requests"),
};
let by_model = client
.query(ME_BY_MODEL_SQL, &[&from, &to, &person])
.await?
.iter()
.map(|r| MeModelRow {
model: r.get("model"),
provider: r.get("provider"),
requests: r.get("requests"),
input_tokens: r.get("input_tokens"),
output_tokens: r.get("output_tokens"),
cost_usd: r.get("cost_usd"),
saved_usd: r.get("saved_usd"),
})
.collect();
let by_project = client
.query(ME_BY_PROJECT_SQL, &[&from, &to, &person])
.await?
.iter()
.map(|r| MeProjectRow {
project: r.get("project"),
requests: r.get("requests"),
cost_usd: r.get("cost_usd"),
saved_usd: r.get("saved_usd"),
})
.collect();
let tools = client
.query(super::mcp::store::ME_TOOLS_SQL, &[&from, &to, &person])
.await
.map(|rows| {
rows.iter()
.map(|r| MeToolRow {
server_id: r.get("server_id"),
tool: r.get("tool"),
calls: r.get("calls"),
result_tokens: r.get("result_tokens"),
context_cost_usd: r.get("context_cost_usd"),
})
.collect()
})
.unwrap_or_default();
let measured: Vec<TimeseriesPoint> = client
.query(ME_TIMESERIES_SQL, &[&from, &to, &person])
.await?
.iter()
.map(|r| {
let day: chrono::DateTime<chrono::Utc> = r.get("day");
TimeseriesPoint {
day: day.format("%Y-%m-%d").to_string(),
requests: r.get("requests"),
cost_usd: r.get("cost_usd"),
saved_usd: r.get("saved_usd"),
reference_cost_usd: r.get("reference_cost_usd"),
}
})
.collect();
let cfg = crate::core::config::Config::load();
Ok(MeUsageResponse {
person: person.to_string(),
team,
org_label: cfg.gateway_server.org_label.clone(),
version: env!("CARGO_PKG_VERSION").to_string(),
from: from.to_rfc3339(),
to: to.to_rfc3339(),
totals,
by_model,
by_project,
tools,
days: fill_gaps(&measured, from, to),
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn shell_paths_cover_exactly_the_public_surface() {
assert!(is_shell_path("/me"));
assert!(is_shell_path("/me/static/me.js"));
assert!(is_shell_path("/me/static/fonts/inter-variable.woff2"));
assert!(!is_shell_path("/api/me/usage"));
assert!(!is_shell_path("/me2"));
assert!(!is_shell_path("/mex/static/a.js"));
assert!(!is_shell_path("/v1/messages"));
}
#[test]
fn embedded_assets_are_nonempty_and_wired() {
assert!(ME_HTML.contains("<!doctype html"));
assert!(
ME_HTML.contains("/me/static/me.js"),
"shell must load the app script"
);
assert!(
ME_HTML.contains("/me/static/base.css"),
"shell must reuse the console design system"
);
assert!(
ME_JS.contains("/api/me/usage"),
"app must talk to the guarded API"
);
assert!(
ME_FONTS_CSS.contains("/me/static/fonts/"),
"font faces must resolve on the proxy port"
);
assert!(!ME_CSS.is_empty());
assert!(!VENDOR_CHART_JS.is_empty());
}
#[test]
fn shell_never_embeds_credentials() {
for needle in ["Bearer ", "gk-", "LEAN_CTX_PROXY_TOKEN="] {
assert!(!ME_HTML.contains(needle), "me.html must not embed {needle}");
}
assert!(
!ME_JS.contains("localStorage.setItem('leanctx-me-key'"),
"key must live in sessionStorage, not persist in localStorage"
);
}
#[test]
fn window_days_are_clamped() {
for (input, expected) in [
(None, DEFAULT_WINDOW_DAYS),
(Some(0), 1),
(Some(7), 7),
(Some(9999), MAX_WINDOW_DAYS),
] {
let days = input
.unwrap_or(DEFAULT_WINDOW_DAYS)
.clamp(1, MAX_WINDOW_DAYS);
assert_eq!(days, expected, "input {input:?}");
}
}
#[test]
fn response_shape_round_trips() {
let resp = MeUsageResponse {
person: "alice@zuehlke.com".into(),
team: Some("platform".into()),
org_label: Some("Zühlke Engineering AG".into()),
version: "3.8.18".into(),
from: "2026-06-03T00:00:00+00:00".into(),
to: "2026-07-03T00:00:00+00:00".into(),
totals: MeTotals {
requests: 412,
input_tokens: 9_000_000,
output_tokens: 310_000,
cost_usd: 84.12,
saved_tokens: 2_400_000,
saved_usd: 41.90,
reference_cost_usd: 190.55,
routed_requests: 96,
},
by_model: vec![MeModelRow {
model: "zuehlke/fast".into(),
provider: "foundry".into(),
requests: 96,
input_tokens: 1_000_000,
output_tokens: 50_000,
cost_usd: 4.20,
saved_usd: 12.80,
}],
by_project: vec![MeProjectRow {
project: "checkout".into(),
requests: 412,
cost_usd: 84.12,
saved_usd: 41.90,
}],
tools: vec![MeToolRow {
server_id: "github".into(),
tool: "get_issue".into(),
calls: 31,
result_tokens: 128_000,
context_cost_usd: 0.32,
}],
days: vec![],
};
let json = serde_json::to_value(&resp).expect("serializes");
assert_eq!(json["person"], "alice@zuehlke.com");
assert_eq!(json["totals"]["routed_requests"], 96);
assert_eq!(json["by_model"][0]["model"], "zuehlke/fast");
let parsed: MeUsageResponse = serde_json::from_value(json).expect("round-trips");
assert_eq!(parsed, resp);
}
#[tokio::test]
async fn me_usage_refuses_identityless_tokens() {
for tags in [
None,
Some(axum::Extension(GatewayTags::default())),
Some(axum::Extension(GatewayTags {
person: None,
team: None,
project: Some("side-quest".into()),
})),
] {
let resp = me_usage(tags, Query(MeQuery { days: Some(7) })).await;
assert_eq!(resp.status(), StatusCode::FORBIDDEN);
}
}
#[tokio::test]
async fn me_usage_without_store_is_503() {
let tags = Some(axum::Extension(GatewayTags {
person: Some("alice".into()),
team: None,
project: None,
}));
let resp = me_usage(tags, Query(MeQuery { days: None })).await;
assert_eq!(resp.status(), StatusCode::SERVICE_UNAVAILABLE);
}
}