feat(dashboard): ST4TS tab — hive-wide stats view

Builds the dashboard UI on the /api/stats-hive endpoint: a new ST4TS
tab showing swarm totals (active agents, turns, tokens, labelled est
cost), a busiest-first per-agent table, and a model-mix bar list.
Plain tables/CSS bars — no chart lib in the dashboard bundle; per-agent
trend charts stay on each agent's own /stats page. Fetched on tab
activation + window change (pull-only, no SSE).

Also adds conn.busy_timeout(500ms) in hive_stats read_agent per review:
turn_stats is rollback-journal, so a read landing mid-INSERT would hit
SQLITE_BUSY and silently drop that active agent — wait the write out.
This commit is contained in:
iris 2026-06-05 22:41:15 +02:00 committed by mara
commit 8e74dda4c5
4 changed files with 263 additions and 2 deletions

View file

@ -17,7 +17,7 @@
use std::collections::HashMap;
use std::path::Path;
use std::time::{SystemTime, UNIX_EPOCH};
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use rusqlite::{Connection, OpenFlags};
use serde::Serialize;
@ -177,6 +177,10 @@ fn u64_from_i64(v: i64) -> u64 {
/// the whole endpoint.
fn read_agent(path: &Path, from: i64) -> rusqlite::Result<AgentAgg> {
let conn = Connection::open_with_flags(path, OpenFlags::SQLITE_OPEN_READ_ONLY)?;
// turn_stats is rollback-journal (not WAL): a read landing while the
// harness is mid-INSERT would get SQLITE_BUSY and drop that active
// agent from the rollup. Wait out the brief write instead.
conn.busy_timeout(Duration::from_millis(500))?;
let mut stmt = conn.prepare(
"SELECT model, input_tokens, output_tokens,
cache_read_input_tokens, cache_creation_input_tokens