fix(dashboard): update 'view queue' link to /builds.html
The build queue moved to its own /builds.html page. The queue-summary
'view queue →' link in tabs.js still pointed at /core.html, so clicking
it landed on the wrong page.
fix(broker): filter agent inbox to unread (acked_at IS NULL)
recent_for was returning all messages regardless of ack state, so the
agent inbox showed everything even after 'mark all read'. Now filters
to acked_at IS NULL — mirroring exactly what mark_all_read drains —
so the inbox empties on reload after the operator drains it.
Remove or shorten comment blocks that:
- explain where code used to live (impl history)
- duplicate rationale already in docs/web-ui.md
- contain speculative/future-work notes
Changes:
- tabs.js: drop "SYST3M tab used to render this" history; keep badge purpose
- schedules.js: drop "used to be a card layout" history + speculative nit note
- stream-worker.js: replace 10-line problem-statement with docs pointer
(rationale already in docs/web-ui.md SSE multiplexing section); trim
future-work parenthetical from subscription-tracking comment
- system-sections.css: drop extraction-history prose; keep what it does
- core.css: drop move-history prose; keep what it imports and why
Part of issue cleanup per operator feedback.
Read the persisted model name from each agent's harness state file
(harness/hyperhive-model) and surface it as a small blue badge on
the container row in the SW4RM tab.
- container_view.rs: add `active_model: Option<String>` to
ContainerView; populated by new `read_active_model` helper that
reads harness/hyperhive-model; only set when container is running
(stale model info from a stopped agent is misleading)
- container_view.rs: add active_model to ContainerView literal in
host_stats test helper
- tabs.js: render badge-model chip after needs-update, before
reminders; add active_model to the row fingerprint so re-renders
fire on model change
- common.css: add .badge-model (blue, 80% opacity — informational)
Follow-up to the destroy-guard removal — the bootstrap/root container is
now destroyable end-to-end. The frontend already offered DESTR0Y/PURG3
for every container; the only manager-gating left was a stale doc-comment
('destroy/purge hidden for the manager') and a vestigial bare block
wrapping the menu appends. Drop both (no behaviour change), and update the
dashboard.md menu doc: 'disabled for the bootstrap container' -> available
for any container (hive-c0re recreates it on next startup if destroyed).
The kill endpoint deserializes ?graceful as a strict bool (true/false),
so the dashboard's ?graceful=1 failed with 'provided string was not
true or false' and graceful shutdown silently broke. The stop and
bulk-stop buttons build the param at two call sites in tabs.js; send
graceful=true instead (a hard kill still omits the param entirely).
Keeps the API contract strict — the FE one-liner damocles and I agreed
on, superseding the lax-bool deserializer in the closed#1917.
Fixes#1914.
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).
applyQuestionAdded / applyQuestionResolved (call.js) called
renderContainersFromState() to refresh the SW4RM rows' per-agent
question-count badges, but that's a tabs.js closure-local not in call.js's
scope — so the question_added / question_resolved SSE handlers threw
'ReferenceError: renderContainersFromState is not defined' and aborted.
Inject it as an onContainersDirty callback via initCall (same pattern as
onCountsChanged), wired by tabs.js to renderContainersFromState. Closes#1826.
The SW4RM tab rendered a per-transient spinner list above the container
list, duplicating the running-step badge already shown on each agent card.
Replace it with a single compact amber banner that appears when the rebuild
queue has active (queued/running) entries — 'build queue — N running / M
queued — view queue →', linking to the full queue on /core.html. Per-agent
detail stays on the cards; the top of the tab just gives the at-a-glance
summary + a jump to the queue. Closes#1817.
Frontend half of #1806 (batch start has no visible running-action feedback).
damocles is routing dashboard start/hard-stop through the rebuild queue as
QueueKind::Start ('start') / Stop ('stop') so they get the same async
queued->running card progression as restart/rebuild/graceful-stop (the sync
transient_guard flash is too brief to see, esp. in a sequential bulk loop).
This adds the row-renderer label cases: 'start' -> starting/start queued,
'stop' -> stopping/stop queued (mirrors the graceful_stop case from #1791).
Forward-compatible: no-op until the backend emits those kinds. Doc updated.
#1785 routes a graceful stop through the rebuild queue as a GracefulStop
entry (wire kind 'graceful_stop'). The SW4RM container-row badge renderer
didn't recognise that kind, so an in-flight graceful stop fell through to
the generic 'rebuilding' / 'rebuild queued' label. Add the case so the card
shows 'stopping…' (running) / 'stop queued' (queued), giving the graceful
stop the same live card progress as a rebuild. Doc the badge kind too.
The bulk-stop / M0V3 failure summaries list which agents failed; as
auto-dismissing toasts an operator could miss a partial failure after
navigating away. Make just those two summaries sticky (duration 0,
click-to-dismiss); transient single-action errors keep auto-dismiss.
Follow-up to the themed-modal component: route every remaining native
browser dialog through modal.js so nothing falls back to the OS chrome.
- modal.js: add themedPrompt (input dialog) + themedToast (non-blocking
transient notification, info/error/ok) alongside openDialog/themedConfirm.
- Migrate call sites: bindAsyncForms confirm/prompt/alerts (common.js),
the answer-validation alert (call.js), the M0V3 reparent confirm + action
toasts (tabs.js), and the schedule form/cancel/fire confirms + validation
and error alerts (schedules.js).
- UX: blocking modal for confirms/prompts; non-blocking toast for transient
errors + validation. Destructive confirms keep the danger styling.
- CSS for the toast stack + prompt input.
common.js <-> modal.js is a safe deferred import cycle (usage is call-time
only); esbuild bundles it clean.
Move the themed dialog out of common.js into its own modal.js module: a
general openDialog(title/message/content/buttons) primitive with themedConfirm
as a thin cancel/confirm wrapper on top. tabs.js imports it from there. No
behaviour change to the stop-confirm flow; the dialog is now a standalone
reusable component other surfaces can open.
Replace the browser-native confirm() on the agent ST0P actions (per-agent
menu + bulk) with an in-theme modal (themedConfirm in common.js), carrying a
'stop gracefully' checkbox. Checked sends POST /kill/<name>?graceful=1 (the
quiesce path); unchecked is today's immediate hard stop, unchanged. The modal
also covers the other destructive menu actions (restart / rebuild / destroy /
purge) so they no longer fall back to the OS dialog.
The agent self-reported status rendered on one line with
white-space:nowrap + text-overflow:ellipsis. Because the "(set N ago)"
stamp trailed the text in the same clipped flow, a long status clipped
the set-time away entirely. Wrap the icon + message in a .status-msg
span clamped to two lines and make the age a non-shrinking flex sibling,
so the message wraps to two lines (then ellipsizes) and the set-time is
always shown.
Replace the per-row save buttons on the P3RM1SS10NS tab with one
page-level "save all (N agents)" button covering both the capabilities
and tool-groups matrices.
Toggling cells stages edits in the DOM; each checkbox carries its
render-time baseline (data-baseline). On save we diff against the
baseline and POST only the perm-types that actually changed per agent
to the new combined endpoint:
POST /api/permissions { changes: [ { agent, tool_groups?, capabilities? } ] }
An omitted field leaves that perm-type untouched (no commit, no diff);
an included array fully replaces it. The backend coalesces caps + groups
for one agent into a single rebuild — no more double-rebuild when an
operator changes both for the same agent. The batch is atomic: on a
clean 200 the bar shows queued -> rebuilding and re-fetches (resetting
baselines); on any validation error nothing is applied and an error note
is shown.
Live capabilities_changed / tool_groups_changed re-renders are skipped
while the section has unsaved edits so a half-finished edit set is not
clobbered; the tab-activation and post-save re-fetches are the recovery
paths.
Pairs with the hive-c0re combined-PermPayload half (damocles).
Final domain: move the operator question queue (questionRowCache, the
sync/apply/filter/fingerprint/buildQuestionLi/renderQuestions fns, formatTtl,
and the .q-ttl countdown ticker) from tabs.js into call.js. tabs.js imports
syncQuestionsFromSnapshot, applyQuestionAdded, applyQuestionResolved,
renderQuestions, and the activeQuestionCount getter; refreshTabCounts now
sums activeApprovalCount()+activeQuestionCount()+operatorInboxCount().
The .status-age (SW4RM) and .approval-ts/.reminder-due/.sched-due
(approvals+schedules) tickers stay in tabs.js — only the questions-specific
.q-ttl ticker moved. questionsState stays in state.js (shared with the SW4RM
badges). Behaviour-preserving; no visual change.
Net: tabs.js 2323 to 1603 lines; the Y3R C4LL pane now lives in call.js,
alongside permissions.js / schedules.js as a per-domain module.
Begin splitting the tabs.js monolith: lift the Y3R C4LL domain into a new
call.js module (mirrors permissions.js / schedules.js).
- questionsState + QUESTION_HISTORY_LIMIT move to state.js: they're read by
both the SW4RM container rows (per-agent question-count badges) and the
Y3R C4LL questions domain, so they're cross-domain shared state.
- operator-inbox domain (state + refresh/render/mark/append) → call.js.
- approvals domain (state + sync/apply/render/diff-panel/history) → call.js.
- call.js exposes count getters (activeApprovalCount, operatorInboxCount);
the entry's refreshTabCounts pulls them. Live-mutation paths call an
injected onCountsChanged callback (registered via initCall at boot)
instead of reaching into the coordinator (avoids a circular import).
- renderPeerHives, physically interleaved in the moved range but part of
the SW4RM/peers domain, stays in tabs.js.
Behaviour-preserving; no visual change. Questions domain follows next.
(Recreated after a harness-restart git-object corruption; identical content.)
Clicking "update & rebuild" (and cancel / respawn / purge) on /core.html
navigated to the bare `ok` response page instead of submitting async. The
C0R3 page (split out of the dashboard) carries `data-async` forms but its
bundle never had the global submit interceptor — that handler lived inline
in tabs.js, so only the dashboard bundle had it. The forms POSTed natively
and the browser followed the response.
Fix: lift the `data-async` submit handler out of tabs.js into a shared
`bindAsyncForms(onSuccess)` in common.js (which already owns the `form`
helper that builds these forms), and call it from both pages:
- tabs.js: `bindAsyncForms(() => refreshState())` — behaviour-preserving
(same handler, now imported).
- core.js: add a `refreshState()` (re-fetch /api/state + re-render) used
for the cold load and as the post-submit refresh, and call
`bindAsyncForms(() => refreshState())` at boot.
Forms with `data-no-refresh` (e.g. meta-update, which gets its update via
the meta SSE events) skip the refresh, same as before.
Folds the SYST3M-tab removal into the same change that adds /core.html
(per operator review — it should be one PR, not an additive page + a
follow-up). Drops the tab + pane from dashboard.html and ~600 lines of
now-dead renderers/wiring from tabs.js: renderMetaInputs, renderRebuildQueue
+ renderQueueEntry + the rebuild-queue row cache / glyph maps / fingerprint,
renderTombstones, the container-load poll (cload* + start/stopContainerLoadPolling),
the two rqe-* elapsed tickers, the meta/tombstone state + sync + apply
handlers, their SSE dispatch entries, and the SYST3M tab-count.
Deliberately KEPT in tabs.js: rebuildQueueState + syncRebuildQueueFromSnapshot
+ applyRebuildQueueChanged + inFlightOpsByAgent — these don't render the
SYST3M panel, they drive the "building…" / "meta-updating…" badges on the
SW4RM agent cards, so the dashboard still subscribes to rebuild_queue_changed
to refresh those badges (applyRebuildQueueChanged now only re-renders
containers, no panel render).
core.css still imports dashboard.css for the shared section styles; splitting
those out + de-duping the renderers into a shared module remains a follow-up.
Largest step of the tabs.js split: the SCH3DUL3S tab — reminders +
scheduled-prompts (the lists, inline create row, per-row edit form, and
fire-now / cancel actions) — moves into a new schedules.js, ~1060 lines
out of tabs.js. It owns its own module state (schedules list +
edit-in-progress tracking) and reads the shared agent roster from
state.js for the target-chip pickers; render + format helpers come from
util.js, so the keystones (state.js, util.js) paid off here.
Exports the four entry-wired functions — applySchedulesChanged /
applyRemindersChanged (mutation dispatch), refreshSchedules /
refreshReminders (tab activation + cold-load) — plus activeScheduleCount
(the count pill). The 19 other functions stay module-private. Also drops
the now-unused paintAtomic import from tabs.js (its only callers were in
this block).
Behaviour-preserving code motion; esbuild inlines schedules.js into the
tabs.js bundle. Build green; tabs.js drops ~1060 lines (4323 → 2969).
The tabs.js split's render-heavy domains (schedules, system, containers,
questions/approvals) all share a handful of pure helpers that lived in
the tabs.js IIFE: paintAtomic (atomic-swap render) and the fmtAgo /
fmtElapsed / fmtDuration / truncate formatters (used 10/15/4/6/6 times
across the file). Pull them into a new dashboard-internal util.js so the
upcoming per-tab modules can import them instead of depending on the
entry's closure — the helper analogue of the state.js roster keystone.
They stay out of the cross-page common.js (their phrasing is
dashboard-specific) but are now a shared dashboard module. All five are
pure, so this is behaviour-preserving code motion; esbuild inlines util.js
into the tabs.js bundle. Build green.
Second module of the tabs.js split (after the roster keystone). The
P3RM1SS10NS tab — the per-agent capabilities + tool-groups matrices — is
the cleanest leaf: six contiguous functions with no module-scoped state
(each render builds fresh from the fetched payload) and no cross-domain
references except the agent roster, which it now imports from state.js.
Moves applyCapabilitiesChanged / applyToolGroupsChanged (the live-update
handlers wired into the entry's mutation dispatch table) and
fetchAndRenderCapabilities / fetchAndRenderToolGroups (called on tab
activation) into a new permissions.js, exporting those four; the two
renderers stay module-private. tabs.js imports the four — the dispatch
table and tab-activation call-sites resolve unchanged.
Behaviour-preserving: pure code motion. esbuild inlines permissions.js
into the tabs.js bundle, so the static output is unchanged. Build green;
tabs.js drops 258 lines.
First step of splitting the 4.3k-line tabs.js monolith into per-tab
modules. The live agent roster (containersState) is the one piece of
genuinely cross-domain state — read by the container tree, the
capabilities + tool-group matrices, the schedule target-chips, the
container-load poll, the selection bar and the operator inbox. Move it
(and its snapshot-sync) into a new dashboard/state.js that each domain
module imports, so the upcoming per-tab extractions have a single source
of truth to depend on instead of a shared IIFE closure.
Minimal by design — only the roster moves; the container row cache and
the apply-handlers (which also re-render) stay with the container domain.
Behaviour-preserving: the imported Map is mutated in place exactly as
before. esbuild inlines state.js into the existing tabs.js bundle, so the
static output is unchanged. Build green.
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.
The dashboard's tab strip now runs on the shared createTabStrip
(@hive/shared/tabs.js) — the third and final consumer of #1464 step 1
(after logs + the agent window selector).
- activateTab is reduced to the per-tab side-effects only
(body.dataset.activeTab, the selection-bar gate, the lazy-loads);
createTabStrip owns the active tab/pane toggle + aria-selected + hash
routing, calling activateTab via onShow. Panes switch via the `hidden`
attribute (was a `.tab-pane-active` class) — the markup carries `hidden`
so there's no flash before the script runs.
- the responsive overflow menu is DROPPED per the design call (move panes
to their own pages rather than hide them in a dropdown). Removes ~150
lines: updateTabbarOverflow, closeOverflowMenu, the click/keydown
handlers, the ResizeObserver + MutationObserver, and syncTabFromHash.
S3TT1NGS is a regular strip tab now (no data-overflow).
- CSS: .tab.active becomes .tab.hive-tab--active; pane visibility is now
.tab-pane[hidden] { display: none }; the overflow dropdown styles are
gone. Count pills (setTabCount/refreshTabCounts) are unchanged.
Behaviour-preserving except the deliberate overflow removal; aria-selected
is now standardised on the tabs.
Removes the standalone P33RS tab + pane. Peer hives now render as a
headline section at the bottom of the SW4RM pane; the swarm/hive name
moves off the cramped strip above the tabbar into a headline at the top
of SW4RM. Both are quiet when nothing's configured:
- swarm/hive identity → `#swarm-identity` h2 in the SW4RM pane (hidden
until hive_name/swarm_name are set), replacing the `#hive-identity`
banner-thin line above the tabbar.
- peer hives → `#peers-block` headline + `#peers-section` cards in the
SW4RM pane. When no peers are federated the headline block is hidden
entirely and a grey "no peer hives configured" note shows in its place.
- drops `peers` from the tab list + the per-refresh peers-tab gating;
renderPeerHives now toggles its own headline.
No backend change — still reads `state.peer_hives` + `hive_name` /
`swarm_name`. Build green.
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.
Swarm-level companion to the per-agent favorite-tools doughnut (#1433).
Aggregates each agent's bash_commands(ts, head) table (written by
hive-bash-mcp) across the whole hive and surfaces the top-10 most-run
command heads on the dashboard ST4TS tab, alongside the existing model
mix.
- hive_stats.rs: AgentAgg gains a `bash` head→count map, filled by a new
guarded `read_bash_heads()` that reuses read_agent's read-only
connection. A missing `bash_commands` table (capture hasn't run for
that agent) or any read error yields an empty map — isolated from
read_agent's error path so it never drops an agent from the rollup.
HiveStats gains `bash_mix: Vec<KeyCount>` (busiest-first, top 10).
Unit tests cover the per-head tally + window cutoff and the
missing-table degrade-to-empty path (in-memory sqlite).
- dashboard: a "favorite tools (bash commands across the swarm)" CSS-bar
list on the ST4TS pane, mirroring the model-mix bars. Header + list
stay hidden until bash_mix has data, so a fresh hive shows no empty
block. (Dashboard ships no chart lib — bars, not a doughnut.)
- docs: dashboard.md ST4TS section documents the new rollup.
Closes#1449. Inert until the hive-bash-mcp capture (#1448, merged) has
recorded data across agents.
Follow-up to the route swap (#1501): now that every separate page is
reachable from the H0M3 hub, the dashboard tab strip should hold only
real in-page tabs, not the "→" links out to other pages (mara: "remove
links to the pages from dashboard - only actual tabs stay here").
- dashboard.html: remove the `tab-matrix`, `tab-flow`, and `tab-logs`
`.tab-link` entries (and the stale `tab-count-flow` pill slot — the
operator inbox moved to Y3R C4LL). The dashboard strip is now SW4RM /
Y3R C4LL / SYST3M / P3RM1SS10NS / SCH3DUL3S / P33RS / ST4TS / S3TT1NGS.
- tabs.js: drop the now-dead `tab-matrix` matrix_gui_enabled gate (the
H0M3 Matrix tile is gated by home.js instead) and the obsolete
`#tab-count-flow` comment. Kept the contextual deep-links into the log
viewer (an agent's logs, a build entry's log) — those are functional
content links, not navigation chrome.
- flow.css: remove the dead `.dashboard-chrome.flow-chrome` +
`.tabbar .tab.active.tab-link` rules — the flow page uses a slim
back-link header, not the dashboard tabbar, and `.tab-link` no longer
exists anywhere.
npm run build green (flow.css 1.8kb). No residual references.
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.
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.
Reminder due-at labels ("in 3m 45s") and schedule next-fire cells
were computed once at render time and never updated. For short-horizon
reminders the stale count was visually misleading.
Both renderers now stamp data-due-at=<unix> on their respective
elements (.reminder-due / .sched-due). A shared 1s setInterval
ticker walks all live elements and rewrites textContent using
fmtDuration / fmtAgo — same zero-re-render pattern as the
existing question TTL chip ticker.
renderMetaInputs snapshots the set of checked data-meta-input values before
calling root.replaceChildren(), then restores them after rebuilding the list.
Without this, a MetaInputsChanged SSE event (e.g. fired when a concurrent
meta-update completes) would silently wipe any checkboxes the operator had
ticked but not yet submitted — forcing them to re-select their inputs.
Approval cards render their age label ("requested N ago") at
render time from a cold /api/state or an approval_added /
approval_resolved SSE event. An approval sitting pending for
an hour would show a stale "0s ago" unless one of those events
fired in the meantime.
Fix: stamp data-requested-at=<unix> on each .approval-ts span.
A 1s setInterval ticker reads all live chips, recomputes the
relative time via fmtAgo, and toggles .stale (amber highlight)
at exactly the 1-hour threshold — no longer dependent on
the next full re-render cycle to apply the amber colour.
Previously only the 'all' chip showed a count (all · N). The operator
had to click each chip to see how many questions were in each category.
Now every chip shows its own count:
all · 5 @operator · 2 @peer · 3 @agentname · 1
This lets the operator see the distribution at a glance and navigate
directly to whichever category has pending items without guessing.
Stamp data-rqe-enqueued / data-rqe-finished / data-rqe-state on queued
and terminal rqe-when spans. A 30s ticker updates them in place so
'queued 2m ago' and 'done 5m ago' labels advance as time passes.
Complement to the keyed rebuild-queue row cache: rows now persist
across snapshots, so without a ticker these static fmtAgo labels
would become arbitrarily stale. The running-entry elapsed ticker
(data-rqe-elapsed, 1s interval) already handled that state;
this fills the gap for queued and terminal states.
Apply the same fingerprint-cache pattern as the container row cache to
the rebuild-queue list. Maintain rebuildQueueRowCache (Map<id, {el,
fingerprint}>) so renderRebuildQueue can reuse <li> nodes whose state
hasn't changed across rebuild_queue_changed snapshots.
The rebuild queue emits a full snapshot on every mutation (a single
rebuild emits queued → running (+ step changes) → done/failed — each
transition is an event). Before this change every event caused a full
replaceChildren() wipe of the list. After this change only the row(s)
whose fingerprint changed get rebuilt; the rest survive intact.
The elapsed-time ticker (data-rqe-elapsed + 1s setInterval) already
updates running-entry timestamps in-place, so ticking elapsed seconds
don't require a re-render and are excluded from the fingerprint.
DOM order is reconciled via insertBefore with no wipe, same as the
container cache.
renderQuestions() was calling root.replaceChildren() on every
question_added / question_resolved SSE event, wiping all <li>
elements including any textarea the operator was typing into.
Add questionRowCache (Map<id, {el, fingerprint}>):
- questionRowFingerprint encodes the static fields that determine
the <li> DOM structure (asker, target, asked_at, deadline_at,
question text, options, multi)
- buildQuestionLi extracts the <li>-building code so it can be
called only when the fingerprint changes
- renderQuestions reuses cached <li> nodes for unchanged questions
and evicts entries for resolved/cancelled questions
Effect: when a new question arrives while the operator is typing a
reply, the existing <li> is reused — the textarea value, radio
selection, and checkbox state are all preserved.
The history <details> open state is also saved before and restored
after the replaceChildren call, so the answered-history panel does
not collapse when a live question event fires.
renderApprovals() called root.replaceChildren() on every
approval_added / approval_resolved SSE event, wiping the spawn
form's name input if the operator had started typing a new agent
name.
Save the current input value before the wipe and restore it into
the freshly-built input element. The form itself is still rebuilt
(no keyed-cache needed here — approval cards have no user inputs),
so all other behaviour is unchanged.
Adds a navigation entry in the per-agent ⋮ overflow menu that opens the
AGENT log tab pre-filtered to that container. The link uses the ?agent= URL
param introduced by the accompanying logs.js change — the operator lands
directly in the journal viewer without having to pick an agent from the
dropdown.
Placed between the rebuild action and the destructive destroy/purge block
so it's reachable but clearly separated from dangerous actions.
On refreshState, the hive-identity title update (fired once when
hive_name/swarm_name are configured) now reads any existing (N) prefix
and preserves it, so the title doesn't briefly drop back to the bare
identity string before refreshTabCounts re-applies the prefix.
When approvals or questions are waiting, document.title is prefixed
with (N) so the operator can see activity in a background browser
tab without switching windows. Clears to the base title when zero.
Implemented in refreshTabCounts (1s tick): strips any existing
prefix before re-applying so the hive-identity title update (once
on state load) composes cleanly.
Stamp data-set-at on .status-age spans so the '(set N ago)' label
stays accurate as time passes. Previously the age was computed once
when the async dashboard-state fetch completed and never updated.
With the keyed container row cache, rows persist much longer between
rebuilds (no more full rebuild on every SSE event), making stale
status-age labels more noticeable. The 30s ticker fixes this without
requiring a full row rebuild or re-fetch.