// /stats.html — hive-wide turn-stats rollup. // // Extracted from the dashboard ST4TS tab. Pull-only (no SSE): fetched // from /api/stats-hive on load and on window change. Plain tables/bars — // this bundle has no chart lib; per-agent trend charts live on each // agent's own /stats page. The window selector is a hash-routed // createTabStrip (#1h / #24h / …), matching the per-agent stats page. import { $, initServerWarnings } from "./common.js"; import { createTabStrip } from "@hive/shared/tabs.js"; let hiveStatsWindow = "24h"; function hsFmtInt(n) { return Number.isFinite(n) ? new Intl.NumberFormat().format(Math.round(n)) : "0"; } function hsFmtTokens(n) { if (!Number.isFinite(n)) return "0"; if (n >= 1e9) return (n / 1e9).toFixed(2) + "B"; if (n >= 1e6) return (n / 1e6).toFixed(2) + "M"; if (n >= 1e3) return (n / 1e3).toFixed(1) + "k"; return String(Math.round(n)); } function hsFmtUsd(n) { if (!Number.isFinite(n)) return "$0"; if (n >= 100) return "$" + n.toFixed(0); if (n >= 1) return "$" + n.toFixed(2); return "$" + n.toFixed(3); } function hsChip(parent, label, value, est) { const c = document.createElement("span"); c.className = "hive-stats-chip" + (est ? " est" : ""); const k = document.createElement("span"); k.className = "k"; k.textContent = label; const v = document.createElement("span"); v.className = "v"; v.textContent = value; c.append(k, v); parent.append(c); } function hsMeta(parent, text) { parent.replaceChildren(); const p = document.createElement("p"); p.className = "meta"; p.textContent = text; parent.append(p); } // Shared bar-list renderer for any `KeyCount[]` mix (model/bash/skill). function renderKeyCountBars(container, mix) { container.replaceChildren(); const max = mix[0].count || 1; for (const kc of mix) { const row = document.createElement("div"); row.className = "hive-stats-bar"; const lbl = document.createElement("span"); lbl.className = "lbl"; lbl.textContent = kc.key; const track = document.createElement("span"); track.className = "track"; const fill = document.createElement("span"); fill.className = "fill"; fill.style.width = Math.max(2, Math.round((100 * kc.count) / max)) + "%"; track.append(fill); const cnt = document.createElement("span"); cnt.className = "cnt"; cnt.textContent = hsFmtInt(kc.count); row.append(lbl, track, cnt); container.append(row); } } // A mix section that hides itself (+ its header) until the backing // capture has recorded data, so the section never shows an empty block // while data is pre-capture. Used by bash_mix and skill_mix. function renderOptionalMix(containerId, headerId, mix) { const el = $(containerId); const h = $(headerId); if (!el) return; if (!mix.length) { el.replaceChildren(); el.hidden = true; if (h) h.hidden = true; return; } el.hidden = false; if (h) h.hidden = false; renderKeyCountBars(el, mix); } function renderHiveStats(s) { const sum = $("hive-stats-summary"); if (sum) { sum.replaceChildren(); hsChip(sum, "window", s.window); hsChip(sum, "active agents", hsFmtInt(s.active_agents)); hsChip(sum, "turns", hsFmtInt(s.total_turns)); const totalTok = (s.total_input_tokens || 0) + (s.total_output_tokens || 0) + (s.total_cache_read_tokens || 0) + (s.total_cache_creation_tokens || 0); hsChip(sum, "tokens", hsFmtTokens(totalTok)); hsChip(sum, "input", hsFmtTokens(s.total_input_tokens)); hsChip(sum, "output", hsFmtTokens(s.total_output_tokens)); hsChip(sum, "cache read", hsFmtTokens(s.total_cache_read_tokens)); hsChip(sum, "est cost", hsFmtUsd(s.est_cost_usd), true); } const at = $("hive-stats-agents"); if (at) { const agents = s.agents || []; if (!agents.length) { hsMeta(at, "no turns in window"); } else { at.replaceChildren(); const table = document.createElement("table"); table.className = "hive-stats-table"; table.innerHTML = "agentturnsinput" + "outputcache readest cost"; const tb = document.createElement("tbody"); for (const a of agents) { const tr = document.createElement("tr"); const cells = [ a.name, hsFmtInt(a.turns), hsFmtTokens(a.input_tokens), hsFmtTokens(a.output_tokens), hsFmtTokens(a.cache_read_tokens), hsFmtUsd(a.est_cost_usd), ]; cells.forEach((txt, i) => { const td = document.createElement("td"); if (i > 0) td.className = "num"; td.textContent = txt; tr.append(td); }); tb.append(tr); } table.append(tb); at.append(table); } } const mm = $("hive-stats-models"); if (mm) { const mix = s.model_mix || []; if (!mix.length) hsMeta(mm, "no turns in window"); else renderKeyCountBars(mm, mix); } // "favorite tools": most-run bash commands across the swarm. renderOptionalMix("hive-stats-bash", "hive-stats-bash-h", s.bash_mix || []); // Most-triggered skills across the swarm. renderOptionalMix( "hive-stats-skills", "hive-stats-skills-h", s.skill_mix || [], ); } async function refreshHiveStats() { try { const resp = await fetch( "/api/stats-hive?window=" + encodeURIComponent(hiveStatsWindow), ); if (!resp.ok) throw new Error("http " + resp.status); renderHiveStats(await resp.json()); } catch (e) { const at = $("hive-stats-agents"); if (at) hsMeta(at, "stats fetch failed: " + e); } } initServerWarnings(); // Window selector → hash-routed createTabStrip (#1h / #24h / …). No panes // here (the selector swaps data, not DOM); paneFor() returns null for each // tab and the strip's `if (pane)` guard handles it. The initial show() // fires onShow once → sets the window + does the first fetch, so no // separate refreshHiveStats() call is needed. createTabStrip(document.getElementById("hive-stats-windows"), { defaultId: hiveStatsWindow, onShow: (w) => { hiveStatsWindow = w; refreshHiveStats(); }, });