stats: per-agent /stats page with chart.js trends + breakdowns

new hive-ag3nt::stats module reads turn_stats.sqlite read-only and
aggregates over 24h/7d/30d windows (hourly/daily buckets) — turn
rate, p50/p95/avg duration, ctx tokens (avg/max), cost token
components, top tools, wake mix, result mix. served by the agent
itself so per-MCP extensions can register more providers without
the host knowing their schemas.

/stats route + /api/stats?window=... on the per-agent web ui.
chart.js v4.4.4 pulled from jsdelivr (SRI hash deferred). nav
links: 📊 chip on the dashboard container row + 📊 stats → on
the per-agent header.

todo housekeeping: softened damocles-area note at the top,
new reverse-proxy + deferred reminder-rollup items, removed
the two telemetry-ui items absorbed by this page.
This commit is contained in:
müde 2026-05-19 00:27:01 +02:00
parent f9f1346eae
commit d3f90f4cc0
8 changed files with 930 additions and 3 deletions

View file

@ -107,6 +107,9 @@ pub async fn serve(
.route("/api/model", post(post_set_model))
.route("/api/new-session", post(post_new_session))
.route("/api/loose-ends", get(api_loose_ends))
.route("/stats", get(serve_stats))
.route("/static/stats.js", get(serve_stats_js))
.route("/api/stats", get(api_stats))
.with_state(state);
let addr = SocketAddr::from(([0, 0, 0, 0], port));
let listener = bind_with_retry(addr, "web UI").await?;
@ -198,6 +201,33 @@ async fn serve_marked_js() -> impl IntoResponse {
)
}
async fn serve_stats() -> impl IntoResponse {
(
[("content-type", "text/html; charset=utf-8")],
include_str!("../assets/stats.html"),
)
}
async fn serve_stats_js() -> impl IntoResponse {
(
[("content-type", "application/javascript")],
include_str!("../assets/stats.js"),
)
}
#[derive(Deserialize)]
struct StatsQuery {
window: Option<String>,
}
async fn api_stats(
State(_state): State<AppState>,
axum::extract::Query(q): axum::extract::Query<StatsQuery>,
) -> axum::Json<crate::stats::Snapshot> {
let window = crate::stats::Window::parse(q.window.as_deref().unwrap_or("24h"));
axum::Json(crate::stats::snapshot_default(window))
}
#[derive(Serialize)]
struct StateSnapshot {
/// Bus seq at the moment this snapshot was assembled. Clients dedupe