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:
parent
68c2b3284e
commit
ea1c6e4d05
2 changed files with 48 additions and 41 deletions
|
|
@ -193,7 +193,7 @@ cross-page link (`◆ FL0W ◆ →`), not a pane swap.
|
||||||
`services.hyperhive.matrix.gui.enable` is off (defaults to
|
`services.hyperhive.matrix.gui.enable` is off (defaults to
|
||||||
`matrix.enable`) so operators without the matrix GUI on don't
|
`matrix.enable`) so operators without the matrix GUI on don't
|
||||||
see a dead link — tabs.js gates the `hidden` attribute on
|
see a dead link — tabs.js gates the `hidden` attribute on
|
||||||
`state.matrix_gui_enabled` from `/api/state` (#607).
|
`state.matrix_gui_enabled` from `/api/state`.
|
||||||
- **Notification controls**: `🔔 enable notifications` when
|
- **Notification controls**: `🔔 enable notifications` when
|
||||||
permission ungranted; `🔕 mute / 🔔 unmute` toggle once granted.
|
permission ungranted; `🔕 mute / 🔔 unmute` toggle once granted.
|
||||||
Always visible in the chrome regardless of active tab.
|
Always visible in the chrome regardless of active tab.
|
||||||
|
|
@ -404,6 +404,23 @@ falls back to the dimmed hyperhive mark (`/favicon.svg`). The card
|
||||||
body sits to the right with three stacked lines
|
body sits to the right with three stacked lines
|
||||||
(`assets/tabs.js::renderContainers`).
|
(`assets/tabs.js::renderContainers`).
|
||||||
|
|
||||||
|
**Icon layout + load strategy:** the `<img>` is absolutely
|
||||||
|
positioned (`inset: 0`) inside the `.container-icon` wrapper —
|
||||||
|
the wrapper is the flex child and sizes itself via `width: 5em` +
|
||||||
|
`aspect-ratio: 1`, the `<img>` is out of flow so its load state
|
||||||
|
(pending, loaded, broken) can never contribute intrinsic size or
|
||||||
|
reflow the row. Without that, the row would briefly grow as the
|
||||||
|
image's natural dimensions arrived, then snap back on
|
||||||
|
`object-fit: contain`. The load itself is fire-and-forget: the
|
||||||
|
dashboard doesn't pre-check whether the agent is reachable, it
|
||||||
|
just lets the `<img>` try and listens for an `error` event. On
|
||||||
|
failure the handler swaps the `src` to `/favicon.svg` (served by
|
||||||
|
the dashboard itself, always reachable) and adds the
|
||||||
|
`icon-unreachable` class for the dimmed look. When the container
|
||||||
|
is known stopped up front (`ContainerView.running = false`) the
|
||||||
|
fallback fires immediately, skipping the doomed `<url>/icon`
|
||||||
|
fetch entirely.
|
||||||
|
|
||||||
- Line 1: agent name (link → new tab), m1nd/ag3nt chip, an
|
- Line 1: agent name (link → new tab), m1nd/ag3nt chip, an
|
||||||
**icon-only nav strip** populated async from the agent backend
|
**icon-only nav strip** populated async from the agent backend
|
||||||
(`📊 stats`, `🖥 screen` when GUI is enabled, `⬡ forge profile`,
|
(`📊 stats`, `🖥 screen` when GUI is enabled, `⬡ forge profile`,
|
||||||
|
|
@ -440,6 +457,21 @@ body sits to the right with three stacked lines
|
||||||
row tint AND draws a **rotating amber ring** around the agent
|
row tint AND draws a **rotating amber ring** around the agent
|
||||||
icon, so it's obvious at a glance which container is actually
|
icon, so it's obvious at a glance which container is actually
|
||||||
moving.
|
moving.
|
||||||
|
**Pending-state derivation:** the pill is sourced from two
|
||||||
|
separate stores in priority order. (1) The operator-initiated
|
||||||
|
**transient** (`transientsState`) is set on the dashboard the
|
||||||
|
moment the operator clicks start / stop / restart / rebuild /
|
||||||
|
destroy / spawn — covers the create-and-start window where the
|
||||||
|
container literally isn't up yet, before any backend state event
|
||||||
|
has fired. (2) If no transient is set, the **rebuild-queue
|
||||||
|
entry** for this agent is consulted (`rebuildQueueState`); this
|
||||||
|
covers worker-driven ops — meta-update cascades, crash-recover
|
||||||
|
rebuilds, approval-driven rebuilds — that the operator didn't
|
||||||
|
click. `ContainerStateChanged` carries neither signal, so the
|
||||||
|
dashboard reads from the two snapshots directly. The
|
||||||
|
`opRunning` flag (driving the `pending-running` row class +
|
||||||
|
spinner) is true when (1) is set OR (2) is in `running` state;
|
||||||
|
queued entries leave `opRunning` false.
|
||||||
Container name + port, and a `ctx · Nk` chip showing the
|
Container name + port, and a `ctx · Nk` chip showing the
|
||||||
agent's last-turn context size (from `ContainerView.ctx_tokens`,
|
agent's last-turn context size (from `ContainerView.ctx_tokens`,
|
||||||
read from the turn-stats sqlite on each `build_all` sweep;
|
read from the turn-stats sqlite on each `build_all` sweep;
|
||||||
|
|
|
||||||
|
|
@ -481,12 +481,9 @@ window.marked = marked;
|
||||||
for (const node of tree) {
|
for (const node of tree) {
|
||||||
const c = node.container;
|
const c = node.container;
|
||||||
const url = `http://${hostname}:${c.port}/`;
|
const url = `http://${hostname}:${c.port}/`;
|
||||||
// Pending state is overlaid from the transient store first
|
// Pending-state derivation + queued-vs-running split — see
|
||||||
// (operator-initiated spawn/destroy/rebuild — covers the
|
// docs/web-ui.md::Container row for the transient → in-flight
|
||||||
// create+start window when the container literally isn't up
|
// queue priority order and the opRunning rationale.
|
||||||
// yet), then from the rebuild_queue (covers worker-driven ops
|
|
||||||
// even if no transient was set). `ContainerStateChanged`
|
|
||||||
// doesn't carry either signal.
|
|
||||||
const transientKind = transientsState.get(c.name)?.kind || null;
|
const transientKind = transientsState.get(c.name)?.kind || null;
|
||||||
const op = !transientKind ? inFlight.get(c.name) : null;
|
const op = !transientKind ? inFlight.get(c.name) : null;
|
||||||
const pending = transientKind
|
const pending = transientKind
|
||||||
|
|
@ -497,12 +494,6 @@ window.marked = marked;
|
||||||
: (op.kind === 'meta_update' ? 'meta-update queued'
|
: (op.kind === 'meta_update' ? 'meta-update queued'
|
||||||
: op.kind === 'destroy' ? 'destroy queued'
|
: op.kind === 'destroy' ? 'destroy queued'
|
||||||
: 'rebuild 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
|
const opRunning = transientKind != null
|
||||||
|| (op != null && op.state === 'running');
|
|| (op != null && op.state === 'running');
|
||||||
const selected = selectionState.has(c.name);
|
const selected = selectionState.has(c.name);
|
||||||
|
|
@ -522,24 +513,12 @@ window.marked = marked;
|
||||||
const prefix = treePrefixDom(node);
|
const prefix = treePrefixDom(node);
|
||||||
if (prefix) li.prepend(prefix);
|
if (prefix) li.prepend(prefix);
|
||||||
|
|
||||||
// Full-height square agent icon, left of the card body. The
|
// Agent icon: 5em square wrapper with an absolutely-positioned
|
||||||
// icon is an <img> absolutely positioned inside a wrapper div:
|
// <img> + fire-and-forget load with /favicon.svg fallback. The
|
||||||
// the div is the flex child and sizes itself via aspect-ratio +
|
// wrapper doubles as the selection toggle (click / keyboard).
|
||||||
// stretch, the <img> is out of flow so its load state — pending,
|
// See docs/web-ui.md::Container row for the layout + load-strategy
|
||||||
// loaded or broken — can never contribute intrinsic size or
|
// rationale.
|
||||||
// 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)
|
|
||||||
const iconImg = el('img', { class: 'container-icon-img', alt: '' });
|
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', {
|
const icon = el('div', {
|
||||||
class: 'container-icon',
|
class: 'container-icon',
|
||||||
role: 'button',
|
role: 'button',
|
||||||
|
|
@ -3116,10 +3095,8 @@ window.marked = marked;
|
||||||
// synchronous read (e.g. the compose autocomplete pulls agent
|
// synchronous read (e.g. the compose autocomplete pulls agent
|
||||||
// names from here instead of refetching on every keystroke).
|
// names from here instead of refetching on every keystroke).
|
||||||
window.__hyperhive_state = s;
|
window.__hyperhive_state = s;
|
||||||
// #607: surface the M4TR1X → tab strip entry only when the
|
// Gate the M4TR1X → tab strip entry — see
|
||||||
// backend's HIVE_MATRIX_GUI_DIR mount is live (otherwise
|
// docs/web-ui.md::Tab strip for the matrix_gui_enabled rationale.
|
||||||
// clicking would 404). `state.matrix_gui_enabled` flips when
|
|
||||||
// the operator toggles `hyperhive.matrix.gui.enable` and rebuilds.
|
|
||||||
const matrixTab = $('tab-matrix');
|
const matrixTab = $('tab-matrix');
|
||||||
if (matrixTab) matrixTab.hidden = !s.matrix_gui_enabled;
|
if (matrixTab) matrixTab.hidden = !s.matrix_gui_enabled;
|
||||||
const openDetails = snapshotOpenDetails();
|
const openDetails = snapshotOpenDetails();
|
||||||
|
|
@ -3201,13 +3178,11 @@ window.marked = marked;
|
||||||
rebuild_queue_changed: applyRebuildQueueChanged,
|
rebuild_queue_changed: applyRebuildQueueChanged,
|
||||||
};
|
};
|
||||||
(function bindDashboardStream() {
|
(function bindDashboardStream() {
|
||||||
// #448: route the EventSource through a SharedWorker so all open
|
// Route through the SharedWorker so all open hyperhive tabs share
|
||||||
// hyperhive tabs share ONE backend SSE connection. Survives
|
// one upstream SSE connection — see docs/web-ui.md (SSE multiplexing
|
||||||
// Firefox's per-tab connection throttling under many-open-tabs
|
// paragraph) for the design + Firefox throttling motivation.
|
||||||
// pressure (the actual mara symptom). `openStream` returns an
|
// `openStream` returns an EventSource-shaped facade with a graceful
|
||||||
// EventSource-shaped facade so the rest of this IIFE is unchanged;
|
// direct-EventSource fallback when SharedWorker isn't supported.
|
||||||
// graceful fallback to direct `new EventSource` when SharedWorker
|
|
||||||
// isn't supported.
|
|
||||||
const es = openStream('/dashboard/stream');
|
const es = openStream('/dashboard/stream');
|
||||||
es.onmessage = (e) => {
|
es.onmessage = (e) => {
|
||||||
let ev;
|
let ev;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue