// /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(); }, });