Part 1 of the dashboard-event-stream-split epic: the server-side
kinds= allow-list already exists and flow.js already uses it
(hive-c0re/src/dashboard/state_snapshot.rs). tabs.js, builds.js,
core.js, and logs.js were the remaining 4 subscribers still taking
every wire kind unfiltered — pure subscription discipline, no new
endpoint needed, per the investigation on the tracking issue.
Each kinds= list is read directly off that page's own existing
MUTATION_HANDLERS/SSE_HANDLERS dispatch table (tabs.js also needs
sent, checked separately for the operator inbox) — a kind not in a
page's table was already a silent no-op today, so this only removes
wire/parse/dispatch-lookup cost for kinds a page never acted on, zero
behavior change.
Note for reviewers: the SharedWorker (stream-worker.js) multiplexes
by exact URL string, so pages that used to share one unfiltered
upstream connection when open simultaneously (e.g. dashboard.html +
builds.html in two tabs) will now each hold their own filtered
connection instead, since their kinds= differ. Each connection is
still a single cheap SSE stream carrying only what that page acts on
— net win over the shared-but-bloated connection this replaces.
Every sub-page tabbar (logs.html, credentials.html, core.html,
builds.html) hand-wrote the same <nav class="hive-tabbar"><a
class="hive-tab">...</a></nav> boilerplate and then called
createTabStrip() on it after the fact. Add <hive-tab-strip>, a
markup-owning custom element (same reuse-boundary pattern as
<hive-menu>/<hive-side-panel>) that renders that markup from a
declarative tabs list, then wires the existing createTabStrip()
behaviour over what it just rendered — no behaviour duplication.
Convert all four sites to use it: each page's JS now calls
`.configure({ tabs, defaultId, onShow })` on the tabbar element instead
of `createTabStrip(el, opts)`, and configure() returns the identical
{ show, active } shape so nothing downstream changes. builds.js's
rebuild-queue count pill (builds-tab-count-rebuild) is expressed as a
tab's `badgeId` and renders nested in the same spot.
The dashboard's own tabbar and the two no-pane stats time-range
pickers are a different markup/behaviour shape and are intentionally
left alone.
The dashboard has a themed modal/dialog system (modal.js: themedToast/
themedConfirm/themedPrompt) and a data-async form submit interceptor
(bindAsyncForms) that every dashboard action routes through. The
per-agent UI never adopted either — it had its own more primitive
data-async handler using native window.confirm()/alert() (8 call
sites) and a duplicated el() DOM helper.
- Moved el() out of dashboard/common.js into shared/src/dom.js.
- Moved modal.js + modal.css from dashboard/src/ to shared/src/,
updating its internal el import.
- Moved bindAsyncForms from dashboard/common.js into shared/forms.js,
alongside the asyncBtn primitive it's built on.
- Updated every dashboard file's imports to the new shared locations
(no re-export shims).
- agent.css now @imports shared/modal.css so the dialogs render
themed there too.
- agent/app.js: dropped its local el()/data-async duplicate, wired
bindAsyncForms(), and replaced all 8 window.confirm() sites with
themedConfirm (async, wrapped in a fire-and-forget IIFE where the
call site needs a synchronous boolean return, e.g. the slash-command
dispatcher).
Closes hyperhive#2791. Verified with a full frontend build
(npm run build) — both dashboard and agent bundles compile clean and
agent.css picks up the .tc-* dialog styles it previously lacked.
logs.html/logs.js previously showed infra containers (hive-ci, hive-forge,
hive-gateway, hive-matrix) in an optgroup within the AGENT tab selector.
This was confusing because infra containers don't run the per-agent hive
daemons, making the unit filter meaningless for them.
Changes:
- Add INFRA tab (between AGENT and SYSTEM) with its own container selector
and full-machine-journal fetch (no unit filter).
- Remove the infra optgroup from the AGENT tab — it now shows agents only.
- loadContainerLists() replaces loadAgentList(): fetches /api/state once and
populates both selectors, avoiding a duplicate network request.
- Deep-link (?agent=hive-ci) now routes to the INFRA tab when the named
container is an infra container, falling back to AGENT otherwise.
- Remove syncUnitSelectForSelection() — no longer needed since the AGENT
tab no longer contains infra containers.
- Extend the 30s timestamp ticker to cover the INFRA tab fetch time.
No backend changes: /api/journal/{name} already supports infra container names.
Extends GET /api/journal/{name} to also accept the four hive infra
container names (hive-ci, hive-forge, hive-gateway, hive-matrix —
hive_priv_sock::InfraContainer is the allowlist), reusing the same
journalctl -M / hive-priv delegation path already used for agent
containers. Infra containers don't run the per-agent hive daemons, so
the unit filter is skipped for them — always the full machine journal.
Frontend: the AGENT tab's agent selector now lists infra containers
in a separate optgroup (sourced from /api/state's existing
infra_containers field), and disables the unit-filter select when one
is chosen.
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'.
The C0R3 rebuild queue only linked out to the logs page (logs →). Add an
inline live-log panel under the queue that streams the currently-running
rebuild's build output, so the operator watches progress without leaving the
page.
Extract the build-log SSE streaming logic (append stdout/stderr, sticky-bottom
scroll, stderr separator, reconnect-replay reset, done/error handling) into a
shared `openBuildLogStream(id, pre, {onDone, onError})` in common.js, and use it
from BOTH the L0GS page BUILD tab (logs.js, previously inline) and the new C0R3
panel (core.js) — one implementation, no duplication.
The panel is one persistent instance keyed to the running entry's build_log_id
(the queue runs one build at a time), in its own container (#rebuild-live-log)
outside rebuild-queue-section so the queue's per-row re-render — rows rebuild as
the build step advances — never tears down the open stream; it reconnects only
when the running build_log_id changes and won't reopen a stream that already
sent done. Collapsible, live/ok/fail badge, raw download. Hidden when nothing
is building; each row keeps its logs → link for full history.
Frontend-only — no backend change (endpoint + build_log_id already existed).
Closes#1860.
Phase 2 of the dashboard route consolidation. The backend now double-registers
every bare top-level route (approve/deny/kill/restart/start/rebuild/destroy/
update-all/answer-question/cancel-question/purge-tombstone/matrix-account-login/
cancel-reminder/retry-reminder/request-spawn/op-send/meta-update/dashboard-stream/
dashboard-history) at an additional /api/<same> path. Switch every dashboard-pkg
fetch / EventSource / form action to the /api/ form so "backend = /api/*" holds
on the frontend side too.
Bare paths still answer, so this is independently deployable; once it ships the
backend can drop the bare registrations (phase 3). webhook/knowledge stays a
distinct webhook prefix (forge-driven, no SPA caller). app.js's /rebuild,/start
are the agent harness UI (different server) and are untouched.
Part of #1846 (phase 2).
Completes the audit log end-to-end. The backend now emits an
audit_entry_added event on /dashboard/stream per privileged action (the
new row flattened at top level). logs.js already subscribes to that stream
for the BUILD tab; add a branch that prepends the new row to the AUDIT
table's cached list (de-duped by id against the cold fetch), bumps the
'latest N of M' total, and re-renders while the AUDIT tab is in view — so
a restart shows up without a manual refresh. Docs updated to match.
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.
Adds @hive/shared/tabs.js — createTabStrip(tabbar, {defaultId, onShow}):
a hash-routed tab strip that resolves controls by `[data-tab="<id>"]`
inside the passed container and panels by `[data-tab-pane="<id>"]`,
toggling `.hive-tab--active` + aria-selected + the panel's `hidden`, and
firing onShow(id) for per-tab side-effects. Always hash-routed (deep-
linkable + back/forward), no element-resolver callbacks (convention over
config), per the #1464 design review.
Wires it up: @hive/shared exports `./tabs.js` + `./tabs.css`, and
common.css @imports tabs.css (component structure, not a swap target, so
inlining is fine — unlike theme.css).
Migrates the logs sub-tabs as the first consumer: markup uses
`.hive-tab`/`data-tab`/`data-tab-pane`, logs.css drops the duplicated
base styles (keeps only its `flex:1` layout delta), and logs.js swaps its
activeTab/showTab/hashchange for createTabStrip (onShow lazy-loads the
SYSTEM tab). Behaviour-preserving. aria-selected is now standardised.
Per operator request: instead of a disk-specific alert, surface a generic
server-warnings banner at the very top of every page, so new system
warnings can be added backend-side with no frontend change.
- hive-c0re `host_stats`: `server_warnings() -> Vec<ServerWarning>`
(`{ kind, level, message }`). The threshold logic lives server-side; the
host disk-pressure check (a `statvfs` probe of `/nix`: ≥85% used → warn,
≥95% → crit) is the first and only producer today. No new deps (libc).
- `/api/state` carries `server_warnings` (replaces the disk-specific
field). Empty when all clear.
- frontend: `renderServerWarnings` / `initServerWarnings` in `common.js`
inject a sticky top-of-<body> banner and render the list, coloured by
`level`. Wired on every page — dashboard (live, via refreshState),
FL0W, L0GS, H0M3. No per-warning frontend code; adding a warning kind
is a pure backend change.
cargo check/clippy/fmt + npm run build green. Closes#1518.
Adds ?agent=name and ?unit=svc URL params to pre-select the agent on
the AGENT tab (used by the per-agent overflow menu journal logs link).
Last-fetched timestamp chip shows how stale the output is.
Agent selection is NOT persisted to localStorage — URL params only.
Three UX improvements for the live build log viewer:
1. Auto-scroll (sticky-bottom): live build output now scrolls to the
bottom as new lines arrive. Stops auto-scrolling when the operator
manually scrolls up; resumes when they scroll back to the bottom.
Same intent-tracking pattern used by the terminal pane.
2. Elapsed-time ticker: running builds show a live seconds/minutes
counter in the row header that ticks every second. Stops and shows
the final duration when the build finishes (done frame received)
or errors out.
3. SSE-driven list refresh: subscribes to /dashboard/stream and
debounces a fetchBuild() call (2s) whenever rebuild_queue_changed
fires while the BUILD tab is active. New log entries and status
changes appear without a manual refresh.
Same cleanup as the previous pass (tabs.js, app.js): replace
innerHTML = '' with replaceChildren() and lowercase the one
uppercase 'HTTP' error in logs.js.
- live detection: use !h.status (null while running) not === 'running'
- SSE: replace named addEventListener('chunk'/'done') with onmessage +
frame.done check — backend sends generic message events, no named type
- SSE payload: use frame.stdout_append/stderr_append not frame.text
- timestamps: started_at/finished_at are unix seconds, multiply by 1000
was already done implicitly — actually use unix arithmetic directly
to avoid new Date() confusion entirely
Also move the misplaced .flow-main-slim rule from the logs section to
the flow-shell section of dashboard.css.
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)