Commit graph hyperhive/frontend
Author SHA1 Message Date
iris
98d015edea agent ui: shared page-header nav on the stats + screen sub-pages
Per mara: the agent sub-pages should use the same back-link nav as the
dashboard's standalone pages, while the live terminal page stays as-is (no
tabs of each other, not an SPA).

Pull the shared `@hive/shared/chrome.css` into the agent bundle (via agent.css's
existing @import line; esbuild inlines it into the one dist/static/agent.css all
agent pages link), then:

- stats.html: replace the ASCII banner + bespoke `.stats-nav` with the shared
  `page-header` (← live back-link + dashboard link + title). Keeps the #back-link
  / #dashboard-link / #title ids stats.js drives, and the time-window picker +
  charts below are unchanged.
- screen.html: replace the bespoke `#toolbar` nav with the same `page-header`
  (← agent back-link + title), keeping the fit / match / debug controls + the
  status text in the bar.

The live terminal (index.html) is untouched. The now-unused `.banner` /
`.stats-nav` / `#toolbar` rules in agent.css are left for a follow-up prune to
keep this diff focused on the markup. Agent build clean.

Closes #1874.
2026-06-22 13:34:58 +02:00
iris
06b2bd8b72 matrix-accounts: 3-state live status dot + user_id + snapshot freshness
Frontend for the BE-4 snapshot (#1702): GET /api/matrix-accounts now returns
per-account `live` + `user_id` and a top-level `as_of_unix`. Render a 3-state
dot instead of the v1 token-present-only one:

- green  (live + container running)  — online
- amber  (live + container DOWN)      — stale: the page already loads
  /api/state containers, so cross-reference running state; a container that's
  down means the daemon is down, so a "live" snapshot there is stale
- amber  (token_present + !live)      — provisioned but offline
- grey   (no token)                   — not provisioned

The daemon rewrites its snapshot only on (re)start, so `as_of_unix` is "live as
of", not a heartbeat — surfaced as a tooltip. We deliberately do NOT dim a
green purely on snapshot age (an old as_of is ambiguous: stable uptime vs dead
daemon); the container cross-ref catches the definitive down case, and true
daemon-up-but-client-dead detection is the daemon-heartbeat follow-up. user_id
is shown next to the account name. When `live` is absent (v1 backend not yet
deployed) the dot falls back to the token-present rendering, so this is safe to
ship independent of the backend deploy.

FE half of #1702 BE-4; pairs with the backend PR.
2026-06-22 12:53:52 +02:00
damocles
ad6b39b425 remove vestigial agent-ports.json tcp web-port map 2026-06-22 11:59:14 +02:00
iris
e797b75ca9 dashboard: stream the running rebuild's build log inline in the queue
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.
2026-06-22 01:55:38 +02:00
iris
93fa264bbf dashboard: stream the running rebuild's build log inline in the queue
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.

One panel keyed to the running entry's build_log_id (the queue runs one build
at a time), reusing the build-log SSE the logs page already uses
(GET /api/build-logs/id/{id}/stream; frames stdout_append/stderr_append/done).
It lives 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. Sticky-
bottom scroll, 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.
2026-06-22 01:48:32 +02:00
iris
4b318a809a dashboard: reset-timer checkbox on recurring fire-now
Manually firing a recurring schedule now offers a "reset timer" checkbox
(default on) in the confirm dialog: when checked, the fire-now POST sends
{reset_timer:true} and the backend re-arms next_fire_at to now + interval.
Unchecking keeps today's behaviour (extra out-of-band pulse, cadence
intact). One-shot schedules omit the checkbox — they're consumed regardless.
The result flash shows "— timer reset" when the backend reports timer_reset.

Pairs with the backend reset_timer/timer_reset work. Closes #1848.
2026-06-22 01:06:24 +02:00
iris
905b38b6c7 dashboard: point frontend at the /api/* backend routes
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).
2026-06-22 00:45:26 +02:00
iris
1c47bd4333 agent-ui: real fixed-width icon column for terminal rows
Replaces the first-character-glyph + negative-text-indent trick (which let a
wide emoji or a leading disclosure caret knock the icon out of column) with a
genuine icon cell.

terminal.js: row() / details() / detailsDiff() take an optional `icon` that
goes in a fixed-width `.row-glyph` element (inline-block, 1.4em). Details
summaries wrap their text in a `.summary-text` span; the disclosure caret
moves to `.summary-text::before` so it leads the text, not the icon — keeping
the icon in the shared column. terminal.css carries the cell + caret rules.

app.js passes the per-tool emoji as `icon` for the flat tool-use row and every
expandable tool summary (Write/Edit/send/ask/answer/bash) plus the 💭 thinking
row, instead of string-prefixing it. A details `🖥️` now lines up under a flat
row's `🧠` regardless of emoji width. Doc: terminal-rendering.md layout
contract updated. Closes #1844.
2026-06-22 00:41:24 +02:00
iris
08afa49c96 dashboard: themedPrompt is a textarea — Enter submits, Shift+Enter newline
The approval deny-reason prompt (and every themedPrompt dialog) used a
single-line input. Make themedPrompt always a resizable <textarea> with
chat-box keys: Enter submits (clicks the confirm button), Shift+Enter inserts
a newline. Short answers stay one keystroke; multi-line reasons (e.g. a deny
note) are now possible. No single-line variant — themedPrompt is only used by
the data-async data-prompt path, so all those dialogs get the textarea. Closes #1840.
2026-06-21 23:55:15 +02:00
iris
fb58f0e5b9 agent-ui: per-tool emoji icons + thinking/turn-end glyphs in terminal
Following the 🧠 thinking-tokens indicator (operator liked it), give terminal
rows evocative glyphs:
- 💭 thinking prose (pairs with the 🧠 token counter)
- / turn-end ok/fail (was ✓/✗)
- per-tool icons via toolIcon(name): 📤 send · 📥 recv ·  ask · ✍️ answer
  ·  remind · 🏷️ set_status · 🪢 loose-ends · ↻/⏹️/▶️/🔄 lifecycle · 📦
  request_* · ⏱️ schedule · 🖥️ bash · 💬 matrix · 📖 Read · 💾 Write · ✏️
  Edit · 🔍 Glob/Grep · 🔧 default — on both flat rows and the rich
  send/ask/answer/Write/Edit summaries.

Dropped the original 🐝 task-row glyphs: claude's nested-agent Task tool is
intentionally omitted from agents' allow-list (mcp.rs), so those rows never
render for agents. Doc table updated.

Closes the emojify-terminal request.
2026-06-21 23:15:25 +02:00
iris
eb1d913b47 fix(dashboard): call.js question handlers referenced tabs.js-only renderContainersFromState
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.
2026-06-21 22:42:34 +02:00
iris
63a79d76ba agent-ui: collapse streamed thinking_tokens ticks into one live row
claude streams a running 'estimated_tokens' counter as system/thinking_tokens
events — many per turn (thousands in a long turn). renderStream rendered each
as a '⚙ thinking_tokens' note, flooding the terminal scrollback. Coalesce
consecutive ticks into a single '🧠 thinking … ~N tokens' note row that
updates in place; reuse the row only while it's still the last one rendered
(nextElementSibling == null) so any other event starts a fresh one. Closes
#1818.
2026-06-21 22:09:31 +02:00
iris
6bb976ec72 dashboard: replace agent-tab transient list with build-queue summary banner
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.
2026-06-21 22:07:14 +02:00
iris
84077289f1 fix(dashboard): call.js renderQuestions referenced tabs.js-only helper
renderQuestions (call.js) called snapshotOpenDetails()/restoreOpenDetails()
which are closure-locals in tabs.js (built on MANAGED_SECTION_IDS) and not in
call.js's module scope. On a /dashboard.html (Y3R C4LL tab) refresh this threw
'ReferenceError: snapshotOpenDetails is not defined' and aborted refreshState
entirely. Give call.js its own section-scoped snapshot/restore pair operating
on the questions-section root (the only section renderQuestions manages), so
open <details> state still survives an SSE re-render without reaching into
tabs.js internals.
2026-06-21 21:56:18 +02:00
iris
3a2cdaa37b dashboard: 'starting…'/'stopping…' card badges for start/stop queue kinds (#1806)
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.
2026-06-21 14:52:31 +02:00
iris
cd7846b348 bash-tasks page: live interval poll + spawn_blocking the dir scan (review)
Two fixes from review:
- Liveness: /api/state isn't polled while online (only during login), so
  hooking refreshBashTasks to it only populated on cold load. Bash tasks
  start + finish asynchronously between turns, so add a light ~4s interval
  to keep the tasks pill live; the /api/state-time call now just does the
  first-paint populate. Doc note corrected to match.
- Move the blocking dir scan + per-file reads in /api/bash-tasks off the
  async executor via tokio::task::spawn_blocking (damocles nit).
2026-06-21 13:28:37 +02:00
iris
4ce919a19f agent-ui: running bash-tasks pill + flyout on the agent page
Adds a 'tasks' header pill (hidden at zero, like inbox/loose-ends) that
opens a side-panel flyout listing the agent's in-flight bash tasks from
GET /api/bash-tasks. Each row shows status (running/queued), task id,
elapsed time, and a truncated single-line command preview. Polled on the
same /api/state cycle as loose-ends (tasks complete async between turns, so
the count stays live); clicking the pill opens the flyout. Snapshot-only
v1 — SSE live-push is a possible follow-up.
2026-06-21 13:28:37 +02:00
iris
cba02fc47e dashboard: label graceful-stop queue entries 'stopping…' on agent cards
#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.
2026-06-19 12:01:44 +02:00
iris
5b99eb1f8f dashboard: keep bulk/M0V3 partial-failure toasts until dismissed
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.
2026-06-19 02:10:18 +02:00
iris
7c2de690ec dashboard: theme all remaining dialogs (replace native confirm/alert/prompt)
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.
2026-06-19 02:10:18 +02:00
iris
a45b56f658 dashboard(modal): label the dialog via aria-labelledby for a11y
Give the themed dialog an accessible name (role=dialog requires one):
label it by its title when present, else by its message, via
aria-labelledby on the box. Addresses an a11y review note on the
stop-confirm modal.
2026-06-19 01:24:54 +02:00
iris
0407e7da62 dashboard: extract themed dialog into a reusable modal.js component
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.
2026-06-19 00:25:22 +02:00
iris
03b10d22cd dashboard(swarm): themed stop confirm modal with graceful checkbox
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.
2026-06-19 00:20:05 +02:00
iris
74196ac582 dashboard(swarm): wrap agent status to two lines, always show set-time
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.
2026-06-18 13:02:11 +02:00
iris
d8d589ccb2 dashboard(matrix-accounts): populate agent picker from state containers
The agent dropdown read a non-existent `agents` field on /api/state, so
it always rendered empty ("— no agents —"). The endpoint exposes the
live roster under `containers` (each entry an object with `.name`).
Read that instead; the existing string-or-object map keeps it robust.
2026-06-18 13:02:02 +02:00
iris
1bc26429f2 dashboard(home): harden matrix-rain fade-trail colour parsing
Follow-up to the matrix-rain backdrop (merged): address argus's review nit
on the fadeColor construction. It string-rewrote `rgb(...)`→`rgba(...)`,
which silently fills the trail solid if getComputedStyle ever returns a
non-`rgb(r, g, b)` form. Extract r,g,b numerically and build the rgba
explicitly so the trail always fades regardless of the returned format
(same rgb fallback end-to-end). CSS/JS-only, home page.
2026-06-17 21:59:12 +02:00
iris
31001d52e8 dashboard(home): matrix-rain backdrop on the H0M3 page
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.
2026-06-17 21:46:25 +02:00
iris
9b54eff5d1 dashboard(html): fix stale P3RM1SS10NS pane comment after the save-all change
The save-all change updated the visible copy + the JS but left this HTML
pane comment describing the old per-row "POSTs to the respective /{agent}
endpoint" flow. Correct it to the page-level save-all → POST /api/permissions
batch (caps + groups coalesce into one rebuild per agent). Comment-only.
2026-06-17 20:14:09 +02:00
iris
fe5e0bd841 dashboard(core): add disk column to the LOAD container-resources table
Surfaces the per-container on-disk footprint the c0re sampler now
reports as `disk_bytes` on /api/container-resources (state dir +
container writable rootfs, shared nix store excluded; sampled
out-of-band every few minutes). Renders bytes→human via the existing
cloadFmtBytes helper, with an em-dash until the first sample lands
(disk_bytes is null then). Pairs with the hive-c0re sampler half.
2026-06-17 19:12:39 +02:00
iris
4297436d94 dashboard: single save-all button for the permissions page
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).
2026-06-17 18:58:56 +02:00
iris
06d57a61f8 dashboard: fix matrix-accounts GET path to /api/matrix-accounts
BE-1 (the list endpoint) lives at GET /api/matrix-accounts (reads go under
/api/, matching /api/state and /api/operator-inbox); the page was calling
the un-prefixed /matrix-accounts. Correct the fetch + the contract comment +
the docs reference. POST /matrix-account-login (mutation, root path like
/approve) is unchanged.
2026-06-16 10:22:53 +02:00
iris
8bb3bd82a1 dashboard: drop matrix-accounts experimental notice; relabel list as provisioned
Per-account token-failure isolation is now live on the matrix daemon, so a
bad credential entered via this form can no longer take down the agent's
whole matrix session — remove the experimental notice (+ its now-unused CSS).

Also relabel the account list 'configured accounts' -> 'provisioned accounts'
to match the v1 read path (lists accounts with a stored token; a
config-declared-but-unprovisioned account appears once provisioned through
the form). Docs + module header updated to match.
2026-06-16 10:22:53 +02:00
iris
9293fe3ac9 dashboard: matrix-accounts page — provision/login per-agent external matrix accounts
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.
2026-06-16 10:22:53 +02:00
iris
c187366961 dashboard: dedent call.js moved blocks to module top level
Cosmetic follow-up to the Y3R C4LL extraction (argus nit on #1705): the
approvals + questions sections kept their original 2-space IIFE indentation
from tabs.js, which read as if they were inside a block and was inconsistent
with the unindented operator-inbox section + the other domain modules.
Uniform dedent — no logic change, build unaffected.
2026-06-16 10:22:53 +02:00
iris
a0fc7e1a9f dashboard: extract questions domain into call.js — Y3R C4LL split complete
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.
2026-06-16 10:22:53 +02:00
iris
d0ec0d2896 dashboard: extract operator-inbox + approvals into call.js (#1451)
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.)
2026-06-16 10:22:53 +02:00
iris
06c5d68071 refactor(dashboard): move SYST3M section CSS into shared system-sections.css
The C0R3 page (/core.html) pulled the four SYST3M panel styles (rebuild
queue, meta inputs, container load) by @importing the whole ~39kb
dashboard.css, dragging the entire dashboard stylesheet into the C0R3
CSS bundle.

Extract those section-internal rules into a new system-sections.css and
@import it from both core.css and dashboard.css. core.html now loads only
the small shared file (its CSS bundle drops from ~40kb to 5.3kb) while
the dashboard keeps the rules — a couple are still referenced from
dashboard JS (the schedules view uses .rqe-source*). The container-load
table's .hive-stats-table already lives in common.css, linked directly
by core.html, so it is unaffected.

Behaviour-preserving: identical rules, same custom properties; the moved
selectors are uniquely named to these panels and not redefined
elsewhere, so the cascade is unchanged. Verified the built bundles:
core.css carries the section rules and no dashboard chrome; dashboard.css
still carries them via the import.
2026-06-15 09:54:55 +02:00
damocles
789ecd86f6 fix(#1661): show default-perms agents with effective values in perms tab 2026-06-14 21:20:03 +02:00
iris
24cbafc64a feat(dashboard): live-append the LOGS AUDIT tab via audit_entry_added
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.
2026-06-14 20:23:54 +02:00
iris
8b991b2cc5 feat(dashboard): AUD1T — privileged-actions audit trail as a LOGS sub-tab
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.
2026-06-13 15:31:50 +02:00
iris
878f95205b fix(dashboard): wire data-async form submit handler on the C0R3 page
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.
2026-06-10 22:17:40 +02:00
iris
8db8bd610f feat(stats): surface first-turn ctx tokens on the per-agent stats page
Builds the read/surface half of the per-session first-turn-tokens metric
(the capture — sessions table + turn_stats.session_id — landed separately).
A fresh claude session's first turn pays the full static prefix (system
prompt + tools + CLAUDE.md + first wake) as uncached input, so its
input_tokens is a clean proxy for prompt / CLAUDE.md sprawl — watching it
over time surfaces creep.

- stats.rs: add `Snapshot.first_turn_ctx: Option<u64>` populated by
  `read_first_turn_ctx` — the agreed per-session derive (first turn,
  `ORDER BY started_at LIMIT 1`, of the most recent session that started
  in the window). Inert-until-capture: `.ok()` maps both "no fresh
  session yet" and "older db without the sessions table" to None, the
  same decoupling as read_bash_breakdown; the field is skipped from the
  JSON when None. Pre-capture rows have a NULL session_id and are excluded.
- agent stats.js: add a "first-turn ctx" summary chip, guarded on a
  numeric value so it stays hidden until capture has data.

clippy + cargo fmt clean; agent bundle builds.
2026-06-10 20:37:35 +02:00
iris
b38c834182 fix(dashboard): keep the logs ← home link + sub-tabs on one row
The /logs.html header puts a flex:1 sub-tab strip (`.logs-tabbar`) next
to the ← home back-link inside the flex `.page-header`. The strip is
itself a flex container, so its default `min-width: auto` keeps it at its
intrinsic content width and — without an explicit nowrap on the parent —
it gets pushed onto its own row under the back-link.

Mirror the working dashboard chrome (`.dashboard-chrome` / `.tabbar`,
which sets `flex-wrap: nowrap`): make `.page-header` explicitly nowrap and
give `.logs-tabbar` `min-width: 0` so it shrinks to share the row instead
of wrapping. Affects every standalone page that uses `.page-header`
(flow / logs / stats / settings); the nowrap is correct for all of them.
2026-06-10 19:51:34 +02:00
iris
1510071e42 feat(dashboard): remove the SYST3M tab now that C0R3 owns it
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.
2026-06-10 19:51:06 +02:00
iris
be8d97ded7 feat(dashboard): add standalone C0R3 page (system tab → its own page)
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).
2026-06-10 19:51:06 +02:00
iris
e89ca90956 feat(agent-ui): show turn start/end times + duration on the agent terminal
The per-agent terminal marks turn boundaries (◆ TURN ← / ✓ turn) but
shows no time. Append a wall-clock HH:MM:SS to both the turn-start and
turn-end rows, plus the elapsed duration on turn-end, rendered as dim
metadata so the boundary glyph stays the focus.

This is the frontend half of the feature; it reads a per-event `ts`
(unix seconds) off the turn_start / turn_end events. The read is guarded
on a numeric `ts`, so until the harness surfaces per-event timestamps
the rows render exactly as before (inert-until-capture). Once the
backend attaches `ts` to the history rows + live SSE frame, the times
light up automatically for both live and scrollback, with no further
frontend change.
2026-06-10 14:36:54 +02:00
iris
986b3df9b0 feat(frontend): add forge link tile to H0M3 hub
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.
2026-06-10 13:22:24 +02:00
iris
ceb3e3b01a feat(frontend): per-agent effort-level quick-picker (#1596)
Adds a reasoning-effort quick-picker to the agent page's overflow menu,
a direct sibling of the existing model quick-picker, wired to the backend
seam from #1597/#1600: /api/state carries effort + available_efforts, the
picker POSTs { effort } to /api/effort (operator-only, mirroring
/api/model), and live updates arrive via the effort_changed SSE event.

The available levels are the backend's to declare — the frontend holds no
hard-coded list; availableEfforts is seeded from state.available_efforts
on cold-load and the picker section is omitted until the backend supplies
the set. Mirrors the model picker otherwise: postEffort(),
renderEffortChip() (updates the new amber effort-chip + button active
states), the cold-load seed, and the effort_changed handler. Build green.

Applies on the next claude session (operator uses the existing
clear-session), per the backend contract.
2026-06-10 01:34:21 +02:00
iris
d7bba48176 fix(frontend): stack the warning banner + tab bar in one sticky wrapper (#1587)
Per mara's review: instead of measuring the banner height in JS and
offsetting the chrome's sticky top, put the warning banner and the page
chrome in the same sticky div so they stack naturally.

common.js builds the wrapper: ensureStickyTop() wraps the page's chrome
(.dashboard-chrome / .page-header) in a single .sticky-top container and
injects the warning banner as its first child. The banner and the chrome
are no longer individually sticky — .sticky-top owns the stickiness, so
they pin together in one context instead of two top:0 stickies colliding
(the banner used to overlay the tab bar). Pages without a chrome (the
H0M3 hub) get a banner-only sticky region. No per-page markup needed; no
JS height measurement. Build green.
2026-06-10 01:05:55 +02:00
iris
0dc85e8261 fix(frontend): flag gone-agent columns in the schedules table (#1586)
The schedules table builds a column per target, including agents that
have since been destroyed (their past schedules linger). Those columns
showed identically to live agents, so the operator couldn't tell a
schedule still targets something that no longer exists.

Flag them: renderSchedulesTableHead now checks each agent column against
the live roster (containersState) and, for any that isn't there (and
isn't 'operator', which is always valid), greys + strikes through the
column label and titles it 'no longer exists (gone)'. The rows stay
visible + cancellable — the operator can see the stale targets and clear
them rather than be surprised by phantom columns. Build green.
2026-06-10 00:59:07 +02:00