common.js + flow.js + index.html + flow.html: cookie scrub + doc pointers (#712 batch 13 cont.)

Final dashboard frontend cleanup. Four files at 0 cookies each.

common.js (11 → 0):
- #406 module-split history → drop, present-state intro
- #448 SharedWorker SSE pipe (×3) → docs/web-ui.md SSE multiplexing
  paragraph pointer
- #515 worker-death self-heal (×3) → docs pointer + brief inline
- #453 bfcache argus-nit (×2) → drop framing, keep inline why
- #451 drag-to-resize → docs/web-ui.md::Side panel pointer
- #233 marked autolinks new-tab → drop cookie

flow.js (8 → 0):
- #406 module-split history → drop
- #389 banner-in-footer → drop framing
- #375 stacking context → docs/web-ui.md::Per-agent page Terminal-wrap
- #408 server-side filter, #499 backend allow-list → drop cookies,
  keep inline why
- #448 SharedWorker per-URL keying → docs pointer
- #163 SSE catchup drift → drop cookie, keep inline why

index.html (13 → 0):
- #389 chrome history → drop
- #459 schedules, #607 matrix, #609 nginx-front, #15 → drop cookies,
  keep functional comments + docs/web-ui.md::Tab strip + gateway.md
  pointers for matrix
- #385 dropped C0NTAINERS heading → drop framing
- #460 reminders-moved-here → drop framing
- #444 SchedulesChanged SSE absence → drop cookie, keep inline why
- #443 selection bar → docs/web-ui.md::Selection bar pointer
- #564 fold-create-into-table → drop framing
- #369#issuecomment-3437 → drop attribution
- #406 step 3 bundle rename → drop history

flow.html (3 → 0):
- #389 slug + #362 pill pattern + #406 step 2 bundle → drop cookies,
  preserve functional descriptions

Total this batch (across all 4 files): **35 cookies scrubbed**.
Net effect: the dashboard SPA's HTML + JS comments all point at
docs/web-ui.md for substantive design context now, with inline
comments only retaining present-state operational descriptions.
This commit is contained in:
iris 2026-05-31 15:07:35 +02:00 committed by mara
commit f7e38c0b42
4 changed files with 123 additions and 182 deletions

View file

@ -1,13 +1,7 @@
// Shared dashboard helpers — extracted from the original monolithic // Shared dashboard helpers used by both index.html (./tabs.js) and
// dashboard JS as step 1 of the #406 split. These bits are used by // flow.html (./flow.js): pure DOM helpers, the side-panel singleton,
// both the tab dashboard (index.html) and the flow page (flow.html): // the OS-notification module, and the path-link / file-preview
// pure DOM helpers, the side-panel singleton, the OS-notification // infrastructure for the side panel.
// module, and the path-link / file-preview infrastructure for the
// side panel.
//
// Each page now has its own entry point — `./tabs.js` for index.html,
// `./flow.js` for flow.html — and both import from here directly
// (#406 steps 2 + 3 complete; #406 closed).
import { linkify as termLinkify } from '@hive/shared/terminal.js'; import { linkify as termLinkify } from '@hive/shared/terminal.js';
@ -57,31 +51,23 @@ export const form = (action, btnClass, btnLabel, confirmMsg, extra = {}, opts =
// tied to its caller, so they don't generalise cleanly. We can lift // tied to its caller, so they don't generalise cleanly. We can lift
// them when a second consumer needs the same shape. // them when a second consumer needs the same shape.
// ─── shared-worker SSE pipe (#448) ────────────────────────────────────── // ─── shared-worker SSE pipe ─────────────────────────────────────────────
// Returns an EventSource-shaped object backed by a SharedWorker that // Returns an EventSource-shaped facade backed by a SharedWorker that
// holds ONE upstream `new EventSource(url)` and fans events out to // holds one upstream `new EventSource(url)` and fans events out to
// every connected tab. Replaces direct `new EventSource(url)` at the // every connected tab. See docs/web-ui.md (SSE multiplexing paragraph)
// dashboard's two consumer sites (tabs.js inline + flow.js via // for the design + Firefox throttling motivation; graceful fallback to
// terminal.js's `streamFactory` option) so N hyperhive tabs share // direct EventSource on environments without SharedWorker.
// ONE backend connection — way under the browser's per-host
// connection cap, immune to per-tab throttling that drops the SSE
// when Firefox suspends background tabs.
// //
// Graceful fallback to direct EventSource on environments without // Consumer API: assign `onmessage` / `onopen` / `onerror`; `.close()`
// SharedWorker (some embedded browsers, some Safari versions). The // drops the subscription (the worker closes the upstream when the last
// per-tab connection cost is the same as today — no regression. // subscriber leaves).
//
// The page consumer uses the returned object like a regular
// EventSource: assign `onmessage` / `onopen` / `onerror`. `.close()`
// tells the worker to drop the subscription; the worker closes the
// upstream EventSource when the last subscriber leaves.
const SHARED_WORKER_PATH = '/static/stream-worker.js'; const SHARED_WORKER_PATH = '/static/stream-worker.js';
const SHARED_WORKER_NAME = 'hyperhive-stream'; const SHARED_WORKER_NAME = 'hyperhive-stream';
// One SharedWorker port per page, reused by all openStream calls on // One SharedWorker port per page, reused by all openStream calls on
// that page. Invalidated on `pagehide` so a bfcache restore picks up // that page. Invalidated on `pagehide` so a bfcache restore picks up
// a fresh port (the cached one may have been collected if all other // a fresh port — the cached port may be dead if all other tabs
// tabs closed while this page was frozen — argus nit on #453). // closed while this page was frozen.
let _sharedPort = null; let _sharedPort = null;
function makeSharedPort() { function makeSharedPort() {
if (typeof SharedWorker === 'undefined') return null; if (typeof SharedWorker === 'undefined') return null;
@ -99,30 +85,11 @@ function getSharedPort() {
return _sharedPort; return _sharedPort;
} }
// #515: detect when the SharedWorker has been killed. Firefox aggressively // SharedWorker death detection: pings from the worker bump the
// reclaims "idle" SharedWorkers under memory pressure (or just on tab // activity clock; a visibility-gated watchdog polls and re-subscribes
// lifecycle quirks we don't fully understand), and there's no native // on a fresh port if the page has been silent past the threshold.
// signal to the client when that happens — `postMessage` on a dead // See docs/web-ui.md (Worker-death self-heal paragraph) for the
// port silently no-ops, and the page just stops receiving events. The // timing rationale + Firefox reclaim symptom.
// observable symptom is "dashboard never refreshes; F5 fixes it" (a
// fresh page creates a fresh worker), which is mara's report on #515.
//
// Pipeline:
// - The worker pings every connected port every WORKER_PING_INTERVAL_MS.
// - `route` (every message arrival, including pings) bumps
// `_lastWorkerActivityAt`.
// - `startWorkerWatchdog` polls every WORKER_WATCHDOG_INTERVAL_MS.
// If the page is visible AND we have active subs AND we haven't
// heard from the worker in > WORKER_DEAD_THRESHOLD_MS, we presume
// the worker is dead, log a warning, and re-subscribe on a fresh
// port.
//
// Numbers picked so a real Firefox tab-suspend / unsuspend cycle (which
// can pause the watchdog itself) doesn't false-positive: 90s without a
// 30s ping means at least three pings missed. Visibility-gated so a
// backgrounded tab — where Firefox throttles setInterval to 1Hz min and
// our healthcheck wouldn't trigger reliably anyway — doesn't try to
// reconnect uselessly.
const WORKER_DEAD_THRESHOLD_MS = 90_000; const WORKER_DEAD_THRESHOLD_MS = 90_000;
const WORKER_WATCHDOG_INTERVAL_MS = 15_000; const WORKER_WATCHDOG_INTERVAL_MS = 15_000;
let _lastWorkerActivityAt = 0; let _lastWorkerActivityAt = 0;
@ -164,25 +131,19 @@ function rebindOnFreshPort() {
noteWorkerActivity(); noteWorkerActivity();
} }
// Registry of live subscriptions on this page. Keyed by url so a // Registry of live subscriptions on this page. Keyed by url; entries
// second openStream call for the same URL (would only happen on a // cache the route function so bfcache-restore re-bind can re-attach
// hypothetical multi-consumer page) attaches to the existing route // it to the fresh port. Today's pages only call openStream once with
// rather than overlapping. Each entry caches the route function so // one URL; the registry shape just keeps the bfcache path correct
// bfcache-restore re-bind can re-attach it to the fresh port. // if that changes.
//
// Today's pages only call openStream once with one URL; the registry
// shape just keeps the bfcache-restore path correct if that changes
// (e.g. /index.html later subscribing to two streams).
const _activeSubs = new Map(); const _activeSubs = new Map();
// One-shot wiring of the page-wide lifecycle hooks: on bfcache // One-shot wiring of page-wide lifecycle hooks. On bfcache freeze
// freeze (`pagehide { persisted: true }`) we unsubscribe so the // we unsubscribe so the worker can close the upstream when the last
// worker can close the upstream when the last live subscriber // live subscriber leaves; on bfcache restore we invalidate the cached
// leaves; on bfcache restore (`pageshow { persisted: true }`) we // port (may be dead after the freeze) and re-attach every active
// invalidate the cached port (it may be dead if all other tabs // subscription to a fresh port. Without this, the consumer's
// closed during the freeze) and re-attach every active subscription // onmessage stays bound but no events flow after restore.
// to a fresh port. argus nit on #453: without this, the consumer's
// onmessage stays bound but no events flow after a bfcache restore.
let _lifecycleBound = false; let _lifecycleBound = false;
function bindLifecycleOnce() { function bindLifecycleOnce() {
if (_lifecycleBound) return; if (_lifecycleBound) return;
@ -245,7 +206,7 @@ export function openStream(url) {
}, },
}; };
const route = (e) => { const route = (e) => {
// #515: any message from the worker is proof of life — note it // Any message from the worker is proof of life — note it
// before the URL filter, since heartbeat pings carry no URL. // before the URL filter, since heartbeat pings carry no URL.
noteWorkerActivity(); noteWorkerActivity();
const m = e.data; const m = e.data;
@ -272,8 +233,8 @@ export function openStream(url) {
_activeSubs.set(url, { target, route }); _activeSubs.set(url, { target, route });
port.addEventListener('message', route); port.addEventListener('message', route);
port.postMessage({ kind: 'subscribe', url }); port.postMessage({ kind: 'subscribe', url });
// #515: seed the activity clock so the watchdog has a baseline; it // Seed the activity clock so the watchdog has a baseline (would
// would otherwise compare against 0 (epoch) and trigger immediately. // otherwise compare against 0 and trigger immediately).
noteWorkerActivity(); noteWorkerActivity();
return target; return target;
} }
@ -330,13 +291,10 @@ export const Panel = (() => {
root.classList.remove('open'); root.classList.remove('open');
root.setAttribute('aria-hidden', 'true'); root.setAttribute('aria-hidden', 'true');
} }
// #451: drag-to-resize the drawer's width. Listens on a thin // Drag-to-resize the drawer's width. See docs/web-ui.md::Side panel
// hit-strip glued to the drawer's left edge; mousedown captures // for the hit-strip + pointer-capture + localStorage persistence
// pointermove + pointerup on the document so the drag continues // model; CSS clamps the stored value to min 320px / max 96vw and
// even if the cursor strays outside the 6px handle band. Width // out-of-range stored values are dropped silently.
// persists to localStorage so it survives page reload. The CSS
// clamps the value (min-width: 320px, max-width: 96vw) — drop
// unparseable / out-of-range stored values silently.
const WIDTH_KEY = 'hyperhive:side-panel-width'; const WIDTH_KEY = 'hyperhive:side-panel-width';
const WIDTH_MIN = 320; const WIDTH_MIN = 320;
function clampWidth(w) { function clampWidth(w) {
@ -476,7 +434,7 @@ function mdNode(text) {
window.marked.setOptions({ breaks: true, gfm: true }); window.marked.setOptions({ breaks: true, gfm: true });
div.innerHTML = window.marked.parse(text); div.innerHTML = window.marked.parse(text);
// marked autolinks URLs but leaves them same-tab — open externally // marked autolinks URLs but leaves them same-tab — open externally
// so a click never navigates away from the dashboard. (issue #233) // so a click never navigates away from the dashboard.
div.querySelectorAll('a[href]').forEach((a) => { div.querySelectorAll('a[href]').forEach((a) => {
a.target = '_blank'; a.target = '_blank';
a.rel = 'noopener noreferrer'; a.rel = 'noopener noreferrer';

View file

@ -8,12 +8,11 @@
</head> </head>
<body class="flow-shell"> <body class="flow-shell">
<!-- Fixed-overlay chrome — just the tab strip (#389 follow-up: <!-- Fixed-overlay chrome — just the tab strip. The full-viewport
slug moved to the dashboard's page footer; the flow page is a terminal has no normal-flow footer position so the dashboard's
full-viewport terminal with no normal-flow footer position, so slug doesn't appear here. The operator can still switch tabs
the slug simply doesn't appear here). The operator can still from the flow page without navigating back; FL0W is the current
switch tabs from the flow page without navigating back; FL0W is page, SW4RM / Y3R C4LL / SYST3M / SCH3DUL3S cross-link to the
the current page, SW4RM / Y3R C4LL / SYST3M cross-link to the
dashboard with the matching hash. --> dashboard with the matching hash. -->
<header class="dashboard-chrome flow-chrome" id="flow-header"> <header class="dashboard-chrome flow-chrome" id="flow-header">
<nav class="tabbar" id="tabbar" role="tablist"> <nav class="tabbar" id="tabbar" role="tablist">
@ -55,7 +54,7 @@
<!-- Operator inbox flyout trigger — count + click → side panel <!-- Operator inbox flyout trigger — count + click → side panel
(singleton, declared below). Hidden until the inbox is non- (singleton, declared below). Hidden until the inbox is non-
empty. Mirrors the agent page's pill pattern (#362). --> empty. Mirrors the agent page's header pill pattern. -->
<button type="button" id="inbox-pill" class="flow-pill" hidden <button type="button" id="inbox-pill" class="flow-pill" hidden
title="open operator inbox"> title="open operator inbox">
<span class="flow-pill-icon" aria-hidden="true">📬</span> <span class="flow-pill-icon" aria-hidden="true">📬</span>
@ -111,10 +110,10 @@
</aside> </aside>
</div> </div>
<!-- Flow-specific bundle (#406 step 2). Contains the broker <!-- Flow-specific bundle. Contains the broker terminal init, the
terminal init, the operator-inbox derived store, the inbox operator-inbox derived store, the inbox pill flyout, and the
pill flyout, and the @-mention composer. Tab renderers etc. @-mention composer. Tab renderers etc. live in
live in `/static/tabs.js` which /flow.html doesn't load. --> `/static/tabs.js` which /flow.html doesn't load. -->
<script type="module" src="/static/flow.js" defer></script> <script type="module" src="/static/flow.js" defer></script>
</body> </body>
</html> </html>

View file

@ -1,11 +1,8 @@
// /flow.html entry point (#406 step 2 — flow-specific split from the // /flow.html entry point. Owns the full-page broker terminal, the
// previous combined entry; #406 step 3 renamed that combined entry // operator-inbox derived store (populated from the broker stream),
// from `app.js` to `tabs.js`). // the inbox pill flyout, and the @-mention compose box. Pulls shared
// // infrastructure (DOM helpers, side panel, OS notifications, path
// Owns the full-page broker terminal, the operator-inbox derived store // linkification) from `./common.js`.
// (populated from the broker stream), the inbox pill flyout, and the
// @-mention compose box. Pulls shared infrastructure (DOM helpers, side
// panel, OS notifications, path linkification) from `./common.js`.
// //
// Does NOT contain the dashboard's tab renderers, mutation-event // Does NOT contain the dashboard's tab renderers, mutation-event
// dispatchers, or refreshState — that's `./tabs.js`, loaded only by // dispatchers, or refreshState — that's `./tabs.js`, loaded only by
@ -106,11 +103,11 @@ import {
if (!flow) return; if (!flow) return;
flow.innerHTML = ''; flow.innerHTML = '';
const tsFmt = (n) => new Date(n * 1000).toISOString().slice(11, 19); const tsFmt = (n) => new Date(n * 1000).toISOString().slice(11, 19);
// Pulse the page banner whenever a broker event lands. (Note: // Pulse the page banner whenever a broker event lands. The
// post-#389 the `.banner` lives in the dashboard's <footer>, not // `.banner` element lives in the dashboard's <footer> rather than
// in the flow page chrome — `pulseBanner` no-ops on /flow.html // in the flow chrome — `pulseBanner` no-ops on /flow.html since
// since there's no element to find. Kept for parity if a future // there's no element to find. Kept for parity if a future chrome
// chrome change reintroduces a banner.) // change reintroduces a banner.
const banner = document.querySelector('.banner'); const banner = document.querySelector('.banner');
let bannerOffTimer = null; let bannerOffTimer = null;
function pulseBanner() { function pulseBanner() {
@ -174,33 +171,28 @@ import {
// Register this row so future replies can reference it. // Register this row so future replies can reference it.
if (ev.id != null && ev.id > 0) msgRowMap.set(ev.id, row); if (ev.id != null && ev.id > 0) msgRowMap.set(ev.id, row);
} }
// Anchor the `↓ N new` pill in `.flow-main` (NOT the default // Anchor the `↓ N new` pill in `.flow-main` rather than the
// `log.parentElement` = `.terminal-wrap`). `.terminal-wrap` // default `.terminal-wrap` parent — see docs/web-ui.md::Per-agent
// applies `backdrop-filter`, which creates a CSS stacking // page (Terminal-wrap) for the backdrop-filter stacking-context
// context — the pill's z-index would otherwise be trapped // gotcha (same shape on the flow page).
// inside and clipped under the fixed composer (issue #375).
// `.flow-main` has no backdrop-filter / stacking-context
// creators, so the pill's z-index reaches the root and floats
// above the composer.
const flowMain = document.querySelector('.flow-main'); const flowMain = document.querySelector('.flow-main');
termCreate({ termCreate({
logEl: flow, logEl: flow,
pillAnchor: flowMain, pillAnchor: flowMain,
historyUrl: '/dashboard/history', historyUrl: '/dashboard/history',
// #408: server-side filter — only the kinds this page actually // Server-side filter — only the kinds this page actually renders
// renders or routes (sent/delivered → broker terminal, // or routes (sent/delivered → broker terminal,
// container_state_changed/_removed → local autocomplete cache). // container_state_changed/_removed → local autocomplete cache).
// Backend (#499) pre-parses the allow-list at subscribe time so // Backend pre-parses the allow-list at subscribe time so the
// the per-frame hot path is one HashSet::contains and the // per-frame hot path is one HashSet::contains and the
// JSON-serialise is skipped entirely on irrelevant kinds. The // JSON-serialise is skipped entirely on irrelevant kinds. The
// dashboard tabs page (tabs.js) keeps the unfiltered subscribe // dashboard tabs page (tabs.js) keeps the unfiltered subscribe
// since it routes every mutation kind into its derived stores. // since it routes every mutation kind into its derived stores.
streamUrl: '/dashboard/stream?kinds=sent,delivered,container_state_changed,container_removed', streamUrl: '/dashboard/stream?kinds=sent,delivered,container_state_changed,container_removed',
// #448: route through the SharedWorker so this page's SSE shares // Route through the SharedWorker — see docs/web-ui.md (SSE
// a single backend connection with /index.html (and any other // multiplexing paragraph). Worker keys on the full URL incl.
// open hyperhive tab). Worker keys on the full URL (incl. // query string, so this filtered subscribe is its own upstream
// query string), so the filtered subscribe is its own upstream // and won't accidentally share with tabs.js's wider subscribe.
// — won't accidentally share with tabs.js's wider subscribe.
streamFactory: openStream, streamFactory: openStream,
renderers: { renderers: {
sent: (ev, api) => renderMsg(ev, api, '→'), sent: (ev, api) => renderMsg(ev, api, '→'),
@ -232,10 +224,9 @@ import {
// Re-sync the local containers cache on every SSE (re)connect. // Re-sync the local containers cache on every SSE (re)connect.
// Live mutation events that fired during a disconnect window // Live mutation events that fired during a disconnect window
// are never replayed, so without this the compose autocomplete // are never replayed, so without this the compose autocomplete
// could drift stale (issue #163). We don't try to recover // could drift stale. We don't try to recover missed broker rows
// missed broker rows here — operator inbox briefly stales on // here — operator inbox briefly stales on reconnect; the
// reconnect; HiveTerminal's history-replay covers the next // history-replay covers the next page load.
// page load.
onStreamOpen: () => { onStreamOpen: () => {
fetch('/api/state').then((r) => r.ok ? r.json() : null).then((s) => { fetch('/api/state').then((r) => r.ok ? r.json() : null).then((s) => {
if (!s || !Array.isArray(s.containers)) return; if (!s || !Array.isArray(s.containers)) return;

View file

@ -8,12 +8,11 @@
</head> </head>
<body class="dashboard-shell"> <body class="dashboard-shell">
<!-- Sticky chrome — just the tab strip now (#389 follow-up: the <!-- Sticky chrome — just the tab strip. The "WE ARE THE WIRED"
"WE ARE THE WIRED" slug moved out of chrome entirely and lives slug lives at the page footer below `<main>`; chrome is
at the page footer below `<main>`; chrome is navigation only). navigation only. Tabs route via the URL hash so F5 / back-
Tabs route via the URL hash so F5 / back-button / shared links button / shared links keep you on the same view. JS owns
keep you on the same view. JS owns the actual show/hide; this the show/hide. -->
is just the menu. -->
<header class="dashboard-chrome"> <header class="dashboard-chrome">
<nav class="tabbar" id="tabbar" role="tablist"> <nav class="tabbar" id="tabbar" role="tablist">
<a class="tab" id="tab-swarm" href="#swarm" role="tab" <a class="tab" id="tab-swarm" href="#swarm" role="tab"
@ -34,10 +33,10 @@
<span class="tab-label">◆ SYST3M ◆</span> <span class="tab-label">◆ SYST3M ◆</span>
<span class="tab-count" id="tab-count-system" hidden></span> <span class="tab-count" id="tab-count-system" hidden></span>
</a> </a>
<!-- SCH3DUL3S (#459): scheduled-prompts surface. List of <!-- SCH3DUL3S: scheduled-prompts surface. List of queued
queued schedules + an operator-direct creation form. schedules + an operator-direct creation form. Count pill
Count pill mirrors the active (non-cancelled) schedule mirrors the active (non-cancelled) schedule count; hidden
count; hidden when zero. --> when zero. -->
<a class="tab" id="tab-schedules" href="#schedules" role="tab" <a class="tab" id="tab-schedules" href="#schedules" role="tab"
aria-controls="tab-pane-schedules" aria-controls="tab-pane-schedules"
data-tab="schedules"> data-tab="schedules">
@ -45,24 +44,25 @@
<span class="tab-count" id="tab-count-schedules" hidden></span> <span class="tab-count" id="tab-count-schedules" hidden></span>
</a> </a>
<!-- M4TR1X (#607): optional matrix web client (fluffychat-web by <!-- M4TR1X: optional matrix web client (fluffychat-web by
default) mounted at /matrix/ by hive-c0re's dashboard router default) mounted at /matrix/ by the gateway when
when `hyperhive.matrix.gui.enable = true`. Same-origin same- `hyperhive.matrix.gui.enable = true`. Same-origin
tab navigation (the static dist is its own SPA). Hidden in JS navigation (the static dist is its own SPA). Hidden in
when `state.matrix_gui_enabled === false` so operators without JS when `state.matrix_gui_enabled === false` so operators
matrix-gui on don't see a dead link (#609 covers the without matrix-gui on don't see a dead link. See
post-#15 nginx-front re-root + .well-known auto-discovery). --> docs/web-ui.md::Tab strip for the gating model and
docs/gateway.md for the matrix vhost + .well-known
auto-discovery. -->
<a class="tab tab-link" id="tab-matrix" href="/matrix/" hidden <a class="tab tab-link" id="tab-matrix" href="/matrix/" hidden
title="open the matrix chat client (fluffychat-web)"> title="open the matrix chat client (fluffychat-web)">
<span class="tab-label">◆ M4TR1X ◆ →</span> <span class="tab-label">◆ M4TR1X ◆ →</span>
</a> </a>
<!-- FL0W is its own page (`/flow.html`), not a tab — per <!-- FL0W is its own page (`/flow.html`), not a tab. The link
operator @ #369#issuecomment-3437 ("yes terminal can be a lives in the tab strip so it reads as a peer surface;
separate page"). The link lives in the tab strip so it clicking navigates rather than swapping panes in place.
reads as a peer surface; clicking navigates rather than Count pill mirrors the dashboard's operator-inbox length
swapping panes in place. Count pill mirrors the dashboard's and is hidden when zero. See docs/web-ui.md::FL0W page. -->
operator-inbox length and is hidden when zero. -->
<a class="tab tab-link" id="tab-flow" href="/flow.html" <a class="tab tab-link" id="tab-flow" href="/flow.html"
title="open the all-agents chat in a dedicated full-page terminal"> title="open the all-agents chat in a dedicated full-page terminal">
<span class="tab-label">◆ FL0W ◆ →</span> <span class="tab-label">◆ FL0W ◆ →</span>
@ -86,9 +86,8 @@
<!-- SW4RM: the swarm itself. Container cards (the central thing <!-- SW4RM: the swarm itself. Container cards (the central thing
the operator looks at) and rebuild queue / cascade visualisation the operator looks at) and rebuild queue / cascade visualisation
that drives them. The tab label itself reads SW4RM, so the that drives them. No inline `C0NTAINERS` h2 heading + divider
inline C0NTAINERS h2 heading + divider would be redundant — — the tab label SW4RM already says it. -->
dropped per #385. -->
<section class="tab-pane" id="tab-pane-swarm" <section class="tab-pane" id="tab-pane-swarm"
role="tabpanel" aria-labelledby="tab-swarm"> role="tabpanel" aria-labelledby="tab-swarm">
<div id="containers-section"> <div id="containers-section">
@ -117,11 +116,8 @@
<!-- SYST3M: passive / rare-interaction state. Meta inputs (lock <!-- SYST3M: passive / rare-interaction state. Meta inputs (lock
bumps), rebuild queue (watch only), kept state from previous bumps), rebuild queue (watch only), kept state from previous
tombstoned agents. Queued reminders moved to the SCH3DUL3S tombstoned agents. Per-section content auto-compresses to a
tab in #460 — they're conceptually "fire X at time Y" too, one-line summary when empty (JS toggle). -->
just self-scheduled by agents instead of operator-set.
Headings stay; the per-section content auto-compresses to a
one-line summary when empty (separate JS toggle). -->
<section class="tab-pane" id="tab-pane-system" <section class="tab-pane" id="tab-pane-system"
role="tabpanel" aria-labelledby="tab-system"> role="tabpanel" aria-labelledby="tab-system">
<h2>◆ M3T4 1NPUTS ◆</h2> <h2>◆ M3T4 1NPUTS ◆</h2>
@ -145,16 +141,15 @@
</div> </div>
</section> </section>
<!-- SCH3DUL3S (#459, #564): scheduled prompts. Creation + edit <!-- SCH3DUL3S: scheduled prompts. Creation + edit are folded
are folded into the same table now (#564) — empty bottom row into the same table — empty bottom row is the create form
is the create form (fill cells, click ), inline-edit-row (fill cells, click ), inline-edit-row expands on the `✎`
expands on the `✎` toggle for existing schedules. Live toggle for existing schedules. Schedules list driven by
schedules list driven by GET /api/schedules; POST /api/schedules GET /api/schedules; POST /api/schedules to create, PATCH
to create, PATCH /api/schedules/{id} to edit, POST /api/schedules/{id} to edit, POST /api/schedules/{id}/cancel
/api/schedules/{id}/cancel for per-target / whole-row cancel. for per-target / whole-row cancel. No SchedulesChanged SSE
#444 backend doesn't emit SchedulesChanged dashboard event yet, event yet, so the list re-fetches on tab activation + after
so the list re-fetches on tab activation + after each submit / each submit / cancel. See docs/web-ui.md::SCH3DUL3S tab. -->
cancel. Live SSE wiring is the future PR C. -->
<section class="tab-pane" id="tab-pane-schedules" <section class="tab-pane" id="tab-pane-schedules"
role="tabpanel" aria-labelledby="tab-schedules"> role="tabpanel" aria-labelledby="tab-schedules">
<h2>◆ SCH3DUL3S ◆</h2> <h2>◆ SCH3DUL3S ◆</h2>
@ -164,12 +159,12 @@
<p class="meta">loading…</p> <p class="meta">loading…</p>
</div> </div>
<!-- QU3U3D R3M1ND3RS (#460): self-scheduled agent reminders. <!-- QU3U3D R3M1ND3RS: self-scheduled agent reminders. Lives
Moved here from the SYST3M tab so the operator has one on this tab so the operator has one place for everything
place for everything that fires at a future time — that fires at a future time — operator-set schedules
operator-set schedules above, agent-self reminders here. above, agent-self reminders here. Backed by GET
Backed by GET /api/reminders; refresh handled by /api/reminders; refresh handled by refreshReminders()
refreshReminders() (called from refreshState). --> (called from refreshState). -->
<h2>◆ QU3U3D R3M1ND3RS ◆</h2> <h2>◆ QU3U3D R3M1ND3RS ◆</h2>
<div class="divider">══════════════════════════════════════════════════════════════</div> <div class="divider">══════════════════════════════════════════════════════════════</div>
<p class="meta">reminders agents have queued for themselves but not yet delivered. cancel to drop a stuck or unwanted entry.</p> <p class="meta">reminders agents have queued for themselves but not yet delivered. cancel to drop a stuck or unwanted entry.</p>
@ -209,11 +204,10 @@
</aside> </aside>
</div> </div>
<!-- Selection action bar (#443). Sticky-bottom strip that slides <!-- Selection action bar. Sticky-bottom strip that slides into
into view when one or more agent cards is selected (click the view when one or more agent cards is selected (click the icon
icon to toggle). Shows the selection count + actions that to toggle). See docs/web-ui.md::Selection bar for the bulk
apply to ALL selected; disabled-with-tooltip for actions that action gating + clear semantics. -->
don't (mara picked option B). Hidden when selection is empty. -->
<div id="selection-bar" class="selection-bar" hidden role="toolbar" <div id="selection-bar" class="selection-bar" hidden role="toolbar"
aria-label="bulk agent actions"> aria-label="bulk agent actions">
<span class="selection-count" id="selection-count"></span> <span class="selection-count" id="selection-count"></span>
@ -223,11 +217,10 @@
title="clear selection (esc)">✕ clear</button> title="clear selection (esc)">✕ clear</button>
</div> </div>
<!-- Single bundled entry (#406 step 3 — renamed from app.js to <!-- Single bundled entry — tabs.js is the dashboard tabs surface;
tabs.js since this bundle is the dashboard *tabs* surface only; flow.html has its own flow.js bundle. esbuild folds
flow.html has its own flow.js bundle). esbuild folds
@hive/shared/terminal.js and the marked npm package into @hive/shared/terminal.js and the marked npm package into
tabs.js; load order is preserved by the module bundler. --> tabs.js. -->
<script type="module" src="/static/tabs.js" defer></script> <script type="module" src="/static/tabs.js" defer></script>
</body> </body>
</html> </html>