From 5e9a22cb386f5ed755ebf37ca4e265522ea36d6c Mon Sep 17 00:00:00 2001 From: iris Date: Mon, 2 Nov 2026 23:15:51 +0100 Subject: [PATCH] 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. --- docs/web-ui/README.md | 6 +- docs/web-ui/dashboard.md | 59 +- docs/web-ui/shape.md | 9 +- frontend/packages/dashboard/build.mjs | 12 +- frontend/packages/dashboard/src/common.css | 32 +- frontend/packages/dashboard/src/dashboard.css | 4 - frontend/packages/dashboard/src/index.html | 14 +- frontend/packages/dashboard/src/links-menu.js | 5 +- frontend/packages/dashboard/src/stats.css | 97 ---- frontend/packages/dashboard/src/stats.html | 63 --- frontend/packages/dashboard/src/stats.js | 189 ------- frontend/packages/dashboard/src/tabs.js | 5 +- frontend/packages/shared/src/chrome.css | 9 +- .../packages/shared/src/links-menu/links.ts | 2 +- .../swarm-ui/test/links-menu.test.mjs | 2 +- hive-agent/src/turn_stats.rs | 8 +- hive-bash-mcp/src/stats.rs | 2 +- hive-c0re/src/coordinator.rs | 53 +- hive-c0re/src/dashboard/misc_api.rs | 38 +- hive-c0re/src/dashboard/mod.rs | 3 - hive-c0re/src/job_queue/exec/tests.rs | 9 +- hive-c0re/src/lifecycle/host_config.rs | 6 +- hive-c0re/src/main.rs | 24 +- hive-c0re/src/server.rs | 1 - hive-c0re/src/socket_server/mod.rs | 1 - hive-c0re/src/stats/hive_stats.rs | 514 ------------------ hive-c0re/src/stats/mod.rs | 9 +- nix/host-modules/deploy.nix | 4 - nix/host-modules/hive-c0re/default.nix | 4 +- nix/host-modules/hive-c0re/options.nix | 86 +-- nix/module-eval/core-toggle.nix | 43 +- nix/packages/frontend.nix | 6 +- 32 files changed, 112 insertions(+), 1207 deletions(-) delete mode 100644 frontend/packages/dashboard/src/stats.css delete mode 100644 frontend/packages/dashboard/src/stats.html delete mode 100644 frontend/packages/dashboard/src/stats.js delete mode 100644 hive-c0re/src/stats/hive_stats.rs diff --git a/docs/web-ui/README.md b/docs/web-ui/README.md index f558352d..d16afe7f 100644 --- a/docs/web-ui/README.md +++ b/docs/web-ui/README.md @@ -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//` (or `http://:/` diff --git a/docs/web-ui/dashboard.md b/docs/web-ui/dashboard.md index 0004f031..028a8c0b 100644 --- a/docs/web-ui/dashboard.md +++ b/docs/web-ui/dashboard.md @@ -12,15 +12,13 @@ hive-c0re serves the dashboard at `/`. It has a fixed chrome header at the top and a `
` 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=` 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). diff --git a/docs/web-ui/shape.md b/docs/web-ui/shape.md index 30fdd0bc..d1c9c5c1 100644 --- a/docs/web-ui/shape.md +++ b/docs/web-ui/shape.md @@ -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 diff --git a/frontend/packages/dashboard/build.mjs b/frontend/packages/dashboard/build.mjs index 92632559..362b6e1f 100644 --- a/frontend/packages/dashboard/build.mjs +++ b/frontend/packages/dashboard/build.mjs @@ -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/.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("")); diff --git a/frontend/packages/dashboard/src/common.css b/frontend/packages/dashboard/src/common.css index bc1d7c22..1b8720b5 100644 --- a/frontend/packages/dashboard/src/common.css +++ b/frontend/packages/dashboard/src/common.css @@ -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 `.-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; -} diff --git a/frontend/packages/dashboard/src/dashboard.css b/frontend/packages/dashboard/src/dashboard.css index cd005b55..1eb5c6a5 100644 --- a/frontend/packages/dashboard/src/dashboard.css +++ b/frontend/packages/dashboard/src/dashboard.css @@ -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. */ diff --git a/frontend/packages/dashboard/src/index.html b/frontend/packages/dashboard/src/index.html index 75781aad..f5423906 100644 --- a/frontend/packages/dashboard/src/index.html +++ b/frontend/packages/dashboard/src/index.html @@ -55,15 +55,10 @@ - -
@@ -148,11 +143,6 @@

loading…

- -
diff --git a/frontend/packages/dashboard/src/links-menu.js b/frontend/packages/dashboard/src/links-menu.js index 3b1d3e83..fe5b4558 100644 --- a/frontend/packages/dashboard/src/links-menu.js +++ b/frontend/packages/dashboard/src/links-menu.js @@ -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) { diff --git a/frontend/packages/dashboard/src/stats.css b/frontend/packages/dashboard/src/stats.css deleted file mode 100644 index 3b4a3ccc..00000000 --- a/frontend/packages/dashboard/src/stats.css +++ /dev/null @@ -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; -} diff --git a/frontend/packages/dashboard/src/stats.html b/frontend/packages/dashboard/src/stats.html deleted file mode 100644 index 5230fb5a..00000000 --- a/frontend/packages/dashboard/src/stats.html +++ /dev/null @@ -1,63 +0,0 @@ - - - - - - hyperhive // ST4TS - - - - - - - - - - -
-

- hive-wide turn statistics, aggregated across every agent over the - selected window. cost is a rough estimate from - approximate per-model list prices — it drifts and is a ballpark, not a - bill. -

- -
-

◇ busiest agents

-

loading…

-

◇ model mix (turns across the swarm)

-
- - - - - - -
- - - - diff --git a/frontend/packages/dashboard/src/stats.js b/frontend/packages/dashboard/src/stats.js deleted file mode 100644 index 93b9f118..00000000 --- a/frontend/packages/dashboard/src/stats.js +++ /dev/null @@ -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 = - "agentturnsinput" + - "outputcache readest cost"; - const tb = document.createElement("tbody"); - for (const a of agents) { - const tr = document.createElement("tr"); - const cells = [ - a.name, - hsFmtInt(a.turns), - hsFmtTokens(a.input_tokens), - hsFmtTokens(a.output_tokens), - hsFmtTokens(a.cache_read_tokens), - hsFmtUsd(a.est_cost_usd), - ]; - cells.forEach((txt, i) => { - const td = document.createElement("td"); - if (i > 0) td.className = "num"; - td.textContent = txt; - tr.append(td); - }); - tb.append(tr); - } - table.append(tb); - at.append(table); - } - } - - const mm = $("hive-stats-models"); - if (mm) { - const mix = s.model_mix || []; - if (!mix.length) hsMeta(mm, "no turns in window"); - else renderKeyCountBars(mm, mix); - } - - // "favorite tools": most-run bash commands across the swarm. - renderOptionalMix("hive-stats-bash", "hive-stats-bash-h", s.bash_mix || []); - // Most-triggered skills across the swarm. - renderOptionalMix( - "hive-stats-skills", - "hive-stats-skills-h", - s.skill_mix || [], - ); -} - -async function refreshHiveStats() { - try { - const resp = await fetch( - "/api/stats-hive?window=" + encodeURIComponent(hiveStatsWindow), - ); - if (!resp.ok) throw new Error("http " + resp.status); - renderHiveStats(await resp.json()); - } catch (e) { - const at = $("hive-stats-agents"); - if (at) hsMeta(at, "stats fetch failed: " + e); - } -} - -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(); - }, -}); diff --git a/frontend/packages/dashboard/src/tabs.js b/frontend/packages/dashboard/src/tabs.js index e308b978..89dc6e3e 100644 --- a/frontend/packages/dashboard/src/tabs.js +++ b/frontend/packages/dashboard/src/tabs.js @@ -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 diff --git a/frontend/packages/shared/src/chrome.css b/frontend/packages/shared/src/chrome.css index e5bc0d4e..284af914 100644 --- a/frontend/packages/shared/src/chrome.css +++ b/frontend/packages/shared/src/chrome.css @@ -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 diff --git a/frontend/packages/shared/src/links-menu/links.ts b/frontend/packages/shared/src/links-menu/links.ts index a5114ab4..18a1ccb4 100644 --- a/frontend/packages/shared/src/links-menu/links.ts +++ b/frontend/packages/shared/src/links-menu/links.ts @@ -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 { diff --git a/frontend/packages/swarm-ui/test/links-menu.test.mjs b/frontend/packages/swarm-ui/test/links-menu.test.mjs index 90076dea..2bec90b1 100644 --- a/frontend/packages/swarm-ui/test/links-menu.test.mjs +++ b/frontend/packages/swarm-ui/test/links-menu.test.mjs @@ -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 }), {}, ); }); diff --git a/hive-agent/src/turn_stats.rs b/hive-agent/src/turn_stats.rs index 39b0ace4..d49121ac 100644 --- a/hive-agent/src/turn_stats.rs +++ b/hive-agent/src/turn_stats.rs @@ -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) diff --git a/hive-bash-mcp/src/stats.rs b/hive-bash-mcp/src/stats.rs index 0d158fe9..24795868 100644 --- a/hive-bash-mcp/src/stats.rs +++ b/hive-bash-mcp/src/stats.rs @@ -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 diff --git a/hive-c0re/src/coordinator.rs b/hive-c0re/src/coordinator.rs index 471beaa1..691c8060 100644 --- a/hive-c0re/src/coordinator.rs +++ b/hive-c0re/src/coordinator.rs @@ -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, - /// 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 `. 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>, /// 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 ` 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 { + pub fn open(db_path: &Path, env: HiveEnv, build_slots: usize) -> Result { 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 diff --git a/hive-c0re/src/dashboard/misc_api.rs b/hive-c0re/src/dashboard/misc_api.rs index e4d6d4d4..50a0fede 100644 --- a/hive-c0re/src/dashboard/misc_api.rs +++ b/hive-c0re/src/dashboard/misc_api.rs @@ -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, -} - -/// 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, - axum::extract::Query(q): axum::extract::Query, -) -> 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 { diff --git a/hive-c0re/src/dashboard/mod.rs b/hive-c0re/src/dashboard/mod.rs index 98ce9f6e..41071e50 100644 --- a/hive-c0re/src/dashboard/mod.rs +++ b/hive-c0re/src/dashboard/mod.rs @@ -103,7 +103,6 @@ fn app(coord: Arc) -> 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)) diff --git a/hive-c0re/src/job_queue/exec/tests.rs b/hive-c0re/src/job_queue/exec/tests.rs index 3f654d31..56ca798c 100644 --- a/hive-c0re/src/job_queue/exec/tests.rs +++ b/hive-c0re/src/job_queue/exec/tests.rs @@ -280,13 +280,8 @@ fn closure(cached: bool) -> AgentClosure { fn coordinator() -> (tempfile::TempDir, Arc) { let dir = tempfile::tempdir().expect("tempdir"); - let coord = Coordinator::open( - &dir.path().join("broker.sqlite"), - HiveEnv::default(), - crate::hive_stats::PriceTable::new(), - 1, - ) - .expect("open coordinator"); + let coord = Coordinator::open(&dir.path().join("broker.sqlite"), HiveEnv::default(), 1) + .expect("open coordinator"); (dir, Arc::new(coord)) } diff --git a/hive-c0re/src/lifecycle/host_config.rs b/hive-c0re/src/lifecycle/host_config.rs index 8885358a..669f07d7 100644 --- a/hive-c0re/src/lifecycle/host_config.rs +++ b/hive-c0re/src/lifecycle/host_config.rs @@ -301,9 +301,9 @@ async fn systemd_daemon_reload() -> Result<()> { /// `harness` and `work` are absent too: they hold that agent's own runtime /// material (`bash-tasks/`, turn-stats and event sqlite dbs) and its, often /// very large, worktrees and build trees, none of which the holder has a stated -/// reason to read, let alone write. hive-c0re still reads `harness` directly on -/// the host (`stats::hive_stats`), which needs no bind mount into anyone else's -/// container. +/// reason to read, let alone write. hive-c0re itself reaches `harness` directly +/// on the host (the paused marker), which needs no bind mount into anyone +/// else's container. fn bind_child_agent_dirs(child: &str, binds: &mut Vec) { let Ok(child) = hive_types::Ident::parse(child) else { tracing::warn!(%child, "skipping child bind: invalid agent name"); diff --git a/hive-c0re/src/main.rs b/hive-c0re/src/main.rs index 5ac6e43f..bbebfb2e 100644 --- a/hive-c0re/src/main.rs +++ b/hive-c0re/src/main.rs @@ -45,7 +45,7 @@ mod test_env; mod workers; pub(crate) use agent_config::{archetypes, limits, permissions, resource_limits, topology}; -pub(crate) use stats::{container_stats, hive_stats, host_stats, otel_metrics, warnings}; +pub(crate) use stats::{container_stats, host_stats, otel_metrics, warnings}; pub(crate) use stores::{broker, build_logs, db, power}; pub(crate) use workers::{agent_sockets, auto_update, crash_watch, knowledge, mcp_sockets}; @@ -69,7 +69,7 @@ enum Cmd { /// Path to a JSON config file holding the host-level daemon config /// (the [`ServeConfig`] shape: /// the container-injected `HiveEnv` fields + the hive-c0re-local - /// `model_prices` table). Used as the base; any per-flag override + /// `build_slots`). Used as the base; any per-flag override /// below wins over the file. Absent → start from the built-in /// defaults. The NixOS module passes a generated config here so the /// `ExecStart` stays short instead of carrying every setting — and @@ -106,14 +106,6 @@ enum Cmd { /// `services.hyperhive.agentIoWeight`. #[arg(long)] agent_io_weight: Option, - /// Override: per-model USD prices (per million tokens) for the - /// hive-wide ST4TS cost estimate, as a JSON object mapping a - /// model-family short name to `{input, output, cache_read, - /// cache_write}`. Models not covered fall back to the built-in - /// estimate. Set via the `services.hyperhive.modelPrices` NixOS - /// option. - #[arg(long)] - model_prices: Option, /// Override: number of concurrent closure-fetching job-queue nodes /// (prebuild / profile-swap / create / meta lock). Set via the /// `services.hyperhive.deploy.hive-controller.buildSlots` NixOS option. @@ -138,7 +130,6 @@ async fn main() -> Result<()> { agent_memory_max, agent_cpu_weight, agent_io_weight, - model_prices, build_slots, } => { // Base config from the --config file (or the built-in @@ -175,10 +166,6 @@ async fn main() -> Result<()> { if let Some(v) = agent_io_weight { sc.env.agent_io_weight = Some(v); } - if let Some(v) = model_prices { - sc.model_prices = - serde_json::from_str(&v).context("--model-prices: invalid JSON")?; - } if let Some(v) = build_slots { sc.build_slots = v; } @@ -194,7 +181,7 @@ async fn main() -> Result<()> { // The forge integration is mandatory (the NixOS module sets both // vars unconditionally), so check them before any worker starts. forge::validate_env()?; - cmd_serve(sc.env, sc.model_prices, sc.build_slots, db, &cli.socket).await + cmd_serve(sc.env, sc.build_slots, db, &cli.socket).await } } } @@ -225,7 +212,6 @@ fn submit_periodic_sweep( )] async fn cmd_serve( env: HiveEnv, - model_prices: crate::hive_stats::PriceTable, build_slots: usize, db: std::path::PathBuf, socket: &std::path::Path, @@ -238,7 +224,7 @@ async fn cmd_serve( // `dashboard_port` is consumed into the Coordinator below; capture the // Copy value first for the dashboard + knowledge-webhook tasks. let dashboard_port = env.dashboard_port; - let coord = Arc::new(Coordinator::open(&db, env, model_prices, build_slots)?); + let coord = Arc::new(Coordinator::open(&db, env, build_slots)?); // Idempotent pre-flight: rewrite pre-meta-layout applied // repos, refresh the files that follow the agent set. // Runs before the boot reconcile. @@ -340,7 +326,7 @@ async fn cmd_serve( // agent-owned, so host-side deletes hit PermissionDenied / readonly-db // under privsep. See issue tracker "perms borked". // (turn-stats.sqlite has no vacuum — it's one tiny row per turn, - // ~hundreds of KB, and the /stats + hive-stats views read it + // ~hundreds of KB, and the agent's own /stats page reads it // directly; pruning it would just lose trend history for no gain.) // Slow per-container disk sampler: hive-priv measures each agent's state/ // + work/ + writable rootfs every 30 min, cached so a sample only reads diff --git a/hive-c0re/src/server.rs b/hive-c0re/src/server.rs index 070b4ba7..a7413f1e 100644 --- a/hive-c0re/src/server.rs +++ b/hive-c0re/src/server.rs @@ -794,7 +794,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"); diff --git a/hive-c0re/src/socket_server/mod.rs b/hive-c0re/src/socket_server/mod.rs index b3ac7271..68d1fc2b 100644 --- a/hive-c0re/src/socket_server/mod.rs +++ b/hive-c0re/src/socket_server/mod.rs @@ -577,7 +577,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"); diff --git a/hive-c0re/src/stats/hive_stats.rs b/hive-c0re/src/stats/hive_stats.rs deleted file mode 100644 index dcec8c37..00000000 --- a/hive-c0re/src/stats/hive_stats.rs +++ /dev/null @@ -1,514 +0,0 @@ -//! Hive-wide turn-stats aggregation for the dashboard's swarm stats -//! view. Reads every agent's per-agent -//! `hyperhive-turn-stats.sqlite` read-only and rolls the rows up into -//! swarm totals + a per-agent rollup + model mix + a *labelled* -//! cost estimate. -//! -//! Why re-read the rows here instead of reusing `hive-agent`'s -//! `stats.rs`: that module lives in a different crate (the agent -//! harness) which hive-c0re can't import. The stable contract is the -//! turn-stats *schema*, so we run a focused query against it. If we -//! ever want a single source of truth, the row-read + aggregation can -//! be lifted into a shared crate — overkill for now. -//! -//! Privsep: the sqlite files are mode 0644 owned by the agent user; -//! `hive-core` can read them fine, but cannot write/delete (which is why -//! retention sweeps run agent-side in the harness, not here). We open -//! read-only so an in-flight harness writer never blocks us. - -use std::collections::HashMap; -use std::path::Path; -use std::time::Duration; - -use rusqlite::{Connection, OpenFlags}; -use serde::{Deserialize, Serialize}; -use utoipa::ToSchema; - -use crate::coordinator::Coordinator; -use chrono::Utc; - -/// Window accepted by `/api/stats-hive?window=`. Maps to a lookback -/// span; the hive view is a flat rollup (no per-bucket trend — the -/// per-agent `/stats` page owns the trend charts). -#[derive(Debug, Clone, Copy)] -pub enum Window { - Hour, - FourHour, - Day, - ThreeDay, - Week, - Month, - /// All available data: aggregate over every recorded turn since the - /// earliest (`from == 0`), with no fixed lookback. - All, -} - -impl Window { - #[must_use] - pub fn parse(s: &str) -> Self { - match s { - "1h" => Self::Hour, - "4h" => Self::FourHour, - "3d" => Self::ThreeDay, - "7d" => Self::Week, - "30d" => Self::Month, - "all" => Self::All, - _ => Self::Day, - } - } - - fn label(self) -> &'static str { - match self { - Self::Hour => "1h", - Self::FourHour => "4h", - Self::Day => "24h", - Self::ThreeDay => "3d", - Self::Week => "7d", - Self::Month => "30d", - Self::All => "all", - } - } - - fn span_secs(self) -> i64 { - match self { - Self::Hour => 3600, - Self::FourHour => 4 * 3600, - Self::Day => 24 * 3600, - Self::ThreeDay => 3 * 24 * 3600, - Self::Week => 7 * 24 * 3600, - Self::Month => 30 * 24 * 3600, - // `All` has no fixed lookback; `hive_snapshot` uses `from == 0` - // (every recorded turn). 0 here is a consistent safe fallback. - Self::All => 0, - } - } -} - -/// Approximate USD price per **million** tokens for one model class. -/// The four fields map to the four token kinds the turn-stats schema -/// records. This is a deliberately rough estimate — Anthropic list -/// pricing drifts, so the dashboard labels the figure as an estimate. -/// -/// Operators keep the table current without a code change via the -/// `services.hyperhive.modelPrices` nix option (passed to -/// `hive-c0re serve --model-prices `, held on the `Coordinator` -/// as a [`PriceTable`]). Any model not covered by the operator table -/// falls back to [`builtin_prices`]. -#[derive(Debug, Clone, Copy, Serialize, Deserialize)] -pub struct Prices { - pub input: f64, - pub output: f64, - pub cache_read: f64, - pub cache_write: f64, -} - -/// Operator-tunable model→price map. Keys are matched -/// case-insensitively as a substring of the model id (e.g. `"sonnet"` -/// matches `"claude-sonnet-4-5"`); the longest matching key wins so a -/// specific entry beats a generic family name. An empty map means -/// every model uses [`builtin_prices`]. -pub type PriceTable = HashMap; - -/// Built-in fallback pricing, per **million** tokens. Used when the -/// operator's [`PriceTable`] has no key matching the model. Figures are -/// current Anthropic list prices for the Claude 4.x family (Opus 4.x, -/// Sonnet 4.x, Haiku 4.5); `cache_write` uses the 1-hour cache-TTL price, -/// which is the default through the Claude subscription the agents run -/// on. Keep in sync with the `services.hyperhive.modelPrices` nix default -/// (`nix/host-modules/hive-c0re/options.nix`). -fn builtin_prices(model: &str) -> Prices { - let m = model.to_ascii_lowercase(); - if m.contains("opus") { - Prices { - input: 5.0, - output: 25.0, - cache_read: 0.5, - cache_write: 10.0, - } - } else if m.contains("haiku") { - Prices { - input: 1.0, - output: 5.0, - cache_read: 0.1, - cache_write: 2.0, - } - } else { - // sonnet + unknown fallback - Prices { - input: 3.0, - output: 15.0, - cache_read: 0.3, - cache_write: 6.0, - } - } -} - -/// Resolve the price for `model`: prefer the operator-provided -/// `table` (longest matching key wins, so a specific -/// `claude-3-5-sonnet` entry beats a generic `sonnet`), else fall back -/// to [`builtin_prices`]. -fn resolve_prices(model: &str, table: &PriceTable) -> Prices { - let m = model.to_ascii_lowercase(); - let mut best: Option<(usize, Prices)> = None; - for (k, v) in table { - let key = k.to_ascii_lowercase(); - if key.is_empty() || !m.contains(&key) { - continue; - } - let better = match best { - Some((blen, _)) => key.len() > blen, - None => true, - }; - if better { - best = Some((key.len(), *v)); - } - } - match best { - Some((_, p)) => p, - None => builtin_prices(&m), - } -} - -#[derive(Debug, Serialize, ToSchema)] -pub struct KeyCount { - pub key: String, - pub count: u64, -} - -#[derive(Debug, Serialize, ToSchema)] -pub struct AgentRollup { - pub name: String, - pub turns: u64, - pub input_tokens: u64, - pub output_tokens: u64, - pub cache_read_tokens: u64, - pub cache_creation_tokens: u64, - /// Labelled estimate — see [`Prices`]. - pub est_cost_usd: f64, -} - -#[derive(Debug, Serialize, ToSchema)] -pub struct HiveStats { - pub window: &'static str, - pub from: i64, - pub now: i64, - /// Number of agents that had at least one turn in the window. - pub active_agents: u64, - pub total_turns: u64, - pub total_input_tokens: u64, - pub total_output_tokens: u64, - pub total_cache_read_tokens: u64, - pub total_cache_creation_tokens: u64, - /// Labelled estimate — see [`Prices`]. - pub est_cost_usd: f64, - /// Per-agent rollup, busiest (most turns) first. - pub agents: Vec, - /// Turns per model across the whole swarm, busiest first. - pub model_mix: Vec, - /// Most-run normalised bash-command heads ("favorite tools") across - /// the whole swarm, busiest first, capped to 10. Empty until the - /// hive-bash-daemon capture has recorded `bash_commands` rows on at - /// least one active agent. - pub bash_mix: Vec, - /// Most-triggered skills (fully-qualified `plugin:skill-name`) across - /// the whole swarm, busiest first, capped to 10. Sourced from each - /// agent's `tool_call_breakdown_json`. Empty until at least one agent has - /// actually invoked a skill. - pub skill_mix: Vec, -} - -#[derive(Default)] -struct AgentAgg { - turns: u64, - input: u64, - output: u64, - cache_read: u64, - cache_creation: u64, - cost: f64, - models: HashMap, - /// Normalised bash-command head → invocation count, from the agent's - /// `bash_commands` table (written by hive-bash-daemon). Empty when that - /// capture hasn't run for this agent (table absent). - bash: HashMap, - /// Skill (fully-qualified `plugin:skill-name`) → invocation count, - /// unpacked from `turn_stats.tool_call_breakdown_json`'s `Skill:*` - /// entries. Empty when no skill was invoked in the window. - skills: HashMap, -} - -fn u64_from_i64(v: i64) -> u64 { - u64::try_from(v).unwrap_or(0) -} - -/// Aggregate one agent's turn-stats over `[from, now]`. Errors bubble -/// up so the caller can skip a single bad/locked db without failing -/// the whole endpoint. -fn read_agent(path: &Path, from: i64, prices: &PriceTable) -> rusqlite::Result { - let conn = Connection::open_with_flags(path, OpenFlags::SQLITE_OPEN_READ_ONLY)?; - // turn_stats is rollback-journal (not WAL): a read landing while the - // harness is mid-INSERT would get SQLITE_BUSY and drop that active - // agent from the rollup. Wait out the brief write instead. - conn.busy_timeout(Duration::from_millis(500))?; - let mut stmt = conn.prepare( - "SELECT model, input_tokens, output_tokens, - cache_read_input_tokens, cache_creation_input_tokens, - tool_call_breakdown_json - FROM turn_stats - WHERE started_at >= ?1", - )?; - let mut agg = AgentAgg::default(); - let rows = stmt.query_map([from], |row| { - Ok(( - row.get::<_, String>(0)?, - u64_from_i64(row.get::<_, i64>(1)?), - u64_from_i64(row.get::<_, i64>(2)?), - u64_from_i64(row.get::<_, i64>(3)?), - u64_from_i64(row.get::<_, i64>(4)?), - row.get::<_, Option>(5)?, - )) - })?; - for r in rows { - let (model, input, output, cache_read, cache_creation, breakdown_json) = r?; - agg.turns += 1; - agg.input = agg.input.saturating_add(input); - agg.output = agg.output.saturating_add(output); - agg.cache_read = agg.cache_read.saturating_add(cache_read); - agg.cache_creation = agg.cache_creation.saturating_add(cache_creation); - let p = resolve_prices(&model, prices); - #[allow( - clippy::cast_precision_loss, - reason = "token counts stay well under f64's 2^53 exact-integer range, so this cost computation loses no precision" - )] - { - agg.cost += (input as f64 * p.input - + output as f64 * p.output - + cache_read as f64 * p.cache_read - + cache_creation as f64 * p.cache_write) - / 1_000_000.0; - } - *agg.models.entry(model).or_insert(0) += 1; - if let Some(json) = breakdown_json { - for (skill, count) in skills_from_breakdown(&json) { - *agg.skills.entry(skill).or_insert(0) += count; - } - } - } - agg.bash = read_bash_heads(&conn, from); - Ok(agg) -} - -/// Pull the `Skill:` entries out of one turn's -/// `tool_call_breakdown_json` blob (see `hive_agent::events::observe_stream` -/// for how they're written), stripping the `Skill:` marker so the rollup -/// keys by the bare (fully-qualified `plugin:skill-name`) skill identifier. -/// Malformed/absent JSON yields nothing rather than erroring — one bad row -/// shouldn't drop a whole agent from the rollup. -fn skills_from_breakdown(json: &str) -> Vec<(String, u64)> { - let Ok(map) = serde_json::from_str::>(json) else { - return Vec::new(); - }; - map.into_iter() - .filter_map(|(k, v)| k.strip_prefix("Skill:").map(|s| (s.to_owned(), v))) - .collect() -} - -/// Tally normalised bash-command heads from the agent's `bash_commands` -/// table (`ts INTEGER, head TEXT`, written by hive-bash-daemon) over -/// `[from, now]`. Best-effort + isolated from `read_agent`'s error path: -/// a missing table (capture hasn't run for this agent) or any read error -/// yields an empty map rather than propagating, so the favorite-tools -/// rollup simply omits that agent and never fails the whole endpoint. -/// `ts` is unix seconds, matching the `from` cutoff. -fn read_bash_heads(conn: &Connection, from: i64) -> HashMap { - let mut out: HashMap = HashMap::new(); - let Ok(mut stmt) = - conn.prepare("SELECT head, COUNT(*) FROM bash_commands WHERE ts >= ?1 GROUP BY head") - else { - return out; - }; - let Ok(rows) = stmt.query_map([from], |row| { - Ok(( - row.get::<_, String>(0)?, - u64_from_i64(row.get::<_, i64>(1)?), - )) - }) else { - return out; - }; - for (head, count) in rows.flatten() { - *out.entry(head).or_insert(0) += count; - } - out -} - -/// Build the swarm-wide rollup. Best-effort: a missing or unreadable -/// per-agent db is skipped (logged), never fatal. -#[must_use] -pub fn hive_snapshot(window: Window, prices: &PriceTable) -> HiveStats { - let now = Utc::now().timestamp(); - // Fixed windows look back a constant span; `all` aggregates every - // recorded turn (`from == 0`). The hive rollup isn't time-bucketed, so - // unlike the per-agent snapshot it needs no adaptive bucket sizing. - let from = match window { - Window::All => 0, - _ => now - window.span_secs(), - }; - - let mut agents: Vec = Vec::new(); - let mut model_mix: HashMap = HashMap::new(); - let mut bash_mix: HashMap = HashMap::new(); - let mut skill_mix: HashMap = HashMap::new(); - let mut total_turns = 0u64; - let mut total_input = 0u64; - let mut total_output = 0u64; - let mut total_cache_read = 0u64; - let mut total_cache_creation = 0u64; - let mut total_cost = 0.0f64; - let mut active_agents = 0u64; - - for name in Coordinator::kept_state_names() { - let path = Coordinator::agent_harness_dir(&name).join("hyperhive-turn-stats.sqlite"); - if !path.exists() { - continue; - } - let agg = match read_agent(&path, from, prices) { - Ok(a) => a, - Err(e) => { - tracing::warn!(agent = %name, error = ?e, "hive-stats: read failed; skipping"); - continue; - } - }; - if agg.turns == 0 { - continue; - } - active_agents += 1; - total_turns += agg.turns; - total_input = total_input.saturating_add(agg.input); - total_output = total_output.saturating_add(agg.output); - total_cache_read = total_cache_read.saturating_add(agg.cache_read); - total_cache_creation = total_cache_creation.saturating_add(agg.cache_creation); - total_cost += agg.cost; - for (m, c) in &agg.models { - *model_mix.entry(m.clone()).or_insert(0) += c; - } - for (h, c) in &agg.bash { - *bash_mix.entry(h.clone()).or_insert(0) += c; - } - for (s, c) in &agg.skills { - *skill_mix.entry(s.clone()).or_insert(0) += c; - } - agents.push(AgentRollup { - name: name.into_string(), - turns: agg.turns, - input_tokens: agg.input, - output_tokens: agg.output, - cache_read_tokens: agg.cache_read, - cache_creation_tokens: agg.cache_creation, - est_cost_usd: agg.cost, - }); - } - - // Busiest agents first. - agents.sort_by(|a, b| b.turns.cmp(&a.turns).then_with(|| a.name.cmp(&b.name))); - - let mut model_mix: Vec = model_mix - .into_iter() - .map(|(key, count)| KeyCount { key, count }) - .collect(); - model_mix.sort_by(|a, b| b.count.cmp(&a.count).then_with(|| a.key.cmp(&b.key))); - - // Busiest commands first, capped to a top-10 "favorite tools" list. - let mut bash_mix: Vec = bash_mix - .into_iter() - .map(|(key, count)| KeyCount { key, count }) - .collect(); - bash_mix.sort_by(|a, b| b.count.cmp(&a.count).then_with(|| a.key.cmp(&b.key))); - bash_mix.truncate(10); - - // Busiest skills first, capped to a top-10 list — same shape as bash_mix. - let mut skill_mix: Vec = skill_mix - .into_iter() - .map(|(key, count)| KeyCount { key, count }) - .collect(); - skill_mix.sort_by(|a, b| b.count.cmp(&a.count).then_with(|| a.key.cmp(&b.key))); - skill_mix.truncate(10); - - HiveStats { - window: window.label(), - from, - now, - active_agents, - total_turns, - total_input_tokens: total_input, - total_output_tokens: total_output, - total_cache_read_tokens: total_cache_read, - total_cache_creation_tokens: total_cache_creation, - est_cost_usd: total_cost, - agents, - model_mix, - bash_mix, - skill_mix, - } -} - -#[cfg(test)] -mod tests { - use super::*; - - /// `read_bash_heads` tallies per-head counts over the window and is - /// isolated from a missing table (returns empty, never errors). - #[test] - fn bash_heads_tally_and_window() { - let conn = Connection::open_in_memory().unwrap(); - conn.execute_batch("CREATE TABLE bash_commands (ts INTEGER NOT NULL, head TEXT NOT NULL);") - .unwrap(); - let now = Utc::now().timestamp(); - for (ts, head) in [ - (now - 100, "cargo"), - (now - 200, "cargo"), - (now - 300, "git"), - (now - 10_000, "rg"), // outside a 1h-ish cutoff below - ] { - conn.execute( - "INSERT INTO bash_commands (ts, head) VALUES (?1, ?2)", - rusqlite::params![ts, head], - ) - .unwrap(); - } - let heads = read_bash_heads(&conn, now - 3600); - assert_eq!(heads.get("cargo").copied(), Some(2)); - assert_eq!(heads.get("git").copied(), Some(1)); - assert_eq!(heads.get("rg").copied(), None); // window cutoff excludes it - } - - /// A missing `bash_commands` table degrades to an empty tally rather - /// than erroring — the pre-capture window on a fresh agent. - #[test] - fn bash_heads_missing_table_is_empty() { - let conn = Connection::open_in_memory().unwrap(); - assert!(read_bash_heads(&conn, 0).is_empty()); - } - - /// `Skill:` entries are unpacked and the marker stripped; a - /// non-skill tool entry in the same blob is ignored. - #[test] - fn skills_from_breakdown_strips_marker_and_ignores_other_tools() { - let json = r#"{"Skill:base:async-task-hygiene":3,"Read":7}"#; - let mut got = skills_from_breakdown(json); - got.sort(); - assert_eq!(got, vec![("base:async-task-hygiene".to_owned(), 3)]); - } - - /// Malformed JSON degrades to empty rather than erroring — one bad row - /// shouldn't drop a whole agent from the rollup. - #[test] - fn skills_from_breakdown_malformed_json_is_empty() { - assert!(skills_from_breakdown("not json").is_empty()); - } - - /// No `Skill:*` keys at all (an agent that hasn't triggered one yet) - /// is also empty, not an error. - #[test] - fn skills_from_breakdown_no_skills_is_empty() { - assert!(skills_from_breakdown(r#"{"Read":1,"Edit":2}"#).is_empty()); - } -} diff --git a/hive-c0re/src/stats/mod.rs b/hive-c0re/src/stats/mod.rs index cfa36917..de9e8e16 100644 --- a/hive-c0re/src/stats/mod.rs +++ b/hive-c0re/src/stats/mod.rs @@ -1,10 +1,9 @@ -//! Metrics aggregation for the dashboard: hive-wide turn-stats -//! rollups, host-system probes / server warnings, and live -//! per-container cgroup load. Each submodule is re-exported at the -//! crate root, so `crate::hive_stats::…` etc. keep working unchanged. +//! Metrics aggregation for the dashboard: host-system probes / server +//! warnings, and live per-container cgroup load. Each submodule is +//! re-exported at the crate root, so `crate::host_stats::…` etc. keep +//! working unchanged. pub mod container_stats; -pub mod hive_stats; pub mod host_stats; pub mod otel_metrics; pub mod sweep_health; diff --git a/nix/host-modules/deploy.nix b/nix/host-modules/deploy.nix index 250eb1e6..ca05b8e4 100644 --- a/nix/host-modules/deploy.nix +++ b/nix/host-modules/deploy.nix @@ -242,10 +242,6 @@ in "c0re" "preBuildAgentTemplates" ] agentPrebuildRemoved) - (lib.mkRenamedOptionModule - [ "services" "hyperhive" "c0re" "modelPrices" ] - [ "services" "hyperhive" "deploy" "hive-controller" "modelPrices" ] - ) (lib.mkRenamedOptionModule [ "services" "hyperhive" "c0re" "agentCpuQuota" ] [ "services" "hyperhive" "deploy" "hive-controller" "agentCpuQuota" ] diff --git a/nix/host-modules/hive-c0re/default.nix b/nix/host-modules/hive-c0re/default.nix index 822bd5d3..9cf6b92c 100644 --- a/nix/host-modules/hive-c0re/default.nix +++ b/nix/host-modules/hive-c0re/default.nix @@ -61,8 +61,7 @@ let # The `hive-c0re serve` config JSON. Keys are snake_case to match the # `ServeConfig` serde shape the daemon deserialises (the # container-injected HiveEnv fields, flattened, plus the hive-c0re-local - # model_prices table); per-flag overrides still work for ad-hoc - # invocations. + # build_slots); per-flag overrides still work for ad-hoc invocations. # # Written to `/etc/hyperhive/serve.json` (managed by # `environment.etc`) rather than embedded as a store-path argument in @@ -97,7 +96,6 @@ let agent_memory_max = controllerCfg.agentMemoryMax; agent_cpu_weight = controllerCfg.agentCpuWeight; agent_io_weight = controllerCfg.agentIoWeight; - model_prices = controllerCfg.modelPrices; build_slots = controllerCfg.buildSlots; }; in diff --git a/nix/host-modules/hive-c0re/options.nix b/nix/host-modules/hive-c0re/options.nix index b382b85e..42ab0939 100644 --- a/nix/host-modules/hive-c0re/options.nix +++ b/nix/host-modules/hive-c0re/options.nix @@ -11,6 +11,10 @@ ... }: let + modelPricesRemoved = '' + The hive dashboard no longer has a stats page, and the price table only fed + its cost estimate. Remove this definition. + ''; agentPrebuildRemoved = '' Hives pull agent closures from the swarm cache; nothing builds locally, so there is no agent-base closure to pre-fetch into the host store. Remove @@ -36,6 +40,14 @@ in "hive-controller" "preBuildAgentTemplates" ] agentPrebuildRemoved) + (lib.mkRemovedOptionModule [ "services" "hyperhive" "c0re" "modelPrices" ] modelPricesRemoved) + (lib.mkRemovedOptionModule [ + "services" + "hyperhive" + "deploy" + "hive-controller" + "modelPrices" + ] modelPricesRemoved) (lib.mkRemovedOptionModule [ "services" "hyperhive" "c0re" "hyperhiveDocs" ] '' Agents take their reference docs from the pinned `hyperhive` input, which carries `docs/`. Remove this definition. @@ -121,80 +133,6 @@ in ''; }; - modelPrices = lib.mkOption { - type = lib.types.attrsOf ( - lib.types.submodule { - options = { - input = lib.mkOption { - type = lib.types.numbers.nonnegative; - description = "USD per million input tokens."; - }; - output = lib.mkOption { - type = lib.types.numbers.nonnegative; - description = "USD per million output tokens."; - }; - cache_read = lib.mkOption { - type = lib.types.numbers.nonnegative; - description = "USD per million cache-read tokens."; - }; - cache_write = lib.mkOption { - type = lib.types.numbers.nonnegative; - description = "USD per million cache-creation (write) tokens."; - }; - }; - } - ); - # Current Anthropic list prices for the Claude 4.x family (Opus - # 4.x, Sonnet 4.x, Haiku 4.5); cache_write is the 1-hour cache-TTL - # price (the default through the Claude subscription the agents run - # on). Keep in sync with `builtin_prices` in - # hive-c0re/src/stats/hive_stats.rs. - default = { - opus = { - input = 5.0; - output = 25.0; - cache_read = 0.5; - cache_write = 10.0; - }; - sonnet = { - input = 3.0; - output = 15.0; - cache_read = 0.3; - cache_write = 6.0; - }; - haiku = { - input = 1.0; - output = 5.0; - cache_read = 0.1; - cache_write = 2.0; - }; - }; - example = { - sonnet = { - input = 3.0; - output = 15.0; - cache_read = 0.3; - cache_write = 6.0; - }; - }; - description = '' - Per-model USD prices (per **million** tokens) used for the - hive-wide cost *estimate* on the dashboard's ST4TS tab. Each key - is a model-family short name matched case-insensitively as a - substring of the active model id at runtime (e.g. `"sonnet"` - matches `"claude-sonnet-4-5"`); the longest matching key wins, so - a specific entry beats a generic family name. Any model not - covered by this table falls back to hive-c0re's built-in - estimate. - - The defaults track Anthropic list pricing at the time of - writing — override them here to keep the estimate current - without a code change. Passed to `hive-c0re serve` as JSON via - `--model-prices`; read only by hive-c0re itself (not injected - into containers). Changes apply on the next host rebuild. - ''; - }; - agentCpuQuota = lib.mkOption { type = lib.types.str; default = "200%"; diff --git a/nix/module-eval/core-toggle.nix b/nix/module-eval/core-toggle.nix index 39a9fa60..2a68b560 100644 --- a/nix/module-eval/core-toggle.nix +++ b/nix/module-eval/core-toggle.nix @@ -1470,12 +1470,6 @@ let c0re.assets = pkgs.gnused; c0re.xdgIcons = pkgs.gnugrep; c0re.hyperhiveRev = "abc123"; - c0re.modelPrices.sonnet = { - input = 1.0; - output = 2.0; - cache_read = 3.0; - cache_write = 4.0; - }; c0re.agentCpuQuota = "300%"; c0re.agentMemoryMax = "8G"; c0re.agentCpuWeight = 50; @@ -1501,7 +1495,6 @@ let && path c.xdgIcons == path pkgs.gnugrep && path c.servedFrontend == path pkgs.coreutils && c.hyperhiveRev == "abc123" - && c.modelPrices.sonnet.cache_write == 4.0 && c.agentCpuQuota == "300%" && c.agentMemoryMax == "8G" && c.agentCpuWeight == 50 @@ -1516,7 +1509,6 @@ let "assets" "xdgIcons" "hyperhiveRev" - "modelPrices" "agentCpuQuota" "agentMemoryMax" "agentCpuWeight" @@ -1575,12 +1567,37 @@ let && !(lib.any (path: refused bare path) (lib.attrNames setting)) && !(lib.any (path: refused sibling path) (lib.attrNames setting)); } + { + # The price table fed only the hive dashboard's stats page, which is gone. + # A definition under either namespace fails evaluation naming the option; + # a host that sets none, or a moved sibling, does not. + name = "a definition of the removed modelPrices option fails evaluation, naming the option"; + ok = + let + refused = + m: path: + lib.any ( + a: !a.assertion && lib.hasInfix "services.hyperhive.${path}.modelPrices" a.message + ) m.assertions; + prices = { + sonnet = { + input = 3.0; + }; + }; + in + refused (hiveWithController { c0re.modelPrices = prices; }) "c0re" + && refused (hiveWithController { + deploy.hive-controller.modelPrices = prices; + }) "deploy.hive-controller" + && !(refused bare "c0re") + && !(refused bare "deploy.hive-controller") + && !(refused (hiveWithController { c0re.buildSlots = 3; }) "c0re"); + } { # What the moved options render with their defaults: the flake source, the agent limits - # and build slots in serve.json, the sudoless-hivectl group's members, - # and the model price table. The same values the options held at their - # old paths. - name = "the flake source, agent limits, build slots, admin group and price table render their defaults unchanged"; + # and build slots in serve.json, and the sudoless-hivectl group's + # members. The same values the options held at their old paths. + name = "the flake source, agent limits, build slots and admin group render their defaults unchanged"; ok = let serve = builtins.fromJSON ( @@ -1597,8 +1614,6 @@ let && serve.agent_cpu_weight == 80 && serve.agent_io_weight == 80 && serve.build_slots == 1 - && serve.model_prices.opus.input == 5.0 - && serve.model_prices.haiku.cache_write == 2.0 && allLocal.users.groups.hive-admin.members == [ ]; } { diff --git a/nix/packages/frontend.nix b/nix/packages/frontend.nix index 66627fc2..25022271 100644 --- a/nix/packages/frontend.nix +++ b/nix/packages/frontend.nix @@ -16,9 +16,9 @@ # $out/dashboard/ the hive-c0re dashboard assets (full layout in # frontend/packages/dashboard/build.mjs): # index.html (operator SPA, served at /) -# stats.html favicon.svg -# static/{tabs,stats,stream-worker}.js{,.map} -# static/{colors,theme,common,dashboard,stats}.css +# favicon.svg +# static/{tabs,stream-worker}.js{,.map} +# static/{colors,theme,common,dashboard}.css # $out/agent/ the per-agent default UI (layered with # services.hyperhive.agent.frontend.extraFiles at activation time) # index.html stats.html screen.html