tabs.js + docs: migrate icon + pending-state prose to docs (#712 batch 11)

Mara on #774: previous batches were cookie-stripping rather than
prose migration. This batch actually moves substantive comment
prose from tabs.js into docs/web-ui.md.

Moved to docs/web-ui.md:
- Container row → **Icon layout + load strategy** sub-paragraph
  (new): absolute-positioning rationale (so img load state can't
  reflow row), fire-and-forget load pattern (no pre-check
  reachability), favicon fallback chain, icon-unreachable class,
  the immediate-fallback-when-stopped optimisation.
- Container row → **Pending-state derivation** paragraph (new):
  three-source priority order (transient → in-flight queue → none),
  why ContainerStateChanged isn't enough, the opRunning flag's role
  in driving the pending-running class + spinner.

Collapsed in tabs.js:
- Icon block (~14 lines of WHY comments + pointer to docs) →
  4-line pointer + behavioural one-liner. Drops #177 / #195 / #202
  cookies en passant since their substance now lives in docs.
- Pending-state block (~22 lines split across two paragraphs) →
  4-line pointer. Drops #769 self-cookie (the queued vs running
  split lives in docs::Container row now).
- SharedWorker EventSource (~7 lines) → 5-line pointer. Drops
  #448 cookie (the SSE multiplexing paragraph in docs already
  has the design + Firefox throttling rationale; the in-code
  comment was duplicating).
- M4TR1X tab gating (~4 lines) → 2-line pointer. Drops #607 cookie
  in both tabs.js + docs/web-ui.md::Tab strip (the substance was
  already in docs, just had the cookie attached).

tabs.js: 11 → 6 refs (92% reduction from baseline 73). Net effect:
~47 lines of substantive prose moved out of tabs.js into
docs/web-ui.md, where it belongs.
This commit is contained in:
iris 2026-05-31 14:23:49 +02:00
commit ea1c6e4d05
2 changed files with 48 additions and 41 deletions

View file

@ -481,12 +481,9 @@ window.marked = marked;
for (const node of tree) {
const c = node.container;
const url = `http://${hostname}:${c.port}/`;
// Pending state is overlaid from the transient store first
// (operator-initiated spawn/destroy/rebuild — covers the
// create+start window when the container literally isn't up
// yet), then from the rebuild_queue (covers worker-driven ops
// even if no transient was set). `ContainerStateChanged`
// doesn't carry either signal.
// Pending-state derivation + queued-vs-running split — see
// docs/web-ui.md::Container row for the transient → in-flight
// queue priority order and the opRunning rationale.
const transientKind = transientsState.get(c.name)?.kind || null;
const op = !transientKind ? inFlight.get(c.name) : null;
const pending = transientKind
@ -497,12 +494,6 @@ window.marked = marked;
: (op.kind === 'meta_update' ? 'meta-update queued'
: op.kind === 'destroy' ? 'destroy queued'
: 'rebuild queued')));
// Split the visual into queued vs running (#769): queued ops get
// no row highlight — the badge text alone says "wait, queued" —
// while running ops keep the amber row tint + add a rotating
// amber ring on the agent icon. Operator-initiated transients
// always count as running (the op is already in flight even if
// the queue worker hasn't picked it up yet).
const opRunning = transientKind != null
|| (op != null && op.state === 'running');
const selected = selectionState.has(c.name);
@ -522,24 +513,12 @@ window.marked = marked;
const prefix = treePrefixDom(node);
if (prefix) li.prepend(prefix);
// Full-height square agent icon, left of the card body. The
// icon is an <img> absolutely positioned inside a wrapper div:
// the div is the flex child and sizes itself via aspect-ratio +
// stretch, the <img> is out of flow so its load state — pending,
// loaded or broken — can never contribute intrinsic size or
// reflow the row. (issue #177)
//
// The icon points straight at the agent's `<url>/icon`. We don't
// guess whether the agent is reachable from the container row —
// we just let the <img> try, and if it actually fails to load
// (agent stopped, restarting, rebuilding — web server not
// answering) the error handler falls it back to the dimmed
// hyperhive mark (`/favicon.svg`, served by the dashboard
// itself, always reachable). (issues #195, #202)
// Agent icon: 5em square wrapper with an absolutely-positioned
// <img> + fire-and-forget load with /favicon.svg fallback. The
// wrapper doubles as the selection toggle (click / keyboard).
// See docs/web-ui.md::Container row for the layout + load-strategy
// rationale.
const iconImg = el('img', { class: 'container-icon-img', alt: '' });
// Icon is the selection toggle. Click → add/remove from
// `selectionState` → re-render. role=button + tabindex makes it
// keyboard-accessible; aria-pressed reflects the toggle state.
const icon = el('div', {
class: 'container-icon',
role: 'button',
@ -3116,10 +3095,8 @@ window.marked = marked;
// synchronous read (e.g. the compose autocomplete pulls agent
// names from here instead of refetching on every keystroke).
window.__hyperhive_state = s;
// #607: surface the M4TR1X → tab strip entry only when the
// backend's HIVE_MATRIX_GUI_DIR mount is live (otherwise
// clicking would 404). `state.matrix_gui_enabled` flips when
// the operator toggles `hyperhive.matrix.gui.enable` and rebuilds.
// Gate the M4TR1X → tab strip entry — see
// docs/web-ui.md::Tab strip for the matrix_gui_enabled rationale.
const matrixTab = $('tab-matrix');
if (matrixTab) matrixTab.hidden = !s.matrix_gui_enabled;
const openDetails = snapshotOpenDetails();
@ -3201,13 +3178,11 @@ window.marked = marked;
rebuild_queue_changed: applyRebuildQueueChanged,
};
(function bindDashboardStream() {
// #448: route the EventSource through a SharedWorker so all open
// hyperhive tabs share ONE backend SSE connection. Survives
// Firefox's per-tab connection throttling under many-open-tabs
// pressure (the actual mara symptom). `openStream` returns an
// EventSource-shaped facade so the rest of this IIFE is unchanged;
// graceful fallback to direct `new EventSource` when SharedWorker
// isn't supported.
// Route through the SharedWorker so all open hyperhive tabs share
// one upstream SSE connection — see docs/web-ui.md (SSE multiplexing
// paragraph) for the design + Firefox throttling motivation.
// `openStream` returns an EventSource-shaped facade with a graceful
// direct-EventSource fallback when SharedWorker isn't supported.
const es = openStream('/dashboard/stream');
es.onmessage = (e) => {
let ev;