Pure `nix fmt` output from the commit before this one — no hand edits. 203 files: 52 md, 42 tsx, 32 js, 32 css, 21 ts, 13 html, 8 json, 3 mjs. Reproduce with `nix develop -c nix fmt` on the parent commit; the result should be byte-identical to this tree. None of the 13 `.prettierignore` entries appears here — verified by intersecting the changed-file list against the ignore file, with a control proving the intersection finds a match when one exists.
189 lines
6.1 KiB
JavaScript
189 lines
6.1 KiB
JavaScript
// /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 =
|
|
"<thead><tr><th>agent</th><th>turns</th><th>input</th>" +
|
|
"<th>output</th><th>cache read</th><th>est cost</th></tr></thead>";
|
|
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();
|
|
},
|
|
});
|