// /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); } 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 { mm.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); mm.append(row); } } } // "favorite tools": most-run bash commands across the swarm. Hidden // (header + list) until the capture has recorded data, so the // section never shows an empty block while capture is pre-data. const bh = $('hive-stats-bash'); const bhH = $('hive-stats-bash-h'); if (bh) { const bmix = s.bash_mix || []; if (!bmix.length) { bh.replaceChildren(); bh.hidden = true; if (bhH) bhH.hidden = true; } else { bh.hidden = false; if (bhH) bhH.hidden = false; bh.replaceChildren(); const max = bmix[0].count || 1; for (const kc of bmix) { 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); bh.append(row); } } } } 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(); }, });