Adds the P33RS tab to the dashboard. The tab is hidden by default and
only shown when at least one peer hive is configured in
`services.hyperhive.peers`. When visible, it renders a card per peer
with a link to that hive's dashboard.
Backend (HYPERHIVE_PEERS env var, PeerHiveView in StateSnapshot) is
wired by a separate PR. This commit is frontend only:
- index.html: P33RS tab strip entry + tab-pane-peers section
- tabs.js: renderPeerHives(), gate logic, 'peers' added to TABS array
Damocles's backend (c41bf1b) landed HYPERHIVE_HIVE_NAME / _SWARM_NAME
env vars and identity.rs accessors. This commit wires them into the two
web surfaces:
hive-c0re/dashboard.rs:
StateSnapshot gains `hive_name` + `swarm_name` (Option<String>),
populated from the env vars the c0re NixOS module injects.
hive-ag3nt/web_ui.rs:
Per-agent StateSnapshot gains the same two fields, populated from
crate::identity::hive_name() / swarm_name().
Dashboard frontend (index.html + tabs.js):
A `<p class="banner-thin" id="hive-identity">` sits above the tab
strip in the chrome. refreshState() populates it with "swarm / hive"
(or just "hive") when the fields are non-null, and updates
document.title to "<swarm>/<hive> // h1ve-c0re". No change to
chrome when both fields are null (backward compat).
Per-agent frontend (index.html + app.js + agent.css):
A hidden `.agent-hive-label` span under the title row shows the
swarm/hive label once setHeader() receives non-null names.
document.title is updated to "<label> // <hive_name>" when hive_name
is set, enabling browser-tab disambiguation when multiple hives are
open in parallel. Styled as a small uppercase purple-dim sub-label.
Adds a fifth tab (◆ S3TT1NGS ◆) at the end of the dashboard tab
strip; moves the browser-notification controls out of the chrome
header into the new pane.
### Why
The chrome was carrying the notification controls as 'always-on
ergonomics' but the surface they belong on is operator preferences,
not navigation. With #589 / #701 likely adding more per-browser
preferences later (theme / density / etc.), having a dedicated
S3TT1NGS pane gives them a home so the chrome strip doesn't keep
accreting controls.
### Shape
- index.html: new `#tab-pane-settings` section with H2 + intro +
H3 'browser notifications' group; same notif-row DOM (same IDs)
migrated from the chrome
- flow.html: drops the duplicated notif-row from its chrome (FL0W
reused the dashboard's strip); adds a `◆ S3TT1NGS ◆ →`
cross-page link to `/#settings`. Notifications still fire on
FL0W when enabled — NOTIF.show() reads
Notification.permission + the hyperhive.notify.muted localStorage
key directly, no buttons needed in the page DOM.
- tabs.js: adds 'settings' to TABS array so hash routing activates
the pane
- dashboard.css: drops the chrome-anchoring `.tabbar #notif-row`
rule and the flow-page reuse comment, both obsolete; the general
`.notif-row` styling stays
- docs/web-ui.md: new `### S3TT1NGS tab` section; Chrome-header
bullet drops 'Notification controls' + adds S3TT1NGS; Browser
notifications section pointers at the new pane
### No JS or backend changes
Same IDs (notif-row, notif-enable, notif-mute, notif-unmute,
notif-status) so `NOTIF.bind()` in common.js wires the new pane
unchanged. NOTIF.show() dispatch is DOM-independent.
### Validation
`npm run --workspace @hive/dashboard build` clean (tabs.js 167.8kb,
flow.js 37.4kb, dashboard.css 50.2kb).
Footer was still pointing at the old canonical at
`git.berlin.ccc.de/vinzenz/hyperhive` — that's the upstream URL
from before #524 moved the public mirror to `forge.darkest.space`.
README + docs already reference the new URL; this was the only
stale frontend pointer.
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)
The N3W SCH3DUL3 vertical form is gone. The schedules table now
carries an always-visible inline create row at the bottom — fill the
cells, click + to POST /api/schedules, the new schedule appears
above on the next refresh.
Per-column inputs match the display semantics:
- `next` → `<input type="datetime-local">` (defaults to now+5min)
- `every` → 4 mini d/h/m/s number inputs (blank/all-zero = one-shot)
- `body` → `<textarea rows=1>` expanding to 4em on focus-within
so multi-line prompts still fit (closes mara's "prompt may still be
multi-line" requirement)
- per-agent columns → checkbox (whole cell clickable via padded label)
- actions → + submit + ⌫ reset (clears the carry without POSTing)
`newScheduleCarry` module-scope object survives the paintAtomic
re-render so mid-typed values stick across schedule-list refreshes
(same pattern as `scheduleEditCarry` from #474).
H2 collapses from "N3W SCH3DUL3" + "QU3U3D SCH3DUL3S" down to a
single "SCH3DUL3S" since the section now does both. Empty-state
branch on `renderSchedulesList` is gone — the table always renders
because the create row is always there.
`#schedule-new-section` div + `renderScheduleNewForm` /
`readScheduleFormCarry` / `submitNewSchedule` and the
`.schedule-new-form` CSS selector all removed. The edit-form path
(#474 `renderScheduleEditForm` colspan'd row on `✎` toggle) is
unchanged — "edit mode is inline in the table" already shipped
there, this PR just adds the same mental model for create.
Validations match the old form: empty targets / empty body /
invalid datetime / non-integer interval each surface an `alert()`.
The + button shows a spinner while the POST is in flight, then
clears the carry + triggers `refreshSchedules()`.
#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 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.
frontend for the #444 scheduled-prompts feature. backend is
already merged (PR #454 + sibling commits): GET /api/schedules
(snapshot), POST /api/schedules (operator-direct submit), POST
/api/schedules/{id}/cancel (whole or per-target).
new chrome
- SCH3DUL3S tab in the dashboard tab strip, between SYST3M and
the FL0W link. count pill shows the number of schedules with
at least one still-active target.
- pane has two sections: N3W SCH3DUL3 (creation form) +
QU3U3D SCH3DUL3S (list of cards).
creation form
- targets multi-select rendered as chip-style checkboxes;
candidates pulled from containersState plus the special
`operator` and `manager` recipients.
- prompt body textarea (required, non-empty trim).
- first-fire datetime-local input, defaulted to "5 minutes from
now" so the form has a sensible pre-filled future timestamp.
- optional interval (seconds) input — blank = one-shot.
- optional description (one-liner shown on the schedule card).
- mid-typing carry: re-rendering the form preserves field values
+ checkbox state. the operator never loses what they were
typing when the schedule list refreshes underneath them.
- POSTs SchedulePromptPayload JSON to /api/schedules; on success
re-fetches the list to surface the new row.
schedules list
- one card per schedule, active rows first (sorted by
next_fire_at_unix), cancelled tail dimmed.
- header: id, source chip (`operator` or `approval` — reuses
the rebuild-queue rqe-source styling for visual consistency),
next-fire countdown / overdue label, recurring vs one-shot
badge, owner.
- body: prompt text in a styled <pre>-ish block with linkified
path references.
- per-target table: target name, last fire age, last result,
per-row cancel button.
- whole-schedule "✕ cancel all" button.
- per-target cancel posts { targets: ["name"] }; cancel-all
posts no body (== cancel whole row).
no-SSE refresh
- the backend doesn't emit SchedulesChanged dashboard events
yet (damocles flagged this as a follow-up PR C). list
re-fetches on:
- tab activation (so switching to SCH3DUL3S never lands stale)
- cold load via refreshState
- after every submit + cancel POST
the operator's typical interactions all force a refresh; the
remaining gap (worker fires while you're staring at the tab)
is the natural argument for PR C.
files
- frontend/packages/dashboard/src/index.html — new tab + pane
with the two sections.
- frontend/packages/dashboard/src/app.js — schedulesState
cache, refreshSchedules, render*ScheduleNewForm, submit +
cancel helpers, tab routing extended for `schedules`, count
pill wired into refreshTabCounts.
- frontend/packages/dashboard/src/dashboard.css — schedule
form + card styling + chip checkboxes + targets table. small
.btn-inline-small helper for the per-target cancel.
validation
- npm run build --workspace=@hive/dashboard clean. app.js
158 kb → 161 kb. CSS 41.3 kb → 43.9 kb.
- browser smoke test isn't possible from inside iris's container;
endpoints are wire-compatible (backend types unchanged) and
the form serialisation matches SchedulePromptPayload's JSON
shape exactly (targets / body / first_fire_at_unix /
interval_seconds / description).
per mara on #443: "dont show all the restart buttons etc., just
show state and links. instead, clicking an agent icon selects that
agent. you can select as many as you like. then you can run an
action on all of them."
selection model
- module-level selectionState = Set<string> of agent logical
names. clicking a container-row icon toggles membership; Esc
clears the whole set (ignored when an editable element has
focus so typing in compose / answer / journal-search isn't
intercepted).
- icon now has role="button" + tabindex="0" so it's
keyboard-accessible; aria-pressed reflects the toggle state.
hover + focus-visible get a mauve ring.
- selected rows get a .selected class — mauve outline +
faint mauve wash on the row, brighter ring on the icon.
- on every renderContainers pass, stale selections (agents
destroyed while selected) are pruned defensively.
sticky action bar
- new #selection-bar in index.html — fixed bottom strip with
count chip, name list, action buttons, clear button. hidden
when selection empty. styled to match the flow composer's
frosted-mauve chrome (vibecore family).
- per mara's option B answer: every action button is visible;
buttons that don't apply to the whole selection are disabled
with a tooltip explaining WHY ("iris is already stopped" etc).
.btn:disabled styling added.
- actions: R3ST4RT / ST0P / ST4RT / R3BU1LD / DESTR0Y / PURG3.
per-action confirm prompt lists the names being acted on.
when the selection includes the manager the ST0P prompt calls
out the consequence (approvals + meta operations pause until
manager is restarted). DESTR0Y/PURG3 stay sub-agent-only;
including the manager disables them with a clear tooltip.
- actions POST per agent in a loop to the existing endpoints
(/restart/{name}, /kill/{name}, etc.); no new backend wire
surface. event-driven derived stores (containersState,
rebuildQueueState) already update live via SSE — no manual
refetch.
- failures from any individual POST surface in a single alert
at the end rather than spamming dialogs mid-loop.
per-card actions removed
- the in-card R3ST4RT / ST0P / ST4RT / R3BU1LD / DESTR0Y / PURG3
block is gone. cards now show identity / state / nav-strip /
status text / drill-ins only — "state and links" per mara.
the contextual needs-update chip in the head row stays.
manager stop
- the "also make manager stoppable" half of #443 ships as PR
#445 (separate small backend change). this PR depends on #445
for the bulk-stop button to actually work on selections
containing the manager; until #445 merges, ST0P on a
manager-included selection will fail individually with a 500
for the manager (other selected agents are still stopped;
failures bucket into the end-of-loop alert).
build clean: npm run build --workspace=@hive/dashboard. no
backend changes here.
PR #392 moved the slug below the tab strip but kept it INSIDE the
sticky chrome — Mara's report ("still at old position everywhere
except flow tab") makes the original intent clearer: the slug
should be at the page BOTTOM, with the chrome reduced to pure
navigation.
## index.html
- Remove the banner-thin from `.dashboard-chrome` entirely.
- Move it into the existing `<footer>` element (above the divider
+ project-link line). Sits at the bottom of every tab pane after
the main content scrolls past.
## flow.html
- Remove the banner-thin from the chrome. The flow page is a
full-viewport terminal with `body { overflow: hidden }` and
no normal-flow footer position — the slug simply doesn't appear
here. The frosted chrome is purely the tab strip now.
## dashboard.css
- `--flow-header-h: 4.7em → 3.6em` — chrome is shorter without
the banner; terminal padding-top + tail-pill offset + inbox-pill
top all derive from this variable, so they follow automatically.
- `footer .banner-thin { margin-bottom: 0.8em }` so the slug
doesn't crash into the divider + link line below it.
No JS changes. Build clean.
Swap the order of `.tabbar` and `.banner` inside `.dashboard-chrome`
so the operator's navigation surface sits at the very top of the
sticky/fixed header — the "WE ARE THE WIRED" slug becomes decoration
below the tabs rather than chrome above them.
Applied to both `index.html` (sticky chrome) and `flow.html`
(fixed-overlay chrome).
No CSS changes — `.tabbar { border-bottom }` still divides tabs from
the area below (now the banner), and active-tab `margin-bottom: -1px`
still merges into that boundary cleanly.
The SW4RM tab label already announces the section — an inline
`<h2>◆ C0NTAINERS ◆</h2>` + divider underneath was redundant
ink. The tab is single-section so there's no other content to
disambiguate from.
`#containers-section` div stays put (app.js targets it by id);
just the heading + divider go.
`npm run build` clean.
Closes#385.
Operator: 'option A (tabs)' (#369#issuecomment-3434) +
'yes terminal can be a separate page' (#369#issuecomment-3437).
## Tab framework
`index.html` becomes a 3-tab dashboard with a sticky chrome header:
- `◆ SW4RM ◆` — containers list (the central thing)
- `◆ Y3R C4LL ◆` — pending approvals + operator-targeted questions
- `◆ SYST3M ◆` — meta inputs + rebuild queue + reminders + tombstones
Hash routing: `#swarm` / `#call` / `#system` (empty → SW4RM).
F5-reloadable + back-button-aware without a router framework.
SSE stays alive across tab switches — count pills on inactive tabs
update live so the operator never loses pulse on what's happening
elsewhere:
- SW4RM: containers with needs_update
- Y3R C4LL: approvals.pending + questions.pending (attn-coloured pill)
- SYST3M: rebuild_queue entries in Queued|Running
Pills hidden when count is zero. setInterval(1s) polls the existing
state stores (cheap, no per-renderer hookup needed).
## FL0W as its own page
The all-agents chat moves to /flow.html — full-viewport vibec0re
layout mirroring the per-agent live page (#362):
- Fixed-overlay frosted-glass header at top (back link + title +
notif controls), backdrop-filter blur shows the scrolled chat
text behind.
- Full-viewport terminal, scroll-padded for the floating chrome so
first/last rows stay reachable.
- Fixed-overlay frosted composer at the bottom.
- Operator inbox surfaces via a pill (📬 inbox · N) in the upper
right — click opens the side-panel flyout with the message list.
In the dashboard tab strip, FL0W is the right-most entry but
renders as a `<a class="tab tab-link" href="/flow.html">` — clicking
navigates to the page rather than swapping a pane. Same pattern
back from flow.html via the `← d4shb04rd` link.
## Implementation notes
- New `/flow.html` page rendered by the same bundled `app.js` — the
flow page just doesn't have the dashboard-chrome DOM, so the
matching renderers no-op silently (each `if (!el) return`).
Avoids splitting the bundle for v1; can extract later if size
becomes a concern.
- `Panel` module gains `openNamed(name, …)` + `refresh(name, …)` —
the legacy untyped `open(title, content)` calls clear the owner,
so file-preview / diff / log drill-ins behave unchanged. `refresh`
is no-op when a different view owns the panel, so live message
events re-render the inbox flyout only when it's actually open.
- `renderInbox` updates BOTH the dashboard's inline `#inbox-section`
(now living on the flow page) AND the flow page's pill count +
side-panel refresh. The dashboard's empty FL0W tab is removed —
inbox + message flow + compose box only exist in flow.html.
- Banner shrinks to a thin Catppuccin gradient strip at the top of
the dashboard chrome (dropped the multi-line ASCII art —
affectionate but pure chrome budget in a tabbed layout).
- `build.mjs` copies both `index.html` + `flow.html` into dist.
## Validation
`npm run build` clean. Dashboard bundle deltas:
app.js 150kb → 152kb (tab routing + count pills + named-Panel)
dashboard.css 33kb → 38kb (tab chrome + flow page layout)
+ dist/flow.html 4.4kb
Browser smoke test isn't possible from inside iris's container
(no JS engine) — drafting as a PR for operator visual review on
next deploy. Worth eyeballing:
- Tab switching feels right; counts update live across SSE events
- FL0W page reads like the agent live page (frosted header + composer)
- Inbox pill opens flyout; live message arrivals refresh it
- Back link from flow → dashboard returns to last tab via the
URL hash (browser remembers the hash across page nav)
Closes#369.
Phase 1 of the backend/frontend code split (#273). Additive — no
existing code is touched; the legacy hive-c0re/assets, hive-ag3nt/
assets and hive-fr0nt/assets trees stay in place until the Rust
cutover later in this branch.
Layout:
frontend/package.json npm workspaces root
frontend/packages/shared/ @hive/shared
src/{base,terminal}.css + terminal.js (ES module)
src/index.js re-exports terminal.js
frontend/packages/dashboard/ @hive/dashboard
src/{index.html, app.js, dashboard.css} ported from hive-c0re/assets
build.mjs esbuild config → dist/
frontend/packages/agent/ @hive/agent
src/{index,stats,screen}.html + agent.css
+ {app,stats}.js ported from hive-ag3nt/assets
build.mjs esbuild config → dist/
Changes vs the existing assets:
- terminal.js is an ES module exporting { create, linkify } instead
of assigning to window.HiveTerminal. The dashboard / agent app.js
files re-expose them on window so the IIFE bodies keep working
unchanged through Phase 1; the global aliases can be dropped in a
follow-up once the IIFEs are unwrapped.
- marked is imported from the marked@4.3.0 npm package (replacing
the vendored hive-fr0nt/assets/marked.umd.js bundle).
- chart.js is imported from chart.js@4.4.4 (replacing the jsDelivr
CDN script tag on the per-agent stats page — page now works
offline / on operator machines without internet egress).
- dashboard.css and agent.css both gain @import lines at the top
that pull base.css + terminal.css from @hive/shared, replacing
the runtime string concatenation in serve_css.
- index.html / stats.html collapse from three / two script tags to
one type="module" tag pointing at the bundled output.
package-lock.json is intentionally omitted from this commit — npm
isn't available in the iris container yet (approval pending) and the
lockfile will land in the next commit on this branch once the
toolchain is in place. The PR will not be opened until it's there.
Phase 2 (nix derivations), Phase 3 (container plumbing + the
hyperhive.frontend.extraFiles option for per-agent layering), and
Phase 4 (Rust cutover to tower_http::ServeDir, delete hive-fr0nt
+ legacy assets dirs) land as follow-up commits on this same
branch.
Refs #273.