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:
parent
1d3ab2fae5
commit
5e9a22cb38
32 changed files with 112 additions and 1207 deletions
|
|
@ -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>/`
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(""));
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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. */
|
||||
|
|
|
|||
|
|
@ -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 -->
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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();
|
||||
},
|
||||
});
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 }),
|
||||
{},
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Reference in a new issue