Watch
0
0
Fork
You've already forked hyperhive
0

remove the hive dashboard's stats page and the price table behind it

The ST4TS page (/stats.html), GET /api/stats-hive, hive-c0re's hive_stats
rollup and the modelPrices option that only fed its cost estimate are
gone. Each agent's own /stats page and the turn-stats database it reads
are unchanged. Setting modelPrices now fails evaluation with a message
saying so.
This commit is contained in:
iris 2026-11-02 23:15:51 +01:00 • committed by mara
commit 5e9a22cb38
12 changed files with 112 additions and 1207 deletions

View file

@ -1,6 +1,5 @@
// esbuild build for @hive/dashboard: one JS+CSS bundle per page (the
// dashboard at `/`, stats at its own
// `.html`), each named after its own entry point below — see the
// dashboard at `/`), each named after its own entry point below — see the
// `entryPoints`/`for` lists for the exact map, not repeated here to
// avoid this comment drifting out of sync with the real build steps.
//
@ -29,7 +28,7 @@ mkdirSync(staticDir(""), { recursive: true });
// follow-up once asset sizes warrant it). esbuild writes each entry
// to `static/<name>.js` based on the entryPoint basename.
await build({
entryPoints: [src("tabs.js"), src("stats.js")],
entryPoints: [src("tabs.js")],
outdir: staticDir(""),
bundle: true,
format: "esm",
@ -58,7 +57,7 @@ await build({
// Stream-worker entry. Lives in a separate bundle: SharedWorker
// scripts run in a different global (`self` is the worker scope, no
// `window`) so they can't be inlined into tabs.js / stats.js. Output is
// `window`) so they can't be inlined into tabs.js. Output is
// at `static/stream-worker.js`; common.js's `openStream` references
// `/static/stream-worker.js` as the SharedWorker URL. `format: 'iife'`
// matches the classic-script load (`new SharedWorker(url, name)` with
@ -89,7 +88,6 @@ for (const entry of [
"theme.css",
"common.css",
"dashboard.css",
"stats.css",
]) {
await build({
entryPoints: [src(entry)],
@ -100,8 +98,6 @@ for (const entry of [
});
}
for (const html of ["index.html", "stats.html"]) {
copyFileSync(src(html), dist(html));
}
copyFileSync(src("index.html"), dist("index.html"));
console.log("dashboard build ok →", dist(""));

View file

@ -3,7 +3,6 @@
@import "@hive/shared/base.css";
@import "@hive/shared/terminal.css";
@import "@hive/shared/tabs.css";
@import "@hive/shared/chrome.css";
/* Shared pill/chip/badge shape (the "extract pill component" issue,
resolved as shared CSS rather than a JS component) — see pill.css
itself for the classnames it covers. */
@ -271,8 +270,7 @@ code {
carries the horizontal gutter. Full-width chrome (the tab strip, the
footer) then spans edge-to-edge for free,
with no negative-margin breakout hacks. Only matches on pages that opt
in by adding the wrapper (index.html); ST4TS
is full-bleed with its own `.<page>-main` padding. */
in by adding the wrapper (index.html). */
.page-content {
padding: 0 1.5em;
}
@ -285,31 +283,3 @@ code {
top: 0;
z-index: 50;
}
/* ─── stats table ─────────────────────────────────────────────────
Shared right-aligned numeric table. Used by /stats.html (busiest
agents) — moved here from dashboard.css when ST4TS became its own
page. */
.hive-stats-table {
width: 100%;
border-collapse: collapse;
font-size: 0.85rem;
margin-bottom: 8px;
}
.hive-stats-table th,
.hive-stats-table td {
border-bottom: 1px solid var(--border);
padding: 5px 8px;
text-align: right;
}
.hive-stats-table th:first-child,
.hive-stats-table td:first-child {
text-align: left;
}
.hive-stats-table th {
color: var(--muted);
font-weight: normal;
}
.hive-stats-table td.num {
font-variant-numeric: tabular-nums;
}

View file

@ -600,7 +600,3 @@ footer .banner-thin {
body.dashboard-shell.has-selection {
padding-bottom: 4.5em;
}
/* ST4TS moved to its own page (`/stats.html`): the
window selector / summary chips / bars live in stats.css, and the
shared `.hive-stats-table` moved to common.css. */

View file

@ -55,15 +55,10 @@
<!-- (Peer hives are no longer a tab — they render as a headline
under 4G3NTS. See tab-pane-agents.) -->
<!-- ST4TS is a standalone page reached from the pages
menu at the right of this strip, not tabs — the strip holds only
in-page tabs. -->
</nav>
<!-- The pages menu and the profile menu, together at the right.
Pages menu: the standalone pages (stats, API docs) and the swarm UI,
filled by links-menu.js from /api/state; the pages link back to `/`,
which is this dashboard. Profile menu: the signed-in user, filled by
Pages menu: the API docs and the swarm UI, filled by links-menu.js
from /api/state. Profile menu: the signed-in user, filled by
user-menu.js. -->
<div class="dashboard-chrome-actions">
<div class="links-menu-slot" id="links-menu"></div>
@ -148,11 +143,6 @@
<p class="meta">loading…</p>
</div>
</section>
<!-- ST4TS lives on its own page (`/stats.html`).
Its DOM only exists there — when tabs.js boots
on this page the corresponding renderers no-op silently (each
guard is `if (!el) return`). -->
</main>
</div>
<!-- /.page-content -->

View file

@ -1,6 +1,6 @@
// Pages menu in the dashboard chrome: one icon button, beside the profile
// avatar at the right, that opens a popover linking the standalone pages
// (stats, API docs) and the swarm UI. It is `@hive/shared`'s `LinksMenu`, the
// (API docs) and the swarm UI. It is `@hive/shared`'s `LinksMenu`, the
// same component the swarm UI mounts for its service links, rendered with
// Preact like the profile menu. The swarm entry only exists when `/api/state`
// carries `swarm_public_url`, so a hive without a configured swarm domain
@ -10,11 +10,10 @@ import { h, render } from "preact";
import { LinksMenu } from "@hive/shared/links-menu.js";
import { $ } from "./common.js";
/** Menu entries for a state snapshot. The two pages open in place; the swarm
/** Menu entries for a state snapshot. The API docs open in place; the swarm
* UI is another site, so it opens in a new tab. */
export function menuLinks(state) {
const links = [
{ icon: "📊", label: "stats", url: "/stats.html", external: false },
{ icon: "🧬", label: "api docs", url: "/api/docs/", external: false },
];
if (state && state.swarm_public_url) {

View file

@ -1,97 +0,0 @@
/* ─── /stats.html — hive-wide turn-stats rollup ────────────────────
Extracted from the dashboard ST4TS tab. Minimal chrome:
the back-link header +
title (.page-header / .page-back / .page-title) live in
@hive/shared/chrome.css.
`.hive-stats-table` lives in common.css. Only the ST4TS-specific
window selector, summary chips, and bars are page-specific. */
body.stats-shell {
margin: 0;
padding: 0;
}
.stats-main {
padding: 1.2em 1.5em 2em;
}
/* Window selector — now lives in the page-header (same row as ← home /
ST4TS title). Fills the remaining header space and right-aligns the
buttons. The shared `.btn` pill look is kept; createTabStrip flags the
active one with `.hive-tab--active`. */
.hive-stats-windows {
display: flex;
flex-wrap: wrap;
gap: 6px;
flex: 1;
min-width: 0;
justify-content: flex-end;
/* Smaller than the default .btn so the pills fit comfortably in the
header without competing visually with the page title. */
font-size: 0.82em;
}
/* Inactive buttons: slightly muted to distinguish from the active window,
but readable even on low-contrast operator colour schemes. */
.hive-stats-windows .btn {
color: var(--subtext1, var(--muted));
border-color: var(--border);
font-size: 1em; /* relative to the .hive-stats-windows 0.82em */
}
.hive-stats-windows .btn.hive-tab--active {
border-color: var(--amber);
color: var(--amber);
}
.hive-stats-chips {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 16px;
}
.hive-stats-chip {
display: inline-flex;
flex-direction: column;
padding: 6px 10px;
background: var(--bg-elev);
border: 1px solid var(--border);
border-radius: 4px;
min-width: 7em;
}
.hive-stats-chip .k {
font-size: 0.72rem;
color: var(--muted);
text-transform: lowercase;
}
.hive-stats-chip .v {
font-size: 1.05rem;
color: var(--fg);
}
.hive-stats-chip.est .v {
color: var(--amber);
}
.hive-stats-bar {
display: grid;
grid-template-columns: 12em 1fr 4em;
align-items: center;
gap: 8px;
margin: 3px 0;
font-size: 0.85rem;
}
.hive-stats-bar .track {
background: var(--bg-elev);
border: 1px solid var(--border);
border-radius: 3px;
height: 14px;
overflow: hidden;
}
.hive-stats-bar .fill {
display: block;
height: 100%;
background: var(--purple);
}
.hive-stats-bar .cnt {
text-align: right;
color: var(--muted);
font-variant-numeric: tabular-nums;
}

View file

@ -1,63 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>hyperhive // ST4TS</title>
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="stylesheet" href="/static/colors.css" />
<link rel="stylesheet" href="/static/theme.css" />
<link rel="stylesheet" href="/static/common.css" />
<link rel="stylesheet" href="/static/stats.css" />
</head>
<body class="stats-shell">
<!-- Minimal chrome: back link + title —
no full dashboard tabbar. Back link points to the
dashboard (served at /). -->
<header class="page-header">
<a class="page-back" href="/">← dashboard</a>
<span class="page-title">ST4TS</span>
<!-- Window selector lives in the header so it behaves like a tab strip.
Hash-routed via createTabStrip. -->
<nav class="hive-stats-windows" id="hive-stats-windows" role="tablist">
<button type="button" class="btn" data-tab="1h">1h</button>
<button type="button" class="btn" data-tab="4h">4h</button>
<button type="button" class="btn" data-tab="24h">24h</button>
<button type="button" class="btn" data-tab="3d">3d</button>
<button type="button" class="btn" data-tab="7d">7d</button>
<button type="button" class="btn" data-tab="30d">30d</button>
<button type="button" class="btn" data-tab="all">all</button>
</nav>
</header>
<main class="stats-main">
<p class="meta">
hive-wide turn statistics, aggregated across every agent over the
selected window. <strong>cost is a rough estimate</strong> from
approximate per-model list prices — it drifts and is a ballpark, not a
bill.
</p>
<div class="hive-stats-chips" id="hive-stats-summary"></div>
<h3>◇ busiest agents</h3>
<div id="hive-stats-agents"><p class="meta">loading…</p></div>
<h3>◇ model mix (turns across the swarm)</h3>
<div id="hive-stats-models"></div>
<!-- "favorite tools": most-run bash commands across the swarm.
Header + list hidden until the hive-bash-daemon capture has
recorded data, so the section never shows an empty block. -->
<h3 id="hive-stats-bash-h" hidden>
◇ favorite tools (bash commands across the swarm)
</h3>
<div id="hive-stats-bash" hidden></div>
<!-- most-triggered skills across the swarm. Header + list hidden until
at least one agent has actually invoked a skill. -->
<h3 id="hive-stats-skills-h" hidden>
◇ skill mix (invocations across the swarm)
</h3>
<div id="hive-stats-skills" hidden></div>
</main>
<script type="module" src="/static/stats.js" defer></script>
</body>
</html>

View file

@ -1,189 +0,0 @@
// /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 { $, initStickyTop } 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);
}
}
initStickyTop();
// 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();
},
});

View file

@ -258,9 +258,8 @@ import { renderUserMenu } from "./user-menu.js";
// ─── tab routing ───────────────────────────────────────────────────────
// Hash-based: `#agents` / `#rebuild` / `#kept`
// activate the matching pane on the dashboard. Empty
// hash defaults to 4G3NTS. ST4TS is NOT a tab — it is a separate page
// (`/stats.html`) reached from the pages menu (links-menu.js). Tab routing
// only applies when the tab DOM is present.
// hash defaults to 4G3NTS. Tab routing only applies when the tab DOM is
// present.
// The shared hash-routed tab strip (@hive/shared/tabs.js) owns the
// tab/pane toggle + aria-selected + routing; `activateTab` runs the
// per-tab side-effects via the strip's onShow. Constructed below (after

View file

@ -1,9 +1,8 @@
/* ─── page chrome: back-link header ────────────────────────────────
The minimal sticky "frosted bar" header used by every standalone page
reached from the dashboard's pages menu — /stats. A back
link to the dashboard on the left, then a page title and, on /stats, a
sub-tab nav. The operator dashboard uses its own richer
sticky chrome (`.dashboard-chrome`); these classes cover the simple
The minimal sticky "frosted bar" header used by every standalone page of
an agent's own UI (/stats, /screen). A back link on the left, then a page
title and, on /stats, a sub-tab nav. The operator dashboard uses its own
richer sticky chrome (`.dashboard-chrome`); these classes cover the simple
pages.
Shared across packages via @hive/shared so a new page only links the

View file

@ -1,6 +1,6 @@
// What the links menu shows for one link, without a DOM: where it opens and
// what it is called. Shared by the swarm UI (its `/api/links`) and the hive
// dashboard (its stats and API docs pages, and the swarm UI).
// dashboard (its API docs page and the swarm UI).
/** One entry of the links menu. */
export interface MenuLink {

View file

@ -17,7 +17,7 @@ test("a link the menu was handed opens in a new tab, without a referrer", () =>
test("a page of the same site opens in place", () => {
assert.deepEqual(
linkAttrs({ label: "stats", url: "/stats.html", external: false }),
linkAttrs({ label: "api docs", url: "/api/docs/", external: false }),
{},
);
});