Closes#2965.
hive-c0re auto-generates an OpenAPI 3 spec via utoipa
(hive-c0re/src/dashboard/mod.rs's ApiDoc), served raw at
/api/openapi.json and browsable as a Swagger UI at /api/docs, but
docs/ never mentioned either — genuinely zero hits grepping the whole
docs/ tree. Documented both in docs/web-ui/dashboard.md's Dashboard
endpoints + H0M3 page sections.
Also added the H0M3 hub's API tile mara suggested ("maybe also add
home page app that opens swagger ui") -- a plain static link to
/api/docs, no gating needed since the endpoint always exists
(unlike Forge/Matrix, which are conditionally enabled).
Adds hyperhive_rev to the dashboard's /api/state StateSnapshot,
resolved via the same current_flake_rev helper get_agent_meta's
per-agent hyperhive_rev already uses. home.js renders it next to the
existing hive-identity line, truncated to the last 12 chars with the
full value in title=, hidden when the flake ref isn't a local path pin.
Requested by annika (infra.run) via dmatrix, hyperhive#2854.
Part 2 of hyperhive#1970 (backend/CLI landed in PR #2378). Renames
/matrix-accounts.html -> /credentials.html and restructures it with a
sub-tab strip (MATRIX / GITHUB), reusing the shared @hive/shared/tabs.js
tab strip already used by /logs.html.
MATRIX tab carries over the existing account list + login form
unchanged. GITHUB tab adds a single-PAT provisioning form: status line
(present/absent, read from GET /api/github-account), a security-warning
banner (dedicated bot account + minimally-scoped token), a link to
generate a PAT at github.com/settings/tokens, and a paste-token form
posting to POST /api/github-account. Both tabs share one agent picker.
Updated docs/web-ui.md + docs/web-ui/dashboard.md to describe the new
page shape, and the H0M3 hub tile (index.html) to point at the renamed
page.
New standalone page /builds.html consolidating rebuild queue, meta
inputs, and build log history into one place, with a new 'Builds' home
tile linking to it. Addresses mara's request: new sub-page with a new
home tile, all three items moved there.
Changes:
- builds.html: new page with three sub-tabs: R3BU1LD QU3U3, M3T4 1NPUTS,
BUILD L0GS. Same minimal-chrome header + createTabStrip pattern as core
and logs pages.
- builds.js: new bundle combining rebuild queue renderer (from core.js),
meta inputs renderer (from core.js), rebuild-live-log renderer (from
core.js), and build log history renderer (from logs.js). Deep-links to
/builds.html?id=N#buildlogs. Count pill id: builds-tab-count-rebuild.
BUILD L0GS tab lazy-loads on first activation.
- builds.css: @imports system-sections.css (rebuild queue + meta inputs +
live-log styles) and logs.css (build-logs-* component styles).
- build.mjs: register builds.js, builds.css, builds.html.
- core.html: remove R3BU1LD QU3U3 + M3T4 1NPUTS tabs (now on builds.html).
Default tab changes to K3PT ST4T3.
- core.js: remove renderMetaInputs, renderRebuildQueue + helpers,
renderRebuildLiveLog + live-log state, elapsed-time tickers,
updateRebuildCount, and the rebuild_queue/meta_inputs SSE handlers.
Remove openBuildLogStream + util imports no longer needed.
- core.css: remove .rebuild-live-log-* rules (moved to system-sections.css
so builds.css can share them via @import).
- system-sections.css: add .rebuild-live-log-* styles (moved from core.css);
update comment to mention builds.html.
- logs.html: remove BUILD tab + pane (moved to builds.html).
- logs.js: remove fetchBuild(), fmtTs, fmtDuration, openBuildLogStream
import, and rebuild_queue_changed SSE debounce. Default tab: 'agent'.
SSE stream retained for audit_entry_added live-appends.
- index.html: add Builds tile (🔨, rebuild queue · meta inputs · build
logs); update Core tile desc to 'kept state · container load'; update
Logs tile desc to remove 'build'.
Adds the falling-glyph "digital rain" effect the operator asked for as a
dimmed background on the home hub.
- index.html: a decorative full-viewport <canvas id="matrix-rain">
(aria-hidden, pointer-events:none) as the first body child.
- home.css: fixes it behind the content (z-index:-1, inset:0) at low
opacity (0.16) so the menu tiles stay legible — the "dimmed bg".
- home.js: startMatrixRain() drives the classic stepped digital rain.
Glyph + fade-trail colours are RESOLVED FROM THE STYLIX PALETTE at
runtime (--green glyphs, --bg trail; resolved via a probe element
since getComputedStyle returns custom props unresolved), so a theme
swap re-colours it rather than hardcoding matrix-green. Stepped via a
55ms interval, paused while the tab is hidden, and skipped entirely
under prefers-reduced-motion.
CSS/JS only, home page only.
New standalone H0M3 page (/matrix-accounts.html) and tile. An agent
picker drives a list of that agent's configured matrix accounts (name,
homeserver, token-stored status) and a provision form that logs in by
password or stores an existing token.
Frontend half of the per-agent external matrix-account provisioning
work. Built against the v1 backend contract:
GET /matrix-accounts?agent=<name>
-> { accounts: [ { name, homeserver, token_present } ] }
POST /matrix-account-login (x-www-form-urlencoded, operator-auth)
fields: agent, account, homeserver, mode=password|token,
user_id?, password?, token?
-> 2xx { ok, user_id } | 4xx { error }
The token is never echoed back; secret inputs are cleared on submit.
Token-status dot reflects token-stored, not live session (a true
up/down indicator needs the daemon account registry, a follow-up). The
form carries an experimental notice pending per-account failure
isolation on the matrix daemon.
Blocked from merge on the backend endpoints and the daemon
failure-isolation fix; opening for review + to pin the UI/backend wire
contract.
Adds an AUDIT sub-tab to /logs.html (alongside BUILD / AGENT / SYSTEM),
consuming GET /api/audit-log ({ entries, total }). A read-only filterable
table: when / agent / action / target / outcome / detail, newest-first,
with a 'latest 500 of N' header from total and a client-side substring
filter. Outcome badges colour ok green / err red, with an err whose detail
starts 'denied:' rendered amber + labelled 'denied' (capability refusal
reads distinct from an execution failure). Lazy-fetched on tab show (like
SYSTEM); a 30s ticker keeps the relative timestamps honest.
The audit_log store + endpoint landed in the prior audit-log backend work;
this is the operator-visible surface for it. Resolves#1647.
Carves the dashboard's SYST3M tab content out into a standalone page at
/core.html, reached from the H0M3 hub, with a createTabStrip sub-tab
strip (default = Rebuild Queue): rebuild queue, meta inputs, kept state,
container load. Same minimal-chrome standalone-page pattern as
/logs.html (← home back-link + sub-tab nav).
This is the additive first step: the new page is its own esbuild bundle
that cold-loads /api/state and subscribes to /dashboard/stream for the
same live rebuild_queue_changed / meta_inputs_changed / meta_update_running
/ tombstones_changed events the dashboard uses. The four section
renderers are ported from tabs.js; the dashboard's SYST3M tab is left in
place and untouched, so this PR cannot regress the existing dashboard.
Removing the now-duplicate SYST3M tab + de-duplicating the renderers
(into a shared module) + splitting the shared section CSS out of
dashboard.css is the deliberate follow-up.
core.css imports dashboard.css wholesale (transitional) so the ported
sections render identically; dashboard.css does not import common.css so
nothing double-loads. build.mjs gains core.js / core.css / core.html
entries; no Rust change (hive-c0re serves dist/ via ServeDir, and the
/dashboard/stream + /dashboard/history routes are registered ahead of
the fallback).
Page name "C0R3" is a placeholder pending the operator's pick — trivially
renamed (the /core.html URL + the C0R3 label).
H0M3 had tiles for every operator surface except the forge. Add a
Forge tile that links straight to the hive-forge web UI. It mirrors
the Matrix tile's gating: hidden by default, revealed by home.js only
when state.forge_present is true, with the href filled from
state.forge_public_url (the gateway-served vhost) or the direct :3000
port fallback — the same precedence the dashboard uses for forge links.
Operators without a forge never see a dead link.
Each home-hub tile now leads with an icon next to its label: 🖥 Dashboard,
📡 Flow, 📜 Logs, 📊 Stats, ⚙ Settings, 💬 Matrix. Matches the existing
emoji nav-icon convention (the per-agent link strip uses 📊/🖥/⬡/↳).
Markup: the label is wrapped with the icon in a .home-tile-head flex row
(icon + label on one line, description below). The icon is aria-hidden —
decorative, the label carries the accessible name. CSS-only layout
addition; no behaviour change. Build green.
Follow-up from #1532. Previously `body.dashboard-shell` / `body.home-shell`
carried the 1.5em horizontal gutter, and full-width chrome broke out of it
with negative margins (`.dashboard-chrome { margin: 0 -1.5em }`, the
`#server-warnings { margin: 0 -1.5em }` override).
Invert it: `<body>` is now full-bleed, and the padded page content lives in
a single inner `.page-content` wrapper that carries the gutter. The
server-warnings banner, the sticky tab strip, and the footer then span the
full width for free — so the `-1.5em` breakout hacks are gone.
- `.page-content { padding: 0 1.5em }` is a shared primitive in common.css
(the dashboard + H0M3 both opt in by wrapping their content; FL0W / L0GS /
ST4TS / S3TT1NGS stay full-bleed with their own `.<page>-main` padding).
- dashboard.html / index.html wrap their content in `.page-content`.
- dashboard.css / home.css: drop the body horizontal gutter (keep
`padding-bottom` for foot breathing room); `.dashboard-chrome` margin
`0 -1.5em 1em` → `0 0 1em`.
- common.css: drop the `#server-warnings` breakout override.
Behaviour/visual-neutral by intent (content gutter unchanged; chrome +
banner already rendered full-width via the old breakout). Worth a gui
screenshot-diff to confirm — esp. the now-full-width footer divider.
Last fold-in of #1464 step 2.
Extracts the hive-wide turn-stats rollup out of the dashboard tab strip
into a standalone /stats.html page, reached from the H0M3 hub — same
minimal-chrome pattern as /flow.html and /logs.html. The dashboard tab
strip is now purely operational.
- new stats.{html,css,js}; stats.js holds the moved render JS and
fetches /api/stats-hive on load + window change.
- migrate the window selector (#hive-stats-windows) from a bespoke
data-w/.active toggle to the shared createTabStrip — now hash-routed
(#1h / #24h / …) and deep-linkable, matching the per-agent /stats page.
- drop the ST4TS tab + pane from dashboard.html and the hive-stats render
block + the stats->refreshHiveStats lazy-load from tabs.js.
- move the shared .hive-stats-table to common.css (the dashboard SYST3M >
C0NT41N3R L04D table still uses it); the ST4TS-only window/chip/bar
styles go to stats.css.
- add a Stats tile to the H0M3 hub (and drop the now-stale "stats" from
the Dashboard tile desc); wire build.mjs + the nix/frontend.nix manifest.
Second slice of #1464 step 2; follows the /settings.html extraction.
Extracts the operator-local preferences (browser-notification toggle)
out of the dashboard tab strip into a standalone /settings.html page,
reached from the H0M3 hub — same minimal-chrome pattern as /flow.html
and /logs.html. The dashboard tab strip is now purely operational.
- new settings.{html,css,js}; settings.js binds NOTIF + renders the
shared server-warnings banner (matching the other standalone pages).
- drop the S3TT1NGS tab + pane from dashboard.html; the NOTIF.bind()
toggle wiring moves to settings.js. The dashboard keeps NOTIF.show()
(approvals / questions) — it reads the same browser permission +
localStorage mute flag the settings page sets, so firing still works.
- move .notif-row / .btn-notif from dashboard.css to settings.css.
- add a Settings tile to the H0M3 hub; wire the new entries into
build.mjs and update the nix/frontend.nix asset manifest comment.
First slice of #1464 step 2; the ST4TS page extraction follows separately.
Split the palette into two standalone stylesheets:
- colors.css — the 16 base16 slots (--base00..--base0F). This is the
entire theme swap contract; a generator (e.g. a stylix base16 scheme,
which is natively base00-base0F) replaces only this file.
- theme.css — the semantic layer (--bg, --purple, …) derived from the
base16 slots via var()/color-mix. Never changes on a swap.
Every page links colors.css then theme.css; theme.css does NOT @import
colors.css (that would re-bake the slots into it) — they're separate
dist outputs so a swap touches just colors.css. Pixel-identical refactor:
base16 defaults are Catppuccin Mocha and the three off-slot vars
(--crust, --muted, --subtext0) derive via color-mix reproducing their
exact prior hexes.
Wires colors.css through both build.mjs CSS entry lists, the
@hive/shared exports map, and all 7 page templates. css-vars.md + the
frontend.nix output-list comment updated.
Step 2 of the nav restructure: make the H0M3 menu hub the landing page
at / and move the dashboard SPA to /dashboard.html, with every surface
linking back to the hub.
Mechanism (frontend-only, no host-side change — confirmed against the
ServeDir route table): the dashboard's ServeDir fallback serves
index.html at / via append_index_html_on_directories, and there is no
SPA path-routing catch-all to break. The dashboard SPA is served as the
plain file dashboard.html so it never shadows the exact-match
/dashboard/stream + /dashboard/history SSE routes registered before the
fallback.
- Swap the HTML entry files: the H0M3 page becomes index.html (loads
home.js → served at /), and the dashboard SPA becomes dashboard.html
(loads tabs.js → served at /dashboard.html). build.mjs copies the new
set; JS bundle names are unchanged (referenced by absolute /static/
paths, independent of the HTML filename).
- H0M3 Dashboard tile now points at /dashboard.html.
- The dashboard gains a "← home" back-link in its chrome; flow.html and
logs.html relabel their back-link from "← dashboard" to "← home"
(href stays / — which is the hub now). Pages link to the hub, not to
each other.
- Agent page (app.js + stats.js): the "↑ dashboard" link now targets
/dashboard.html. The API base (rebuild / answer-question /
mark-all-read POSTs) stays the origin root, unchanged.
- Comment-only: tabs.js / flow.js / common.js references to the
dashboard's old index.html filename updated to dashboard.html.
Note for review: git renders the file swap as a deleted home.html + an
added dashboard.html + a heavily-modified index.html, because index.html
exists on both sides with swapped content. It's a content swap, not a
rewrite — the built dist/ is verified (index.html→home.js, dashboard.html
→tabs.js).
Deferred to a follow-up: removing the FL0W / L0GS / M4TR1X "→" page-links
from the dashboard tab strip (touches tabs.js gating/overflow), and the
shared reusable chrome component (a later step).
Agents that `send(to: "operator")` were easy to miss — they only
surfaced on the FL0W firehose with no read-state (#1469). Surface them
on the Y3R C4LL ("things waiting on you") tab as a proper inbox.
Backend:
- broker: `unread_for_recipient(recipient, limit)` — unacked messages
for a recipient, newest-first. Mirrors `mark_all_read`'s filter
EXACTLY (`recipient = ?1 AND acked_at IS NULL`, no `delivered_at`
condition) so everything listed is exactly what mark-read clears —
operator rows never get `delivered_at` set (no agent-socket recv).
- dashboard: `GET /api/operator-inbox` → `{ messages: [...] }` (id,
from, body, at, in_reply_to, validated file_refs). Mark-read reuses
the existing `POST /api/agent/operator/mark-all-read` (the route
format-validates the name; "operator" passes; `mark_all_read`
already acks `to="operator"` rows).
Frontend (Y3R C4LL):
- New ◆ 1NB0X ◆ section listing unread messages (sender · time · body,
path-linkified) + a "✓ mark all read" button.
- Cold-loaded on page load + on tab activation; appended live from the
broker `sent` stream (deduped on row id); cleared on mark-all-read.
- Unread count folds into the Y3R C4LL tab pill + the browser-title
`(N)` prefix, so messages are visible from any tab.
Removing the now-redundant FL0W operator-inbox UI is a clean follow-up
(deferred to avoid a flow.js conflict with the in-flight #1473).
Backend (broker + route) is host-side — @damocles to review per plan.
Closes#1469.
Phase 1 of stylix integration (#1457): extract the Catppuccin palette
into a dedicated, separately-linked stylesheet so a theme swap can
replace just that file without rebuilding the rest of the frontend.
- shared/src/theme.css (new): the `:root` palette, moved out of
base.css (which now holds only the body typography it references).
- shared/package.json: export `./theme.css`.
- dashboard/src/theme.css + agent/src/theme.css (new): one-line
re-exports of @hive/shared/theme.css so each package's esbuild emits
its own standalone `dist/static/theme.css` (palette is NOT inlined
into the page bundles).
- both build.mjs: add theme.css to the CSS build list.
- every page (dashboard index/flow/logs, agent index/stats/screen):
link `theme.css` first, ahead of the page CSS, so the `:root` vars
resolve for everything.
- docs/web-ui/css-vars.md: document the split + the no-rebuild rationale.
Behaviour-neutral — same colours, just relocated. Verified both
`npm run build` outputs: theme.css emits standalone (383b) with the
palette; no `--*` palette defs duplicated into common.css/agent.css.
Phase 2 (nix derivation that swaps theme.css from stylix colours) is a
follow-up; touches nix/frontend.nix, coordinating with damocles.
Part of #1457.
C0NT41N3R L04D on the SYST3M tab + GET /api/container-resources.
Backend (hive-c0re/src/container_stats.rs): reads cgroup v2 cpu.stat +
memory.{current,peak,max} for each running agent machine
(machine-h\x2d<name>.scope under machine.slice), read-only/world-
readable so no hive-priv. CPU is a two-sample (~200ms) host-normalised
percentage; one shared sleep covers all agents. Skips agents whose
scope dir is absent (= not running). Network omitted: agents share the
host netns, no per-container counter.
Frontend: a polled C0NT41N3R L04D table on SYST3M (agent / cpu / mem /
peak / limit with meter bars), reusing the ST4TS table style. Polls
/api/container-resources every 5s only while the tab is active.
Backend reviewed-in-principle by damocles (path escaping + cpu delta
math); ping for the on-host sign-off.
Builds the dashboard UI on the /api/stats-hive endpoint: a new ST4TS
tab showing swarm totals (active agents, turns, tokens, labelled est
cost), a busiest-first per-agent table, and a model-mix bar list.
Plain tables/CSS bars — no chart lib in the dashboard bundle; per-agent
trend charts stay on each agent's own /stats page. Fetched on tab
activation + window change (pull-only, no SSE).
Also adds conn.busy_timeout(500ms) in hive_stats read_agent per review:
turn_stats is rollback-journal, so a read landing mid-INSERT would hit
SQLITE_BUSY and silently drop that active agent — wait the write out.
- activateTab('schedules') now calls both refreshSchedules() and
refreshReminders() since both sections live on the same tab.
(The previous SYST3M/system target was wrong.)
- Update index.html comment to reflect schedules_changed SSE coverage
- Update index.html reminders comment to mention reminders_changed SSE
- Update tabs.js reminders section comment to reflect SSE coverage
Without a viewport meta tag, some browsers (notably Firefox with
Fingerprinting Protection active) internally read screen.availWidth /
screen.availHeight to compute the default viewport size, producing the
console warning 'Fingerprinting Protection is altering screen.availWidth
and screen.availHeight'.
Fix:
- Add <meta name="viewport" content="width=device-width, initial-scale=1">
to all five pages that were missing it (dashboard index/flow/logs, agent
index/stats). screen.html already had it.
- Replace window.innerWidth with document.documentElement.clientWidth in
the side-panel drag-resize code in common.js. clientWidth returns the
actual CSS layout viewport width and is not rounded by Firefox's
Fingerprinting Protection, making the drag calculation correct even
with privacy.resistFingerprinting enabled.
Add a new P3RM1SS10NS tab to the dashboard that consolidates all
per-agent permission configuration:
Backend:
- GET /api/capabilities returns { caps: [...], assignments: {...} }
driven by Capability::ALL variants (manage_root_agent,
read_host_journal, query_agent_state)
- POST /api/capabilities/{agent} writes capabilities.json and queues
a rebuild so HIVE_CAPABILITIES takes effect
Frontend:
- New 'permissions' entry in TABS, placed after 'system'
- P3RM1SS10NS tab pane with two sections:
C4P4B1L1T13S — agents × capabilities checkbox matrix (.cap-*)
T00L GR0UPS — agents × tool-groups checkbox matrix (.tg-*) moved
from SYST3M tab
- activateTab('permissions') fetches both tables; neither has an SSE
channel so they re-fetch on each activation to stay fresh
- CSS for .cap-* mirrors the .tg-* layout (scrollable, Catppuccin)
Section heading, element id, CSS classes, and JS functions all renamed
from 'capabilities'/'cap-*' to 'tool-groups'/'tg-*' to accurately
describe what the UI manages (tool-group permissions, not a capabilities
system).
Backend (hive-c0re/src/dashboard.rs):
GET /api/tool-groups — returns { groups: [...], assignments: {...} };
groups list comes from ToolGroup::ALL so the UI needs no change when
a new group is added (satisfies the 'no extend ui' requirement)
POST /api/tool-groups/{agent} — accepts { groups: [...] }, calls
set_groups() then enqueues a rebuild so the new HIVE_TOOL_GROUPS
env var takes effect immediately
hive-sh4re/src/lib.rs:
Added ToolGroup::ALL const (ordered slice of every group)
Added ToolGroup::as_str() — snake_case wire name, matches serde
Frontend:
SYST3M tab: new C4P4B1L1T13S section above K3PT ST4T3 with
#capabilities-section placeholder
tabs.js: fetchAndRenderCapabilities() + renderCapabilities() —
columns are built from the groups array returned by the API;
each row has one checkbox per group and a save button that POSTs
and re-fetches after 800ms; agents without explicit assignments
show a (default) label; triggered on each SYST3M tab activation
dashboard.css: .cap-table-wrap/.cap-table/.cap-row/.cap-agent-*
styles for the scrollable matrix table
Add a ⋮ overflow button at the right end of the dashboard tab strip.
SETTINGS and LOGS always live in the overflow dropdown (marked
data-overflow="default" in index.html — never rendered in the main bar).
Dynamic overflow: when the bar is too narrow to fit all remaining tabs,
rightmost tabs spill into the dropdown right-to-left. Implemented via
JS measurement + ResizeObserver; no CSS-only relayout trick needed.
The ⋮ button:
- hidden when the dropdown is empty (wide screens with only the default
tabs overflowed, which are always there anyway → button always shown)
- gets .tabbar-overflow-active when the current hash tab is inside
- dropdown items clone the tab label + count badge from the original
- closes on outside click / Escape
MutationObserver on the tabbar catches P33RS/M4TR1X hidden-attribute
changes so the overflow recalculates when those tabs are shown/hidden.
Add /logs.html as a standalone page (same back-link pattern as flow.html):
- BUILD tab: all-agents build log history via new GET /api/build-logs endpoint
- AGENT tab: per-container journald viewer with agent selector + unit filter
- SYSTEM tab: host-side hive-c0re.service logs via new GET /api/journal-host endpoint
Remove inline log drill-ins from SW4RM container rows (buildJournalTrigger
and buildBuildLogsTrigger) — log viewing now lives on the dedicated page.
flow.html: strip the full dashboard tabbar, replace with a simple back link
matching the new logs page chrome.
index.html: add L0GS tab link to /logs.html in the tab strip.
Backend additions:
- build_logs::list_recent_all — cross-agent query (newest first, cap 100)
- GET /api/build-logs — all-agents variant backed by list_recent_all
- GET /api/journal-host — host journald (no -M container flag), restricted
to allow-listed units (hive-c0re.service)
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.