From 09cb705738358764960b2b1ce6240caec87ed8cf Mon Sep 17 00:00:00 2001 From: iris Date: Mon, 8 Jun 2026 23:37:53 +0200 Subject: [PATCH] refactor(frontend): move ST4TS to its own /stats.html page (#1464 step 2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- frontend/packages/dashboard/build.mjs | 6 +- frontend/packages/dashboard/src/common.css | 28 +++ frontend/packages/dashboard/src/dashboard.css | 95 +--------- .../packages/dashboard/src/dashboard.html | 45 +---- frontend/packages/dashboard/src/index.html | 7 +- frontend/packages/dashboard/src/stats.css | 84 +++++++++ frontend/packages/dashboard/src/stats.html | 53 ++++++ frontend/packages/dashboard/src/stats.js | 167 ++++++++++++++++ frontend/packages/dashboard/src/tabs.js | 178 +----------------- nix/frontend.nix | 6 +- 10 files changed, 367 insertions(+), 302 deletions(-) create mode 100644 frontend/packages/dashboard/src/stats.css create mode 100644 frontend/packages/dashboard/src/stats.html create mode 100644 frontend/packages/dashboard/src/stats.js diff --git a/frontend/packages/dashboard/build.mjs b/frontend/packages/dashboard/build.mjs index 63c6f6b5..b0d79747 100644 --- a/frontend/packages/dashboard/build.mjs +++ b/frontend/packages/dashboard/build.mjs @@ -52,7 +52,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('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(''), bundle: true, format: 'esm', @@ -91,7 +91,7 @@ await build({ // so a swap replaces only it) + theme.css (the semantic derivation // layer) + common.css (shared typography, badges, buttons, inbox, side // 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({ entryPoints: [src(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)); } diff --git a/frontend/packages/dashboard/src/common.css b/frontend/packages/dashboard/src/common.css index 22c6fb37..b377eb58 100644 --- a/frontend/packages/dashboard/src/common.css +++ b/frontend/packages/dashboard/src/common.css @@ -558,3 +558,31 @@ body.home-shell #server-warnings { background: color-mix(in srgb, var(--red) 18%, transparent); 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; +} diff --git a/frontend/packages/dashboard/src/dashboard.css b/frontend/packages/dashboard/src/dashboard.css index 3b1f0229..5cc492ea 100644 --- a/frontend/packages/dashboard/src/dashboard.css +++ b/frontend/packages/dashboard/src/dashboard.css @@ -1392,97 +1392,14 @@ body.dashboard-shell.has-selection { padding-bottom: 4.5em; } cursor: default; } -/* ─── ST4TS tab: hive-wide turn-stats rollup ────────────────────────────── - Plain tables + CSS bars (no chart lib in the dashboard bundle). */ -.hive-stats-windows { - display: flex; - 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; -} +/* 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 (the SYST3M › + C0NT41N3R L04D table below still uses it). */ /* ─── SYST3M › C0NT41N3R L04D: live cgroup cpu/mem ───────────────────────────── - Reuses the `.hive-stats-table` styling from the ST4TS tab; only the - inline meter bar is new. */ + Reuses the shared `.hive-stats-table` styling (now in common.css); + only the inline meter bar is new. */ .cload-meter { display: inline-block; width: 6em; diff --git a/frontend/packages/dashboard/src/dashboard.html b/frontend/packages/dashboard/src/dashboard.html index e1f20160..07ef0a9b 100644 --- a/frontend/packages/dashboard/src/dashboard.html +++ b/frontend/packages/dashboard/src/dashboard.html @@ -60,14 +60,9 @@ - - - ◆ ST4TS ◆ - + @@ -241,35 +236,11 @@ - - + +
+ ← home + 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 new file mode 100644 index 00000000..cf8eb18b --- /dev/null +++ b/frontend/packages/dashboard/src/stats.js @@ -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 = '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 { + 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(); }, +}); diff --git a/frontend/packages/dashboard/src/tabs.js b/frontend/packages/dashboard/src/tabs.js index 333268cf..73f5a9e1 100644 --- a/frontend/packages/dashboard/src/tabs.js +++ b/frontend/packages/dashboard/src/tabs.js @@ -4055,11 +4055,11 @@ window.marked = marked; })(); // ─── tab routing ─────────────────────────────────────────────────────── - // Hash-based: `#swarm` / `#call` / `#system` / `#schedules` / - // `#permissions` / `#stats` activate the matching pane on the - // dashboard. Empty hash defaults to SW4RM. FL0W and S3TT1NGS are NOT - // tabs — they're separate pages (`/flow.html`, `/settings.html`) - // reached from the H0M3 hub. Tab routing only + // Hash-based: `#swarm` / `#call` / `#system` / `#permissions` / + // `#schedules` activate the matching pane on the dashboard. Empty + // hash defaults to SW4RM. FL0W, S3TT1NGS, and ST4TS are NOT tabs — + // they're separate pages (`/flow.html`, `/settings.html`, + // `/stats.html`) reached from the H0M3 hub. Tab routing only // 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). // The shared hash-routed tab strip (@hive/shared/tabs.js) owns the @@ -4085,176 +4085,16 @@ window.marked = marked; fetchAndRenderCapabilities(); fetchAndRenderToolGroups(); } - // ST4TS: hive-wide rollup is a pull (no SSE) — fetch on activation. - if (target === 'stats') { refreshHiveStats(); } if (target === 'call') { refreshOperatorInbox(); } // SYST3M › C0NT41N3R L04D: live cgroup poll only while the tab is // open (cpu needs a short two-sample read each refresh). if (target === 'system') { startContainerLoadPolling(); } else { stopContainerLoadPolling(); } } - // ─── ST4TS: hive-wide turn-stats rollup ────────────────────────────────── - // Pull-only (no SSE): fetched from /api/stats-hive on tab activation and - // on window change. Plain tables/bars — the dashboard bundle has no chart - // lib, and per-agent trend charts live on each agent's own /stats page. - 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 = '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 { - 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(); + // ST4TS (hive-wide turn-stats rollup) moved to its own page, + // `/stats.html` — the render JS + the window selector live in + // stats.js now. The dashboard no longer fetches + // /api/stats-hive. // ─── SYST3M › C0NT41N3R L04D: live per-container cgroup cpu/mem ──────────── // Pull-only, polled at 5s ONLY while the SYST3M tab is active (cpu is a diff --git a/nix/frontend.nix b/nix/frontend.nix index dcaf687a..87d43200 100644 --- a/nix/frontend.nix +++ b/nix/frontend.nix @@ -17,9 +17,9 @@ # frontend/packages/dashboard/build.mjs): # index.html (H0M3 hub, served at /) dashboard.html (operator SPA, # served at /dashboard.html) flow.html logs.html settings.html -# favicon.svg -# static/{home,tabs,flow,logs,settings,stream-worker}.js{,.map} -# static/{colors,theme,common,home,dashboard,flow,logs,settings}.css +# stats.html favicon.svg +# static/{home,tabs,flow,logs,settings,stats,stream-worker}.js{,.map} +# static/{colors,theme,common,home,dashboard,flow,logs,settings,stats}.css # $out/agent/ the per-agent default UI (layered with # hyperhive.frontend.extraFiles at activation time) # index.html stats.html screen.html