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.
#437 (multi-step progress on rebuild_queue entries — closed) ×2:
in tabs.js current-step comment + in docs/web-ui.md prose
referencing the phase annotation.
#501 (PR — merged for #437) ×1: paired with #437 in the same
tabs.js current-step comment.
#575 (X button to cancel pending builds — closed) ×2: in tabs.js
cancel-form comment + in docs/web-ui.md cancel-button prose.
#436 (route approval execution through rebuild_queue — closed)
×1 in docs/web-ui.md: scrubbed the parenthetical "; #436" next
to the approval source-chip description (small freebie since I
was editing that paragraph anyway). The approval-as-source-chip
is the implementation of #436; the cookie was just history.
Pre-scanned docs for matching cookies; caught all 3 doc cookies
in the same PR.
Container row's pending highlight fired for both queued + running
rebuild-queue entries, so a long queue painted half the SW4RM tab
amber. Mara on #769: don't highlight while queued, show running as
spinner on agent icon.
- tabs.js: derive opRunning (transient OR op.state === 'running')
separately from pending; add a pending-running row class.
Operator-initiated transients still count as running so the
rebuild-now-clicked → backend-picks-it-up window stays visually
consistent.
- dashboard.css: move the amber border + tint from .pending to
.pending-running (queued rows now keep their normal look, with
the badge text still saying "rebuild queued"). Add a 1s linear
rotating amber arc as .pending-running > .container-icon::after,
with overflow:visible so the ring sits just outside the icon and
composes with the mauve selected ring.
- docs/web-ui.md::Container row: describe the queued vs running
split in prose. Drop the orphaned (#398) cookie while I'm at it
— small #712 freebie.
#272 (show approval requested-at — closed) ×2 in tabs.js (live
ApprovalAdded fallback note + amber stale chip comment) + ×1 in
docs/web-ui.md (Approval card identity header). #275 (select all
for meta inputs — closed) ×1 in tabs.js (bulk-select rationale)
+ ×1 in docs/web-ui.md (M3T4 1NPUTS tree control note). The
amber-stale chip line in tabs.js now points at
docs/web-ui.md::Approval card for the rendered spec instead of
the closed issue.
Same theme as the tabs.js scrubs in this batch — drop the 7 #NNN
cookies (#444, #459, #460, #467, #474, #535, #564) the SCH3DUL3S
section was carrying. The substantive prose stands on its own;
the citations were just attribution tracking that belongs in the
git log + issue threads, not the user docs.
Mara on #744: 'no docs md changes needed?' — yes, this.
mara's review nit: explaining the absence of a manager special-case
implicitly endorses the idea that special cases would be normal.
Default is no special case; the doc shouldn't dwell on it.
- ST0P comment block removed entirely (the bulk-button line above
is self-explanatory; the substantive 'c0re survives manager-down'
rationale lives in the host docs, not here).
- M0V3 helper comment reframed: trim the 'no special-case' framing
but keep the substantive note that the backend refuses moves it
can't satisfy and the failure surfaces in the bulk roll-up.
- docs/web-ui.md::Selection bar bullet list: drop the 'manager
included; no special-case' qualifiers; just describe the action.
M0V3→ROOT keeps the rationale that backend refusals surface in
the roll-up but stops calling out manager specifically.
selection + selection-bar in tabs.js carried six #443 cookies + two
#596 cookies — most marking the bulk-bar architectural decision
('actions live in bar, not on per-card buttons; manager not
special-cased') or the SW4RM-tab gate. All scrubbed; the canonical
docs/web-ui.md::Selection bar section already covered the bulk
actions in detail but was missing a preface for the **interaction
model** itself (how selection enters/exits, why per-card buttons
moved). Added that as the opening paragraph of the section so the
in-code refs have something to point at.
Net in this batch:
- tabs.js: -8 #NNN refs (#443 ×6, #596 ×2)
- docs/web-ui.md: +9 lines (interaction-model preface)
- functional code unchanged; build clean
refs #712
Per mara on #14: 'make agent page not assume root path, links / api
calls need to be relative'. atlas's nginx side (#15) will mount the
per-agent UI at a prefix like /agent/<name>/ instead of its own
port; for the page to keep working under that prefix, every
in-page reference needs to resolve document-relative rather than
root-anchored.
Converted in this pass:
- HTML <link>/<script>/<img>/<a> hrefs in index.html, stats.html,
screen.html: '/icon' → 'icon', '/static/agent.css' →
'static/agent.css', back links '/' → './'.
- app.js fetch() targets ('/api/state' → 'api/state', /api/cancel,
/api/loose-ends, etc.), form actions ('/login/start', '/send'),
EventSource urls ('/events/stream', '/events/history').
- stats.js fetch() targets.
- screen.html WebSocket URL: was hardcoded as
ws(s)://host/screen/ws; now derived from document.baseURI via
new URL('screen/ws', document.baseURI) so the gateway prefix
flows through.
Slash-command labels (/cancel, /compact, …) and the dashboard-port
link (different port, intentionally absolute) intentionally
untouched.
Added a new 'Per-agent relative paths' section to docs/web-ui.md
covering the rationale + the trailing-slash gotcha (sub-pages like
/stats must NOT have a trailing slash, or 'static/app.js' resolves
under /stats/ instead of replacing the segment).
Functional code unchanged; build clean. Damocles + atlas can
proceed with the backend / nginx side without depending on this
landing first, but once both ship the agent page works under the
gateway-prefixed URL without further changes.
refs #14
Two threads cleaned up in tabs.js:
1. Top-of-file '#406 step 1/2/3' historical narrative deleted —
it documented past module splits (moves to common.js / flow.js,
file rename app.js → tabs.js) which are git-history concerns,
not current-behaviour docs. Replaced with a two-sentence
contract description that references docs/web-ui.md and the
present-tense module split.
2. Five 'moved to ./common.js (#406)' stubs deleted — same
reasoning. The imports at the top of the file already document
what's in common.js; standalone 'this function lives elsewhere'
pointers are noise once you stop tracking the move event.
paintAtomic's substantive rationale migrated to a new
'Atomic section repaint' subsection in docs/web-ui.md (under the
existing focus-preservation note); in-code comment shrinks to a
two-line reference. Same pattern as the topology-tree batch.
Net: 36 fewer lines in tabs.js, 14 new in docs/web-ui.md.
Functional code unchanged; build clean.
refs #712
First slice of #712 (dashboard SPA frontend → docs/). Shrinks the
buildAgentTree + treePrefixDom comment blocks in tabs.js to one-line
docs/ references; adds a new 'Topology tree' subsection to
docs/web-ui.md under '### Container row' carrying the substantive
prose.
Net effect on this region:
- 6 #NNN tracking cookies dropped (#363, #361, #388)
- 30 lines of substantive design comments collapsed to 6 lines of
references in tabs.js
- 35 new lines in docs/web-ui.md (the migrated prose, restructured
into a single coherent subsection rather than two adjacent comment
blocks)
- functional code unchanged; build clean
Deliberately small pattern-check (one cohesive subsystem) before
expanding to the rest of #712 (~140 more refs across tabs.js +
dashboard.css + index.html + common.js + flow.js). Reviewers can
sanity-check the shape (where prose lands, how short the in-code
ref becomes, whether section nesting reads well) before I scale.
refs #712
Two changes from mara's review:
1. drop the manager special-case. Both M0V3 affordances now apply
regardless of whether the manager is in the selection; backend
topology::set_parent refuses the manager move and the failure
surfaces in the bulk-action error roll-up. Matches the #443 ST0P
policy of 'don't pre-gate manager actions, let the backend speak'.
2. enable the M0V3 → <pick> picker for multi-select. Was single-agent
only in v1. Picker now omits every selected agent itself plus the
union of every selected agent's descendants (cycle-safe across the
whole batch); on submit POSTs once per selected agent sequentially,
same shape as the existing bulk-button loop. Confirm message +
error roll-up adapt to selection size.
docs/web-ui.md updated to match.
Backend POST /api/topology/set-parent already shipped; the dashboard
was missing the operator surface to drive it. Adds two affordances
to the SW4RM tab's selection bar (alongside the existing R3ST4RT /
ST0P / ST4RT / R3BU1LD / DESTR0Y / PURG3 actions):
- '⇡ M0V3 → ROOT' (bulk): promote selected agents to top-level
(parent=null). Disabled when all selected are already at root or
the selection includes the manager (backend refuses anyway).
- '⇢ M0V3 → [pick]' (single-agent only): inline <select> dropdown
+ button pair. Dropdown lists every container that isn't the
target nor a descendant of it (client-side BFS via the existing
c.parent map). On submit POSTs form-encoded
'child=<name>&new_parent=<target>' to /api/topology/set-parent;
the backend re-checks the cycle invariant and re-emits a
container snapshot so the tree repaints without a reload.
Both POSTs hit a single URL, so addBulkButton grew an optional
'perAgentBodyFor(name)' hook to handle the body-driven endpoint
shape (vs the URL-suffix /start/<name> shape every other action
uses). Lifecycle endpoints unchanged.
Mauve chrome (var(--mauve)) reads as 'structural change' rather
than the destructive red / amber of destroy / rebuild.
closes#486
Frontend half of #607 v0 — adds the `◆ M4TR1X ◆ →` page-link entry
to the dashboard tab strip between SCH3DUL3S and FL0W →. Same
`tab-link` class as FL0W → since /matrix/ is its own SPA, not an
in-place pane swap.
Hidden via the `hidden` attribute by default; tabs.js's `refreshState`
flips it based on `s.matrix_gui_enabled` (added in #610). When the
operator hasn't set `hyperhive.matrix.gui.enable = true` the snapshot
returns false and the tab strip doesn't surface a dead link.
docs/web-ui.md updated:
- chrome-header tab-strip list now includes M4TR1X → with the
hidden-when-disabled note
- new "M4TR1X page" section under FL0W explaining the same-origin
serve via hive-c0re ServeDir + the manual homeserver URL pick on
first login (with #609 tracking the post-#15 nginx-front re-root +
.well-known/matrix/client auto-discovery)
Frontend half of #437 — paired with damocles's #501 backend which
adds `step: Option<String>` to `QueueEntry` and annotates 9
worker phases across ApplyCommit / Spawn / MetaUpdate / Rebuild
pipelines.
When `entry.step` is present, render a cyan `↳ <step>` sub-line
below the main row in `renderQueueEntry`. Wraps to its own flex
line via `flex-basis: 100%`, indented 1.8em to align under the
state glyph + kind. Terminal transitions clear `step` on the
backend, so the sub-line just disappears on Done / Failed rows —
no client-side staleness handling needed.
CSS: `.rqe-step` keys off cyan to visually group with the
running-spinner color cue (vs the muted `.rqe-when` /
`.rqe-reason` chips). `tabular-nums` keeps the indented arrow
column stable as step strings change length mid-build.
docs/web-ui.md updated to describe the new sub-line behaviour
in the R3BU1LD QU3U3 section.
#406 was the JS-split refactor: pull shared helpers into
common.js (step 1), pull the flow-only IIFEs into flow.js
(step 2), then rename the legacy combined entry from app.js
to tabs.js (step 3 — this commit) to reflect that the bundle
now owns the dashboard tabs surface only.
What moved:
- `frontend/packages/dashboard/src/app.js` → `tabs.js`
- `build.mjs` entry: `src('app.js')` → `src('tabs.js')`;
output is now `static/tabs.js`
- `index.html` `<script>` src: `/static/app.js` →
`/static/tabs.js`; the FL0W-section comment retouched
- `flow.html` reference from `/static/app.js`-as-tabs to
`/static/tabs.js`; notif + offscreen-inbox comments rewired
to point at the correct owners (common.js for NOTIF,
flow.js for renderInbox)
- `common.js`, `flow.js`, `tabs.js` headers: scrub stale
"app.js" references; document that #406 steps 2 + 3 are
done and both pages import directly from common.js
- `dashboard.css` comments: every "by app.js" → "by tabs.js"
- `docs/web-ui.md`, `docs/conventions.md`: legacy
`assets/app.js` → `assets/tabs.js` (the path prefix is
itself stale from a much-earlier rename, but consistent
with the rest of those docs)
- `CLAUDE.md` file map: refresh the dashboard package's
src/ and dist/ layouts to reflect the post-split shape
(tabs.js, flow.js, common.js, stream-worker.js)
What did NOT move:
- `frontend/packages/agent/src/app.js` (per-agent UI entry)
stays named `app.js` — it's a separate package, has only
one entry point, no split happened there
- The two "no-op when target absent" guards in renderContainers
and renderQuestions are kept as belt-and-suspenders for any
future page that adds tabs.js without the corresponding
sections; comments updated to note this rather than the
pre-split flow.html reason
Functional behaviour identical; this is a pure rename + comment
sweep.
mara confirmed target editing on q #195 ("also let me edit targets").
Damocles wired `targets_add` / `targets_remove` onto PATCH
/api/schedules/{id} in #478. UI side: the schedule edit form's
read-only targets callout becomes a multi-select checkbox box
(same chip styling as the new-schedule form) pre-checked for
currently-active targets.
Submit logic:
- diff new selection against `originalActiveTargets` to populate
`targets_add` (checked, not originally active) and
`targets_remove` (originally active, now unchecked)
- only include each key in the PATCH body when non-empty
- guard against zero-target submission with a clear alert
pointing the operator at `✕ cancel all` as the intended path
- already-active targets that aren't in the live candidate list
(e.g. a since-destroyed container) still surface so the
operator can intentionally drop them
Note (re-add semantics): backend's replace-on-conflict drops
the cancelled-target tombstone + history on re-add — per the
design discussion with damocles, operator intent on re-adding
reads as "fresh start, target is active again." UI copy below
the chip row reflects this.
Carry persistence extended to the `targets` array so the
checkbox state survives a state-poll re-render mid-edit;
listens on `change` in addition to `input` for checkbox events.
Operator wants to trigger a scheduled prompt immediately
instead of waiting for the next interval. Adds a `↯ fire now`
button on every active schedule row, next to `✕ cancel all`.
Semantics (per design discussion with damocles):
- recurring schedules → out-of-band pulse, `next_fire_at_unix`
untouched; the regular cadence keeps firing on the original
schedule. Operator gets an extra fan-out, not a phase shift.
- one-shots → consumed after the manual fire. Operator's
intent reads as "the scheduled time was wrong, send NOW";
leaving the original time would be surprising.
Confirm dialog spells out the recurring-vs-one-shot
difference up front so the operator knows what they're about
to do. Button is disabled when every target is already
cancelled (nothing to fire).
Talks to `POST /api/schedules/{id}/fire-now` (damocles is
wiring the backend in parallel). Mauve styling distinguishes
it from cancel (red) and submit (amber); fits the existing
btn pattern.
docs/web-ui.md updated.
Per damocles's PATCH /api/schedules/{id} backend (#475), each
non-cancelled schedule row gets a `✎ edit` button that toggles
an inline form pre-filled with current values. Editable:
- body (textarea)
- description (blank to clear)
- next-fire (datetime-local)
- interval (shared composer from #466 — all-zero flips to one-shot)
Targets stay immutable per the design call with damocles:
per-target last-result history is keyed on them; cancel + new
schedule is the documented retarget workaround. The form
surfaces the active target list as a read-only note explaining
this.
Submit semantics — the form computes a PATCH diff against the
original schedule and only includes keys for fields that
actually changed. Blank description → `null` (clear), all-zero
interval on a recurring schedule → `null` (flip to one-shot).
No-op submit (no fields changed) just closes the edit form.
Refactored the interval composer (#466) into a shared
`buildIntervalComposer({ label, namePrefix, initialSeconds })`
helper so the new-schedule and edit-schedule forms use the same
chip/d/h/m/s/preview widget. New-schedule form behavior
unchanged; edit-form input names are prefixed `edit_interval_`
to avoid FormData collisions when both happen to mount.
State survives state-poll re-renders via `editingSchedules`
(Set of ids being edited) + `scheduleEditCarry` (per-id
mid-edit values) — same pattern the new-schedule form uses
with `readScheduleFormCarry`.
Replaces the single raw-seconds field in the SCH3DUL3D PR0MPTS
creation form with a structured composer:
- Preset chip row (1m / 5m / 15m / 30m / 1h / 6h / 12h / 1d / 7d
/ one-shot) that fills the parts inputs in one click
- Four small d/h/m/s number inputs combined into total seconds
on submit (all-zero = one-shot, preserving the backend null
semantic)
- Live "↻ every …" preview using the existing fmtDuration helper
so the operator sees what they're about to queue
Carry semantics extended to round-trip the split fields across
state-poll re-renders; legacy `interval` carry still honoured if
an older bundle is in the page.
Docs (web-ui.md) updated to describe the new composer.
mara wanted one place for everything that fires at a future time.
The QU3U3D R3M1ND3RS section moves out of the SYST3M tab and
lands at the bottom of the SCH3DUL3S tab, below QU3U3D SCH3DUL3S.
Pure layout change — refreshReminders() and /api/reminders
unchanged; the section just lives under a different parent now.
Reminders don't contribute to the SCH3DUL3S pill count (kept as
"active schedules" only — reminders are self-scheduled and high
cardinality; adding them would make the pill noisy).
docs/web-ui.md picks up a SCH3DUL3S tab section (the previous
docs lumped the schedules subsection under SYST3M, but the tab
has been separate since #459); reminders subsection follows
schedules in the same tab.
Phase 4 of #273 — the actual switch. Both axum routers now serve
their static surface via `tower_http::services::ServeDir` mounted
as a fallback service, reading the dist path from `HIVE_STATIC_DIR`
(set by Phase 3's NixOS module wiring).
Deletes:
- `hive-c0re/assets/{index.html, app.js, dashboard.css}`
- `hive-ag3nt/assets/{index.html, app.js, agent.css, stats.html,
stats.js, screen.html}`
- The whole `hive-fr0nt/` crate (workspace member dropped, both
hive-c0re and hive-ag3nt drop their `hive-fr0nt.workspace = true`
dep). Its contents now live as `@hive/shared` under
`frontend/packages/shared/`.
Rust changes:
- `hive-c0re/src/dashboard.rs`: remove `serve_index`, `serve_css`,
`serve_app_js`, `serve_shared_js`, `serve_marked_js`,
`serve_favicon` (all six `include_str!` handlers); replace their
routes with a single `.fallback_service(ServeDir::new(static_dir))`
on the router. Fail closed (anyhow::bail) if `HIVE_STATIC_DIR` is
unset or not a directory at startup.
- `hive-ag3nt/src/web_ui.rs`: remove `serve_index`, `serve_css`,
`serve_app_js`, `serve_shared_js`, `serve_marked_js`,
`serve_stats`, `serve_stats_js`, `serve_screen`; same
`fallback_service` pattern. `serve_icon` stays (consumes
`/etc/hyperhive/icon.svg` + `branding/hyperhive.svg` fallback,
neither of which lives under the frontend dist).
- `AgentLink` URLs for stats/screen switched from `/stats` / `/screen`
to `/stats.html` / `/screen.html` since ServeDir doesn't auto-
append the extension and the on-disk filename is the natural URL
post-cutover.
- `Cargo.toml` (workspace): drop `hive-fr0nt` member + workspace
dep, add `tower-http = { version = "0.6", features = ["fs"] }`.
- `hive-c0re/Cargo.toml` + `hive-ag3nt/Cargo.toml`: drop the
`hive-fr0nt.workspace = true` dep, add `tower-http.workspace =
true`.
Docs updated:
- `CLAUDE.md`: file map reflects `frontend/` (was `hive-fr0nt/` +
`assets/`) and the ServeDir/HIVE_STATIC_DIR shape.
- `docs/web-ui.md` 'Shape (shared by both)' section: describes the
ServeDir fallback + bundled-by-esbuild surface, no more
`include_str!` references.
- `docs/terminal-rendering.md`: src paths point at
`frontend/packages/{agent,shared}/src/`; marked is the npm dep,
not vendored UMD.
Validation:
- `cargo check --workspace` — clean (5 warnings, all pre-existing
in `rebuild_queue.rs`, none on changed files).
- `cargo clippy --workspace --all-targets` — clean (11 warnings,
same pre-existing source).
- `cd frontend && npm run build` from the prior commit's lockfile
produces the dist directories the new routers consume:
dashboard: `dist/{index.html, static/{app.js, dashboard.css}}`
agent: `dist/{index.html, stats.html, screen.html,
static/{app.js, stats.js, agent.css}}`
(favicon.svg lands in dashboard/ during the nix build —
`nix/frontend.nix` install phase copies `branding/hyperhive.svg`
there, since it's outside the npm tree.)
Refs #273.
The previous take put a shared NavLink wire type in hive-sh4re and
duplicated the link-building logic across crates. Per @mara on #326:
that doesn't fit the eventual frontend/backend split goal (#273).
The agent backend is the natural source of truth for what links its
own page exposes; hive-c0re just passes the list through to the
dashboard.
* hive-ag3nt/src/web_ui.rs: agent_links now also serves the
config-repo link + reads agent-declared dashboardLinks extras
from {state_dir}/hyperhive-dashboard-links.json. AgentLink gains a
kind enum (Container | Forge | External) so the frontend can build
the right href no matter which surface is rendering. The host
header is no longer used — URLs are paths for Container/Forge,
absolute for External.
* hive-c0re/src/dashboard.rs: new GET /api/agent/{name}/links route,
a same-origin proxy that fetches the agent's /api/state and
forwards just the links field. No shared wire type — hive-c0re
treats the payload as opaque JSON (serde_json::Value). All failure
modes degrade to an empty list so the dashboard still renders.
* hive-c0re/assets/app.js: container card head row gets an async-
populated icon-only nav strip from the proxy. The hardcoded stats
link, the standalone config-repo trigger, and the extras block are
gone. The deployed:<sha> chip stays — the agent harness can't know
its own deployed sha, so this chip is how the operator sees what's
live alongside the agent's (root-only) config link.
* hive-ag3nt/assets/app.js: agent page meta-links rendered via
el() / textContent (DOM build) so agent-declared icon / label / url
strings never reach innerHTML. kind-based href resolution mirrors
the dashboard side.
* docs/web-ui.md: dashboard + per-agent sections updated for the new
architecture.
Closes#262.
Remove the depth-2 cap in walk_meta_inputs so every fetched input
at every depth is surfaced, not just two levels (issue #275). The
uncapped walk needs a guard: a visited-node set makes it a spanning
tree — each fetched node walked once, at its shallowest path — so
shared subtrees don't re-walk and a cycle can't recurse forever.
A two-pass walk (claim a node's direct inputs before descending)
keeps shallow inputs at a shallow path.
Frontend: renderMetaInputs indents each row by its slash-path depth
and shows the leaf segment (full path on hover), plus a select-all /
select-none control so a long input list isn't ticked box by box.
post_meta_update returns 200 immediately and runs the nix flake
update + agent-rebuild ripple in a background task, so the META
INPUTS panel looked idle for the whole multi-minute window (#259).
Track in-flight runs with a Coordinator atomic counter, exposed via
an RAII MetaUpdateGuard held across run_meta_update. Surface it as
the meta_update_running snapshot field plus a MetaUpdateRunning SSE
event (flipped only when the count crosses 0, so concurrent runs
flip the flag once). The panel shows a pulsing in-progress banner
and disables the update button while a run is active.