diff --git a/.gitignore b/.gitignore
index 8086652..c42a4ff 100644
--- a/.gitignore
+++ b/.gitignore
@@ -43,4 +43,5 @@ scripts/.venv/
# Admin UI build artifacts
crates/proxy/admin-ui/node_modules/
-crates/proxy/admin-ui/dist/
+# dist/ is committed so `include_str!` works at compile time without a build step
+# crates/proxy/admin-ui/dist/
diff --git a/crates/proxy/admin-ui/dist/index.html b/crates/proxy/admin-ui/dist/index.html
new file mode 100644
index 0000000..2f6681c
--- /dev/null
+++ b/crates/proxy/admin-ui/dist/index.html
@@ -0,0 +1,61 @@
+
+
+
+
+
+ Proxy Admin
+
+
+
+
+
+
+
diff --git a/crates/proxy/src/admin/routes/mod.rs b/crates/proxy/src/admin/routes/mod.rs
index fa4d699..4f62393 100644
--- a/crates/proxy/src/admin/routes/mod.rs
+++ b/crates/proxy/src/admin/routes/mod.rs
@@ -6,6 +6,8 @@ pub mod keys;
pub mod logs;
pub mod mcp;
pub mod models;
+pub mod traffic;
+pub mod uptime;
use crate::admin::auth::{
extract_csrf_cookie, generate_csrf_token, validate_admin_token, validate_csrf_tokens,
@@ -401,6 +403,8 @@ pub fn admin_router(shared: SharedState, token: Arc>)
"/admin/api/mcp-servers/{name}",
delete(mcp::remove_mcp_server),
)
+ .route("/admin/api/traffic", get(traffic::get_traffic))
+ .route("/admin/api/uptime", get(uptime::get_uptime))
.with_state(shared.clone())
// Innermost: CSRF check runs after auth succeeds.
.layer(middleware::from_fn_with_state(
@@ -440,7 +444,7 @@ async fn health() -> Json {
}
/// Serve the embedded SPA HTML with a per-request CSP nonce.
-static SPA_HTML: &str = include_str!("../../../admin-ui/index.html");
+static SPA_HTML: &str = include_str!("../../../admin-ui/dist/index.html");
async fn serve_spa() -> axum::response::Response {
// Generate a per-request nonce (128-bit, base64url-encoded).
diff --git a/crates/proxy/src/cost/mod.rs b/crates/proxy/src/cost/mod.rs
index 0014984..f25f97a 100644
--- a/crates/proxy/src/cost/mod.rs
+++ b/crates/proxy/src/cost/mod.rs
@@ -441,6 +441,7 @@ mod tests {
.time_to_live(std::time::Duration::from_secs(86400))
.build(),
),
+ started_at: std::time::SystemTime::now(),
};
let vk_ctx = VirtualKeyContext {
diff --git a/crates/proxy/src/main.rs b/crates/proxy/src/main.rs
index e7c3c31..b818631 100644
--- a/crates/proxy/src/main.rs
+++ b/crates/proxy/src/main.rs
@@ -497,6 +497,7 @@ async fn async_main(args: Vec) {
.time_to_live(std::time::Duration::from_secs(86400))
.build(),
),
+ started_at: std::time::SystemTime::now(),
};
// Admin token: use env var or generate 256-bit random hex written to a file.
@@ -597,6 +598,16 @@ async fn async_main(args: Vec) {
}
});
+ // Spawn background health checker with a snapshot of backend base URLs.
+ // base_url lives in BackendConfig (not RuntimeConfig), so we snapshot
+ // it here once at startup rather than storing it in SharedState.
+ let backend_urls: Vec<(String, String)> = multi_config
+ .backends
+ .iter()
+ .map(|(name, bc)| (name.clone(), bc.base_url.clone()))
+ .collect();
+ admin::health_check::spawn(shared.clone(), backend_urls);
+
// Bind admin listener; spawned after the shutdown channel is created below.
let admin_app = admin::routes::admin_router(shared.clone(), admin_token);
let admin_bind = std::env::var("ADMIN_BIND").unwrap_or_else(|_| "127.0.0.1".into());