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
32 changed files with 112 additions and 1207 deletions

View file

@ -12,8 +12,7 @@ what you actually do here.
Everything starts at the **dashboard**, served at `/`. A pages menu
(the link icon beside the profile avatar, at the right of the tab strip) links to the other
surfaces — Stats, the API docs and the swarm UI — and each of
those pages links back to the dashboard.
surfaces — the API docs and the swarm UI.
The dashboard is where you'll spend most of your time. It's a single
page with four tabs:
@ -25,9 +24,6 @@ page with four tabs:
- **K3PT ST4T3** — kept state from agents with no container
(tombstones), with a purge button per entry.
Usage stats live on their own page, reached from the pages menu:
**Stats** (`/stats.html`, swarm-wide usage stats).
Each agent also has its own page — a full terminal view of that
agent's session, reachable by selecting its name anywhere in the
dashboard, or directly at `/agent/<name>/` (or `http://<host>:<port>/`

View file

@ -12,15 +12,13 @@ hive-c0re serves the dashboard at `/`.
It has a fixed chrome header at the top and a `<main>` that shows exactly
one tab pane at a time. The URL hash (`#agents`, `#rebuild`, `#meta`,
`#kept`) drives which pane is
active; hash changes don't reload the page. ST4TS
is a standalone page reached from the pages menu, not from the
dashboard tab strip.
active; hash changes don't reload the page.
**Chrome header** (fixed, overlays the active tab pane):
- **Pages menu**: a link icon at the right end of the chrome, beside the
profile avatar (`links-menu.js`, `@hive/shared`'s `LinksMenu`, the same
component the swarm UI uses for its service links), opening a popover with links to **stats**
(`/stats.html`), **api docs** (`/api/docs/`) and **swarm** (the swarm
component the swarm UI uses for its service links), opening a popover with links to **api docs**
(`/api/docs/`) and **swarm** (the swarm
UI at the swarm apex domain, opened in a new tab). The swarm entry
comes from `state.swarm_public_url`, which hive-c0re reads from
`HIVE_SWARM_PUBLIC_URL` (set from `services.hyperhive.swarm.domain`);
@ -35,8 +33,7 @@ dashboard tab strip.
If the name doesn't load the avatar shows a question mark, and without the URL the
popover has no links.
- **Tab strip**: `◆ 4G3NTS ◆`, `◆ R3BU1LD QU3U3 ◆`,
`◆ K3PT ST4T3 ◆`. In-page tabs only — ST4TS
lives on its own page, reached from the pages menu.
`◆ K3PT ST4T3 ◆`.
- **Banner-thin** (`░▒▓█▓▒░ HYPERHIVE / HIVE-C0RE / WE ARE THE WIRED ░▒▓█▓▒░`)
— sits below the tab strip.
- **No warning banner.** No page shows hive-level warnings (host disk
@ -165,48 +162,6 @@ A build's output and history aren't on this page. hive-c0re logs every container
to the journal, and the swarm's `builds` Grafana board reads those lines: the builds with
their outcome and runtime, and the output of the one you pick.
## ST4TS page (`/stats.html`)
Hive-wide turn statistics, aggregated across every agent's
`hyperhive-turn-stats.sqlite` for the selected window. Not a dashboard
tab — a standalone page reached from the dashboard's pages menu,
minimal chrome (a back-link and a title). Distinct from
each agent's own `/stats` page (which carries the per-agent trend
charts): ST4TS is the swarm-level rollup.
- **Window selector** (`1h`, `4h`, `24h`, `3d`, `7d`, `30d`, `all`) —
a hash-routed tab strip (`#1h` / `#24h` / …) — re-fetches on change.
- **Summary chips**: active agents, turns, total/input/output/cache-read
tokens, and a labelled **est cost**.
- **Busiest agents** table — one row per agent (most turns first):
turns, input / output / cache-read tokens, est cost.
- **Model mix** — turns per model across the swarm, as CSS bars.
- **Favorite tools** — most-run normalised bash-command heads across the
swarm (top 10, as CSS bars), aggregated from each agent's
`bash_commands` table (written by the hive-bash-daemon capture). The
header + list stay hidden until at least one agent has recorded a
command, so the section never shows an empty block on a fresh hive.
Backed by `GET /api/stats-hive?window=<w>` in `hive-c0re`
(`hive_stats.rs`): for every name from
`Coordinator::kept_state_names()` it opens
`agent_harness_dir(name)/hyperhive-turn-stats.sqlite` read-only (with a
500 ms `busy_timeout`, since `turn_stats` is rollback-journal) and rolls
the rows up, skipping missing / unreadable / zero-turn dbs so one
bad db never fails the endpoint. This is a **pull** surface (no SSE):
the page fetches the data on page load and on window change. Rendered
with plain tables + CSS bars — the dashboard bundle ships no chart
library.
The cost figure is a deliberately rough estimate from a per-model
price table (`est_cost_usd`); it drifts with list pricing and the page
labels it accordingly. The table is operator-tunable via the
`services.hyperhive.deploy.hive-controller.modelPrices` nix option — each key is a
model-family short name (matched case-insensitively as a substring of
the model id, longest match wins) mapping to
`{ input, output, cache_read, cache_write }` USD-per-million-token
prices. Models not covered fall back to hive-c0re's built-in estimate.
## Container row
A full-height **square agent icon** (5em, capped) on the left. The
@ -509,12 +464,6 @@ below — some endpoints aren't in it yet.
build logs.
- `GET /api/reminders` — list pending reminders for the
dashboard's queued-reminders panel.
- `GET /api/stats-hive?window=<1h|4h|24h|3d|7d|30d|all>` — hive-wide
turn-stats rollup for the ST4TS tab. Aggregates every agent's
`hyperhive-turn-stats.sqlite` read-only (skips missing / unreadable /
zero-turn dbs); returns swarm totals, a busiest-first per-agent
rollup, swarm model mix, and a labelled `est_cost_usd`. Window
defaults to `24h`.
- `POST /api/cancel-reminder/{id}` — hard-delete a pending reminder.
- `POST /api/retry-reminder/{id}` — re-arm a reminder whose delivery
failed (clears the failure state so the scheduler retries).

View file

@ -16,13 +16,10 @@
- `GET /static/*` → bundled CSS + JS produced by esbuild
(`frontend/packages/{dashboard,agent}/build.mjs`). Both pages
pull the shared terminal pane + Catppuccin palette + typography
from `@hive/shared` (was `hive-fr0nt`); the dashboard ships one
CSS bundle per page (`colors.css` + `theme.css` + `common.css`,
loaded by every page, plus a page-specific bundle —
`dashboard.css` /
`stats.css`);
from `@hive/shared` (was `hive-fr0nt`); the dashboard ships
`colors.css` + `theme.css` + `common.css` + `dashboard.css`;
`common.css` inlines `@hive/shared`'s
`base.css` + `terminal.css` + `tabs.css` + `chrome.css` +
`base.css` + `terminal.css` + `tabs.css` +
`pill.css` via esbuild's `@import` resolution.
`terminal.js` exports `{ create, linkify }` as ES module
members; a back-compat shim in the IIFE bodies still exposes

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

View file

@ -11,8 +11,8 @@
//! `note` (failure detail for `result_kind = "failed"`).
//!
//! Lives in the harness dir (`HYPERHIVE_HARNESS_DIR`) as
//! `hyperhive-turn-stats.sqlite`; the dashboard reads it read-only and no
//! vacuum sweeps it. Schema is
//! `hyperhive-turn-stats.sqlite`; the agent's own `/stats` page reads it
//! and no vacuum sweeps it. Schema is
//! intentionally append-only — every column has a default so future
//! additions don't break old readers; new columns land via
//! `ALTER TABLE ... ADD COLUMN ... DEFAULT ...` in the migration
@ -164,8 +164,8 @@ impl TurnStats {
}
let conn = Connection::open(path)
.with_context(|| format!("open turn_stats db {}", path.display()))?;
// Not WAL: hive-c0re reads this db read-only as another uid, which
// fails on a WAL database whose sidecars it cannot create.
// Rollback journal, not WAL: the default, and what every existing
// database already uses.
conn.busy_timeout(crate::sqlite_writer::BUSY_TIMEOUT)
.context("set turn_stats busy_timeout")?;
conn.execute_batch(SCHEMA)

View file

@ -4,7 +4,7 @@
//!
//! Best-effort: a missing db, a lock, or a parse miss is logged and
//! swallowed — recording a stat never affects task execution. The
//! read/aggregation side lives host-side in `hive-c0re` and reads the
//! read/aggregation side is the agent harness's `stats.rs`, which reads the
//! same `bash_commands(ts INTEGER, head TEXT)` rows.
//!
//! Normalisation is deliberately rough (quoting-naive) — it just needs

View file

@ -69,13 +69,6 @@ pub struct Coordinator {
/// Same drop-in, same relative-share and `None` semantics as
/// [`Self::agent_cpu_weight`].
pub agent_io_weight: Option<u32>,
/// Operator-tunable model→price table backing the hive-wide cost
/// estimate on the ST4TS tab. Set via `services.hyperhive.modelPrices`
/// and passed to `hive-c0re serve --model-prices <json>`. Models not
/// in the table fall back to the built-in estimate in `hive_stats`.
/// hive-c0re-local (read only by the `/api/stats-hive` handler), so it
/// is *not* part of `HiveEnv` and is never injected into containers.
pub model_prices: crate::hive_stats::PriceTable,
agents: Mutex<HashMap<String, AgentSocket>>,
/// Tombstone for transients that have JUST been cleared. The
/// crash watcher polls every 10s and would race the
@ -215,9 +208,9 @@ impl Default for HiveEnv {
/// On-disk shape of the `hive-c0re serve --config <file>` JSON: the
/// container-injected [`HiveEnv`] (flattened) plus the hive-c0re-local
/// `model_prices` table (read only by the `/api/stats-hive` handler, never
/// injected into containers — hence kept out of `HiveEnv`). The NixOS
/// module writes this so the `ExecStart` carries one `--config` flag
/// settings that are never injected into containers — hence kept out of
/// `HiveEnv`. The NixOS module writes this so the `ExecStart` carries one
/// `--config` flag
/// instead of every host-level setting as its own JSON-blob argument.
/// `#[serde(default)]` lets any field be omitted and fall back to its
/// canonical default.
@ -226,10 +219,9 @@ impl Default for HiveEnv {
pub struct ServeConfig {
#[serde(flatten)]
pub env: HiveEnv,
pub model_prices: crate::hive_stats::PriceTable,
/// Number of concurrent closure-fetching job-queue nodes (prebuild /
/// profile-swap / create / meta lock). hive-c0re-local like
/// `model_prices` — never injected into containers. Set via
/// profile-swap / create / meta lock). hive-c0re-local — never
/// injected into containers. Set via
/// `services.hyperhive.deploy.hive-controller.buildSlots`.
pub build_slots: usize,
}
@ -238,7 +230,6 @@ impl Default for ServeConfig {
fn default() -> Self {
Self {
env: HiveEnv::default(),
model_prices: crate::hive_stats::PriceTable::default(),
build_slots: 1,
}
}
@ -250,16 +241,14 @@ mod serve_config_tests {
#[test]
fn deserialises_flat_with_per_field_defaults() {
// The flattened HiveEnv fields + the local model_prices all live at
// The flattened HiveEnv fields + the local build_slots all live at
// the JSON top level. Present fields are read; omitted ones fall
// back to their canonical defaults (so a partial config is valid).
let sc: ServeConfig = serde_json::from_str(
r#"{
"hyperhive_flake": "/x",
"dashboard_port": 1234,
"model_prices": {
"opus": {"input": 5.0, "output": 25.0, "cache_read": 0.5, "cache_write": 10.0}
}
"build_slots": 3
}"#,
)
.expect("flat ServeConfig should deserialise");
@ -267,7 +256,15 @@ mod serve_config_tests {
assert_eq!(sc.env.dashboard_port, 1234);
// omitted → HiveEnv::default()
assert_eq!(sc.env.agent_cpu_quota, "200%");
assert!(sc.model_prices.contains_key("opus"));
assert_eq!(sc.build_slots, 3);
}
#[test]
fn a_leftover_model_prices_key_is_ignored() {
// Configs written before the hive stats page went carry the key.
let sc: ServeConfig = serde_json::from_str(r#"{"model_prices": {"opus": {"input": 5.0}}}"#)
.expect("an unknown key must not break startup");
assert_eq!(sc.build_slots, 1);
}
#[test]
@ -275,7 +272,7 @@ mod serve_config_tests {
let sc: ServeConfig = serde_json::from_str("{}").expect("empty config valid");
assert_eq!(sc.env.dashboard_port, 7000);
assert_eq!(sc.env.hyperhive_flake, "/etc/hyperhive");
assert!(sc.model_prices.is_empty());
assert_eq!(sc.build_slots, 1);
}
}
@ -336,12 +333,7 @@ impl std::fmt::Display for SetPausedByNameError {
}
impl Coordinator {
pub fn open(
db_path: &Path,
env: HiveEnv,
model_prices: crate::hive_stats::PriceTable,
build_slots: usize,
) -> Result<Self> {
pub fn open(db_path: &Path, env: HiveEnv, build_slots: usize) -> Result<Self> {
let HiveEnv {
hyperhive_flake,
claude_code_path,
@ -386,7 +378,6 @@ impl Coordinator {
agent_memory_max,
agent_cpu_weight,
agent_io_weight,
model_prices,
agents: Mutex::new(HashMap::new()),
recent_transient: Mutex::new(HashMap::new()),
graceful_stop_pending: Mutex::new(HashSet::new()),
@ -1072,13 +1063,7 @@ mod open_tests {
use super::{Coordinator, HiveEnv};
fn open(db: &std::path::Path) -> Coordinator {
Coordinator::open(
db,
HiveEnv::default(),
crate::hive_stats::PriceTable::new(),
1,
)
.expect("open coordinator")
Coordinator::open(db, HiveEnv::default(), 1).expect("open coordinator")
}
/// A build log row the previous process left open is closed as failed

View file

@ -1,45 +1,15 @@
//! Remaining single-endpoint dashboard handlers: mark-all-read,
//! spawn-request and hive-wide turn stats.
//! Remaining single-endpoint dashboard handlers: mark-all-read and
//! spawn-request.
use axum::{
extract::{Path as AxumPath, State},
http::StatusCode,
response::{IntoResponse, Response},
};
use serde::{Deserialize, Serialize};
use utoipa::{IntoParams, ToSchema};
use serde::Serialize;
use utoipa::ToSchema;
use super::{AppState, Ident, error_response};
use crate::hive_stats::HiveStats;
#[derive(Deserialize, IntoParams)]
pub(super) struct StatsHiveQuery {
/// Stats window; defaults to `24h`.
window: Option<String>,
}
/// Hive-wide turn-stats rollup for the dashboard swarm-stats view.
///
/// Aggregates every agent's `hyperhive-turn-stats.sqlite` read-only
/// (skips missing/unreadable ones).
#[utoipa::path(
get,
path = "/api/stats-hive",
params(StatsHiveQuery),
responses((status = 200, description = "hive-wide turn-stats rollup", body = HiveStats)),
tag = "misc_api"
)]
pub(super) async fn api_stats_hive(
State(state): State<AppState>,
axum::extract::Query(q): axum::extract::Query<StatsHiveQuery>,
) -> Response {
let window = crate::hive_stats::Window::parse(q.window.as_deref().unwrap_or("24h"));
axum::Json(crate::hive_stats::hive_snapshot(
window,
&state.coord.model_prices,
))
.into_response()
}
#[derive(Serialize, ToSchema)]
pub(super) struct MarkAllReadBody {

View file

@ -103,7 +103,6 @@ fn app(coord: Arc<Coordinator>) -> axum::Router {
.routes(routes!(health::get_health_live))
.routes(routes!(health::get_health_ready))
.routes(routes!(state_snapshot::api_state))
.routes(routes!(misc_api::api_stats_hive))
.routes(routes!(misc_api::post_mark_all_read))
.routes(routes!(build_logs::get_build_logs_all))
.routes(routes!(build_logs::get_build_log_for_node))
@ -264,7 +263,6 @@ mod tests {
let coord = Coordinator::open(
&dir.path().join("broker.sqlite"),
crate::coordinator::HiveEnv::default(),
crate::hive_stats::PriceTable::new(),
1,
)
.expect("open coordinator");
@ -352,7 +350,6 @@ mod router_build_probe {
.routes(routes!(health::get_health_live))
.routes(routes!(health::get_health_ready))
.routes(routes!(state_snapshot::api_state))
.routes(routes!(misc_api::api_stats_hive))
.routes(routes!(misc_api::post_mark_all_read))
.routes(routes!(build_logs::get_build_logs_all))
.routes(routes!(build_logs::get_build_log_for_node))

Some files were not shown because too many files have changed in this diff Show more