refactor(frontend): move ST4TS to its own /stats.html page (#1464 step 2)
Extracts the hive-wide turn-stats rollup out of the dashboard tab strip
into a standalone /stats.html page, reached from the H0M3 hub — same
minimal-chrome pattern as /flow.html and /logs.html. The dashboard tab
strip is now purely operational.
- new stats.{html,css,js}; stats.js holds the moved render JS and
fetches /api/stats-hive on load + window change.
- migrate the window selector (#hive-stats-windows) from a bespoke
data-w/.active toggle to the shared createTabStrip — now hash-routed
(#1h / #24h / …) and deep-linkable, matching the per-agent /stats page.
- drop the ST4TS tab + pane from dashboard.html and the hive-stats render
block + the stats->refreshHiveStats lazy-load from tabs.js.
- move the shared .hive-stats-table to common.css (the dashboard SYST3M >
C0NT41N3R L04D table still uses it); the ST4TS-only window/chip/bar
styles go to stats.css.
- add a Stats tile to the H0M3 hub (and drop the now-stale "stats" from
the Dashboard tile desc); wire build.mjs + the nix/frontend.nix manifest.
Second slice of #1464 step 2; follows the /settings.html extraction.
This commit is contained in:
parent
fdf05c1673
commit
09cb705738
10 changed files with 367 additions and 302 deletions
|
|
@ -52,7 +52,7 @@ mkdirSync(staticDir(''), { recursive: true });
|
||||||
// follow-up once asset sizes warrant it). esbuild writes each entry
|
// follow-up once asset sizes warrant it). esbuild writes each entry
|
||||||
// to `static/<name>.js` based on the entryPoint basename.
|
// to `static/<name>.js` based on the entryPoint basename.
|
||||||
await build({
|
await build({
|
||||||
entryPoints: [src('tabs.js'), src('flow.js'), src('logs.js'), src('home.js'), src('settings.js')],
|
entryPoints: [src('tabs.js'), src('flow.js'), src('logs.js'), src('home.js'), src('settings.js'), src('stats.js')],
|
||||||
outdir: staticDir(''),
|
outdir: staticDir(''),
|
||||||
bundle: true,
|
bundle: true,
|
||||||
format: 'esm',
|
format: 'esm',
|
||||||
|
|
@ -91,7 +91,7 @@ await build({
|
||||||
// so a swap replaces only it) + theme.css (the semantic derivation
|
// so a swap replaces only it) + theme.css (the semantic derivation
|
||||||
// layer) + common.css (shared typography, badges, buttons, inbox, side
|
// layer) + common.css (shared typography, badges, buttons, inbox, side
|
||||||
// panel) plus its own page-specific bundle.
|
// panel) plus its own page-specific bundle.
|
||||||
for (const entry of ['colors.css', 'theme.css', 'common.css', 'dashboard.css', 'flow.css', 'logs.css', 'home.css', 'settings.css']) {
|
for (const entry of ['colors.css', 'theme.css', 'common.css', 'dashboard.css', 'flow.css', 'logs.css', 'home.css', 'settings.css', 'stats.css']) {
|
||||||
await build({
|
await build({
|
||||||
entryPoints: [src(entry)],
|
entryPoints: [src(entry)],
|
||||||
outfile: staticDir(entry),
|
outfile: staticDir(entry),
|
||||||
|
|
@ -101,7 +101,7 @@ for (const entry of ['colors.css', 'theme.css', 'common.css', 'dashboard.css', '
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const html of ['index.html', 'dashboard.html', 'flow.html', 'logs.html', 'settings.html']) {
|
for (const html of ['index.html', 'dashboard.html', 'flow.html', 'logs.html', 'settings.html', 'stats.html']) {
|
||||||
copyFileSync(src(html), dist(html));
|
copyFileSync(src(html), dist(html));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -558,3 +558,31 @@ body.home-shell #server-warnings {
|
||||||
background: color-mix(in srgb, var(--red) 18%, transparent);
|
background: color-mix(in srgb, var(--red) 18%, transparent);
|
||||||
border-bottom: 1px solid var(--red);
|
border-bottom: 1px solid var(--red);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ─── stats table ─────────────────────────────────────────────────
|
||||||
|
Shared right-aligned numeric table. Used by /stats.html (busiest
|
||||||
|
agents) and the dashboard SYST3M › C0NT41N3R L04D table — 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;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1392,97 +1392,14 @@ body.dashboard-shell.has-selection { padding-bottom: 4.5em; }
|
||||||
cursor: default;
|
cursor: default;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ─── ST4TS tab: hive-wide turn-stats rollup ──────────────────────────────
|
/* ST4TS moved to its own page (`/stats.html`): the
|
||||||
Plain tables + CSS bars (no chart lib in the dashboard bundle). */
|
window selector / summary chips / bars live in stats.css, and the
|
||||||
.hive-stats-windows {
|
shared `.hive-stats-table` moved to common.css (the SYST3M ›
|
||||||
display: flex;
|
C0NT41N3R L04D table below still uses it). */
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 6px;
|
|
||||||
margin: 12px 0;
|
|
||||||
}
|
|
||||||
.hive-stats-windows .btn.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-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;
|
|
||||||
}
|
|
||||||
.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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ─── SYST3M › C0NT41N3R L04D: live cgroup cpu/mem ─────────────────────────────
|
/* ─── SYST3M › C0NT41N3R L04D: live cgroup cpu/mem ─────────────────────────────
|
||||||
Reuses the `.hive-stats-table` styling from the ST4TS tab; only the
|
Reuses the shared `.hive-stats-table` styling (now in common.css);
|
||||||
inline meter bar is new. */
|
only the inline meter bar is new. */
|
||||||
.cload-meter {
|
.cload-meter {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: 6em;
|
width: 6em;
|
||||||
|
|
|
||||||
|
|
@ -60,14 +60,9 @@
|
||||||
<span class="tab-count" id="tab-count-schedules" hidden></span>
|
<span class="tab-count" id="tab-count-schedules" hidden></span>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<!-- ST4TS: hive-wide turn-stats rollup (swarm totals, busiest
|
<!-- ST4TS lives on its own page now (`/stats.html`), reached from
|
||||||
agents, model mix, est cost). Fetched on tab activation +
|
the H0M3 hub — the hive-wide rollup is a standalone read-only
|
||||||
window change from GET /api/stats-hive. -->
|
view, not part of the operational tab strip. -->
|
||||||
<a class="tab" id="tab-stats" href="#stats" role="tab"
|
|
||||||
aria-controls="tab-pane-stats"
|
|
||||||
data-tab="stats">
|
|
||||||
<span class="tab-label">◆ ST4TS ◆</span>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<!-- (Peer hives are no longer a tab — they render as a headline
|
<!-- (Peer hives are no longer a tab — they render as a headline
|
||||||
under SW4RM. See tab-pane-swarm.) -->
|
under SW4RM. See tab-pane-swarm.) -->
|
||||||
|
|
@ -241,35 +236,11 @@
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- ST4TS: hive-wide turn-stats aggregate. Plain tables/bars (the
|
<!-- ST4TS: hive-wide turn-stats aggregate lives on its own page now
|
||||||
dashboard bundle has no chart lib); data from GET
|
(`/stats.html`), reached from the H0M3 hub. The markup + the
|
||||||
/api/stats-hive?window=, fetched on tab activation + on
|
hive-stats render JS moved there; when tabs.js boots on the
|
||||||
window change. Per-agent trend charts stay on each agent's
|
dashboard the renderers are simply gone (no stats tab to
|
||||||
own /stats page. -->
|
activate). -->
|
||||||
<section class="tab-pane" id="tab-pane-stats" data-tab-pane="stats" hidden
|
|
||||||
role="tabpanel" aria-labelledby="tab-stats">
|
|
||||||
<h2>◆ ST4TS ◆</h2>
|
|
||||||
<div class="divider">══════════════════════════════════════════════════════════════</div>
|
|
||||||
<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-windows" id="hive-stats-windows">
|
|
||||||
<button type="button" class="btn" data-w="1h">1h</button>
|
|
||||||
<button type="button" class="btn" data-w="4h">4h</button>
|
|
||||||
<button type="button" class="btn active" data-w="24h">24h</button>
|
|
||||||
<button type="button" class="btn" data-w="3d">3d</button>
|
|
||||||
<button type="button" class="btn" data-w="7d">7d</button>
|
|
||||||
<button type="button" class="btn" data-w="30d">30d</button>
|
|
||||||
</div>
|
|
||||||
<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-mcp 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>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- FL0W: lives on its own page now (`/flow.html`). The
|
<!-- FL0W: lives on its own page now (`/flow.html`). The
|
||||||
message-flow + inbox + compose DOM only exists there — when
|
message-flow + inbox + compose DOM only exists there — when
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@
|
||||||
|
|
||||||
<a class="home-tile" href="/dashboard.html">
|
<a class="home-tile" href="/dashboard.html">
|
||||||
<span class="home-tile-label">Dashboard</span>
|
<span class="home-tile-label">Dashboard</span>
|
||||||
<span class="home-tile-desc">containers · approvals · system · stats</span>
|
<span class="home-tile-desc">containers · approvals · permissions · schedules · system</span>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a class="home-tile" href="/flow.html">
|
<a class="home-tile" href="/flow.html">
|
||||||
|
|
@ -39,6 +39,11 @@
|
||||||
<span class="home-tile-desc">build · agent · system logs</span>
|
<span class="home-tile-desc">build · agent · system logs</span>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
<a class="home-tile" href="/stats.html">
|
||||||
|
<span class="home-tile-label">Stats</span>
|
||||||
|
<span class="home-tile-desc">hive-wide turn stats · cost · model mix</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
<a class="home-tile" href="/settings.html">
|
<a class="home-tile" href="/settings.html">
|
||||||
<span class="home-tile-label">Settings</span>
|
<span class="home-tile-label">Settings</span>
|
||||||
<span class="home-tile-desc">operator-local prefs · browser notifications</span>
|
<span class="home-tile-desc">operator-local prefs · browser notifications</span>
|
||||||
|
|
|
||||||
84
frontend/packages/dashboard/src/stats.css
Normal file
84
frontend/packages/dashboard/src/stats.css
Normal file
|
|
@ -0,0 +1,84 @@
|
||||||
|
/* ─── /stats.html — hive-wide turn-stats rollup ────────────────────
|
||||||
|
Extracted from the dashboard ST4TS tab. Same minimal-
|
||||||
|
chrome pattern as /flow.html and /logs.html: the back-link header +
|
||||||
|
title (.logs-header / .logs-back / .logs-title) live in common.css.
|
||||||
|
`.hive-stats-table` also lives in common.css — it's shared with the
|
||||||
|
dashboard SYST3M › C0NT41N3R L04D table. 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. Buttons keep the shared `.btn` pill look; the active
|
||||||
|
one is flagged by createTabStrip via `.hive-tab--active` (was a bespoke
|
||||||
|
`.active` toggle). */
|
||||||
|
.hive-stats-windows {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px;
|
||||||
|
margin: 12px 0;
|
||||||
|
}
|
||||||
|
.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;
|
||||||
|
}
|
||||||
53
frontend/packages/dashboard/src/stats.html
Normal file
53
frontend/packages/dashboard/src/stats.html
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
<!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. Same pattern as flow.html /
|
||||||
|
logs.html — no full dashboard tabbar. Back link points to the
|
||||||
|
H0M3 hub (served at /), not the dashboard. -->
|
||||||
|
<header class="logs-header">
|
||||||
|
<a class="logs-back" href="/">← home</a>
|
||||||
|
<span class="logs-title">ST4TS</span>
|
||||||
|
</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>
|
||||||
|
|
||||||
|
<!-- Window selector — a hash-routed createTabStrip (#1h / #24h / …),
|
||||||
|
like the per-agent /stats page. Buttons keep the `.btn` pill
|
||||||
|
look; createTabStrip toggles `.hive-tab--active` and the page
|
||||||
|
re-fetches on change via onShow. -->
|
||||||
|
<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>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<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-mcp 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>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<script type="module" src="/static/stats.js" defer></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
167
frontend/packages/dashboard/src/stats.js
Normal file
167
frontend/packages/dashboard/src/stats.js
Normal file
|
|
@ -0,0 +1,167 @@
|
||||||
|
// /stats.html — hive-wide turn-stats rollup.
|
||||||
|
//
|
||||||
|
// Extracted from the dashboard ST4TS tab. Pull-only (no SSE): fetched
|
||||||
|
// from /api/stats-hive on load and on window change. Plain tables/bars —
|
||||||
|
// this bundle has no chart lib; per-agent trend charts live on each
|
||||||
|
// agent's own /stats page. The window selector is a hash-routed
|
||||||
|
// createTabStrip (#1h / #24h / …), matching the per-agent stats page.
|
||||||
|
import { $, initServerWarnings } from './common.js';
|
||||||
|
import { createTabStrip } from '@hive/shared/tabs.js';
|
||||||
|
|
||||||
|
let hiveStatsWindow = '24h';
|
||||||
|
|
||||||
|
function hsFmtInt(n) {
|
||||||
|
return Number.isFinite(n) ? new Intl.NumberFormat().format(Math.round(n)) : '0';
|
||||||
|
}
|
||||||
|
function hsFmtTokens(n) {
|
||||||
|
if (!Number.isFinite(n)) return '0';
|
||||||
|
if (n >= 1e9) return (n / 1e9).toFixed(2) + 'B';
|
||||||
|
if (n >= 1e6) return (n / 1e6).toFixed(2) + 'M';
|
||||||
|
if (n >= 1e3) return (n / 1e3).toFixed(1) + 'k';
|
||||||
|
return String(Math.round(n));
|
||||||
|
}
|
||||||
|
function hsFmtUsd(n) {
|
||||||
|
if (!Number.isFinite(n)) return '$0';
|
||||||
|
if (n >= 100) return '$' + n.toFixed(0);
|
||||||
|
if (n >= 1) return '$' + n.toFixed(2);
|
||||||
|
return '$' + n.toFixed(3);
|
||||||
|
}
|
||||||
|
function hsChip(parent, label, value, est) {
|
||||||
|
const c = document.createElement('span');
|
||||||
|
c.className = 'hive-stats-chip' + (est ? ' est' : '');
|
||||||
|
const k = document.createElement('span'); k.className = 'k'; k.textContent = label;
|
||||||
|
const v = document.createElement('span'); v.className = 'v'; v.textContent = value;
|
||||||
|
c.append(k, v);
|
||||||
|
parent.append(c);
|
||||||
|
}
|
||||||
|
function hsMeta(parent, text) {
|
||||||
|
parent.replaceChildren();
|
||||||
|
const p = document.createElement('p');
|
||||||
|
p.className = 'meta';
|
||||||
|
p.textContent = text;
|
||||||
|
parent.append(p);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
mm.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);
|
||||||
|
mm.append(row);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// "favorite tools": most-run bash commands across the swarm. Hidden
|
||||||
|
// (header + list) until the capture has recorded data, so the
|
||||||
|
// section never shows an empty block while capture is pre-data.
|
||||||
|
const bh = $('hive-stats-bash');
|
||||||
|
const bhH = $('hive-stats-bash-h');
|
||||||
|
if (bh) {
|
||||||
|
const bmix = s.bash_mix || [];
|
||||||
|
if (!bmix.length) {
|
||||||
|
bh.replaceChildren();
|
||||||
|
bh.hidden = true;
|
||||||
|
if (bhH) bhH.hidden = true;
|
||||||
|
} else {
|
||||||
|
bh.hidden = false;
|
||||||
|
if (bhH) bhH.hidden = false;
|
||||||
|
bh.replaceChildren();
|
||||||
|
const max = bmix[0].count || 1;
|
||||||
|
for (const kc of bmix) {
|
||||||
|
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);
|
||||||
|
bh.append(row);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshHiveStats() {
|
||||||
|
try {
|
||||||
|
const resp = await fetch('/api/stats-hive?window=' + encodeURIComponent(hiveStatsWindow));
|
||||||
|
if (!resp.ok) throw new Error('http ' + resp.status);
|
||||||
|
renderHiveStats(await resp.json());
|
||||||
|
} catch (e) {
|
||||||
|
const at = $('hive-stats-agents');
|
||||||
|
if (at) hsMeta(at, 'stats fetch failed: ' + e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
initServerWarnings();
|
||||||
|
|
||||||
|
// Window selector → hash-routed createTabStrip (#1h / #24h / …). No panes
|
||||||
|
// here (the selector swaps data, not DOM); paneFor() returns null for each
|
||||||
|
// tab and the strip's `if (pane)` guard handles it. The initial show()
|
||||||
|
// fires onShow once → sets the window + does the first fetch, so no
|
||||||
|
// separate refreshHiveStats() call is needed.
|
||||||
|
createTabStrip(document.getElementById('hive-stats-windows'), {
|
||||||
|
defaultId: hiveStatsWindow,
|
||||||
|
onShow: (w) => { hiveStatsWindow = w; refreshHiveStats(); },
|
||||||
|
});
|
||||||
|
|
@ -4055,11 +4055,11 @@ window.marked = marked;
|
||||||
})();
|
})();
|
||||||
|
|
||||||
// ─── tab routing ───────────────────────────────────────────────────────
|
// ─── tab routing ───────────────────────────────────────────────────────
|
||||||
// Hash-based: `#swarm` / `#call` / `#system` / `#schedules` /
|
// Hash-based: `#swarm` / `#call` / `#system` / `#permissions` /
|
||||||
// `#permissions` / `#stats` activate the matching pane on the
|
// `#schedules` activate the matching pane on the dashboard. Empty
|
||||||
// dashboard. Empty hash defaults to SW4RM. FL0W and S3TT1NGS are NOT
|
// hash defaults to SW4RM. FL0W, S3TT1NGS, and ST4TS are NOT tabs —
|
||||||
// tabs — they're separate pages (`/flow.html`, `/settings.html`)
|
// they're separate pages (`/flow.html`, `/settings.html`,
|
||||||
// reached from the H0M3 hub. Tab routing only
|
// `/stats.html`) reached from the H0M3 hub. Tab routing only
|
||||||
// applies when the tab DOM is present (e.g. not on the flow page
|
// applies when the tab DOM is present (e.g. not on the flow page
|
||||||
// itself, where these elements don't exist and the loop no-ops).
|
// itself, where these elements don't exist and the loop no-ops).
|
||||||
// The shared hash-routed tab strip (@hive/shared/tabs.js) owns the
|
// The shared hash-routed tab strip (@hive/shared/tabs.js) owns the
|
||||||
|
|
@ -4085,176 +4085,16 @@ window.marked = marked;
|
||||||
fetchAndRenderCapabilities();
|
fetchAndRenderCapabilities();
|
||||||
fetchAndRenderToolGroups();
|
fetchAndRenderToolGroups();
|
||||||
}
|
}
|
||||||
// ST4TS: hive-wide rollup is a pull (no SSE) — fetch on activation.
|
|
||||||
if (target === 'stats') { refreshHiveStats(); }
|
|
||||||
if (target === 'call') { refreshOperatorInbox(); }
|
if (target === 'call') { refreshOperatorInbox(); }
|
||||||
// SYST3M › C0NT41N3R L04D: live cgroup poll only while the tab is
|
// SYST3M › C0NT41N3R L04D: live cgroup poll only while the tab is
|
||||||
// open (cpu needs a short two-sample read each refresh).
|
// open (cpu needs a short two-sample read each refresh).
|
||||||
if (target === 'system') { startContainerLoadPolling(); } else { stopContainerLoadPolling(); }
|
if (target === 'system') { startContainerLoadPolling(); } else { stopContainerLoadPolling(); }
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── ST4TS: hive-wide turn-stats rollup ──────────────────────────────────
|
// ST4TS (hive-wide turn-stats rollup) moved to its own page,
|
||||||
// Pull-only (no SSE): fetched from /api/stats-hive on tab activation and
|
// `/stats.html` — the render JS + the window selector live in
|
||||||
// on window change. Plain tables/bars — the dashboard bundle has no chart
|
// stats.js now. The dashboard no longer fetches
|
||||||
// lib, and per-agent trend charts live on each agent's own /stats page.
|
// /api/stats-hive.
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
|
||||||
mm.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);
|
|
||||||
mm.append(row);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// "favorite tools": most-run bash commands across the swarm. Hidden
|
|
||||||
// (header + list) until the capture has recorded data, so the
|
|
||||||
// section never shows an empty block while capture is pre-data.
|
|
||||||
const bh = $('hive-stats-bash');
|
|
||||||
const bhH = $('hive-stats-bash-h');
|
|
||||||
if (bh) {
|
|
||||||
const bmix = s.bash_mix || [];
|
|
||||||
if (!bmix.length) {
|
|
||||||
bh.replaceChildren();
|
|
||||||
bh.hidden = true;
|
|
||||||
if (bhH) bhH.hidden = true;
|
|
||||||
} else {
|
|
||||||
bh.hidden = false;
|
|
||||||
if (bhH) bhH.hidden = false;
|
|
||||||
bh.replaceChildren();
|
|
||||||
const max = bmix[0].count || 1;
|
|
||||||
for (const kc of bmix) {
|
|
||||||
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);
|
|
||||||
bh.append(row);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function bindHiveStatsWindows() {
|
|
||||||
const tabs = $('hive-stats-windows');
|
|
||||||
if (!tabs) return;
|
|
||||||
tabs.addEventListener('click', (ev) => {
|
|
||||||
const btn = ev.target.closest('button[data-w]');
|
|
||||||
if (!btn) return;
|
|
||||||
hiveStatsWindow = btn.dataset.w;
|
|
||||||
for (const b of tabs.querySelectorAll('button')) b.classList.toggle('active', b === btn);
|
|
||||||
refreshHiveStats();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
bindHiveStatsWindows();
|
|
||||||
|
|
||||||
// ─── SYST3M › C0NT41N3R L04D: live per-container cgroup cpu/mem ────────────
|
// ─── SYST3M › C0NT41N3R L04D: live per-container cgroup cpu/mem ────────────
|
||||||
// Pull-only, polled at 5s ONLY while the SYST3M tab is active (cpu is a
|
// Pull-only, polled at 5s ONLY while the SYST3M tab is active (cpu is a
|
||||||
|
|
|
||||||
|
|
@ -17,9 +17,9 @@
|
||||||
# frontend/packages/dashboard/build.mjs):
|
# frontend/packages/dashboard/build.mjs):
|
||||||
# index.html (H0M3 hub, served at /) dashboard.html (operator SPA,
|
# index.html (H0M3 hub, served at /) dashboard.html (operator SPA,
|
||||||
# served at /dashboard.html) flow.html logs.html settings.html
|
# served at /dashboard.html) flow.html logs.html settings.html
|
||||||
# favicon.svg
|
# stats.html favicon.svg
|
||||||
# static/{home,tabs,flow,logs,settings,stream-worker}.js{,.map}
|
# static/{home,tabs,flow,logs,settings,stats,stream-worker}.js{,.map}
|
||||||
# static/{colors,theme,common,home,dashboard,flow,logs,settings}.css
|
# static/{colors,theme,common,home,dashboard,flow,logs,settings,stats}.css
|
||||||
# $out/agent/ the per-agent default UI (layered with
|
# $out/agent/ the per-agent default UI (layered with
|
||||||
# hyperhive.frontend.extraFiles at activation time)
|
# hyperhive.frontend.extraFiles at activation time)
|
||||||
# index.html stats.html screen.html
|
# index.html stats.html screen.html
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue