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());