Commit graph hyperhive/frontend/packages/dashboard
Author SHA1 Message Date
iris
1acc271108 fix(#2443): address asyncBtn review notes
- asyncBtn now returns fn().finally(...) so callers can await/chain it
- Move re-fetch calls inside try/catch in core.js and permissions.js so
  network errors from fetchAndRenderStalePerms / fetchAndRender* are
  caught instead of escaping as unhandled rejections
- clearStaleAgent returns the asyncBtn promise so the function is
  properly awaitable when a button is present
- Update asyncBtn doc comment to reflect the return-value contract
2026-07-20 19:59:45 +02:00
iris
bd7ae83860 fix(#2443): catch fetch() network errors in bindAsyncForms doSubmit
Wrap the fetch() call in a try/catch so network errors (offline, DNS
failure, CORS) surface via themedToast instead of becoming unhandled
promise rejections. The asyncBtn finally() still restores the button
either way — the catch just adds the missing operator feedback.
2026-07-20 19:58:04 +02:00
iris
4b45c5cd3d feat(#2443): asyncBtn — shared reusable component, replace ad-hoc disable/spinner patterns
add `asyncBtn(btn, fn)` to `@hive/shared/forms.js` as the single
reusable component for async button actions:
  1. double-click guard: returns immediately if btn is already disabled
  2. saves btn.innerHTML, replaces with spinner while in-flight
  3. restores btn on resolve or reject via finally

wire it into all ad-hoc disable/spinner/restore patterns:
  - common.js: bindAsyncForms uses asyncBtn internally
  - core.js: 'clear perms' button
  - permissions.js: clearStaleAgent
  - schedules.js: saveSchedule submit, editSchedule submit
  - app.js: buildAnswerForm, buildInboxMarkAllRow

fireScheduleNow in schedules.js is left with its existing childNode
save/restore because it shows a custom result flash on the button
content after a successful fire-now (the auto-restore of asyncBtn
would overwrite it); the surrounding themedConfirm dialog already
acts as a natural double-click barrier before the fetch.

saveAll in permissions.js is also left as-is: it uses a custom
'queued ✓' success label + a 900ms delay before re-fetch; the
btn.dataset.busy flag is its own double-submit guard.
2026-07-20 19:51:03 +02:00
iris
66c715842f dashboard: add hive infra containers to the logs UI agent selector
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.
2026-07-19 14:49:17 +02:00
damocles
c42ecc9188 add post_swap label to the dashboard build-node map (#2390) 2026-07-17 01:06:33 +02:00
iris
9edd37501a chore(frontend): update npm dependencies
- dompurify 3.2.4 -> 3.4.12 (fixes a moderate-severity XSS advisory
  chain flagged by npm audit; dompurify sanitizes untrusted markdown
  output in both the dashboard and agent UIs)
- marked 18.0.4 -> 18.0.6
- esbuild 0.28.0 -> 0.28.1

npm audit now reports 0 vulnerabilities. Verified with npm run build
(dashboard + agent) and nix fmt --fail-on-change (no changes).
2026-07-15 23:30:25 +02:00
iris
614e8c6d5a fix(#2515): derive per-agent in-flight badge from node state, not DAG state
inFlightOpsByAgent() read e.agent, a DAG-level field that no longer
exists (agent moved to per-node when DAGs became agent-per-node). So
the SW4RM tab's 'building...'/'meta-updating...' badges never matched
any real agent, and multi-agent DAGs (a startup sweep's MetaLock
cascade, a hive-wide restart) showed nothing at all on the per-agent
cards even while the rebuild queue clearly had them in flight.

Derive each agent's in-flight kind+state from its own node(s) within
the entry instead: a DAG can be 'running' overall while a given
agent's subgraph hasn't started (still queued behind an earlier node
in its chain), so per-node state is also more accurate than the old
per-DAG state for the badge, not just more available.
2026-07-15 23:15:14 +02:00
iris
990f021c32 fix(#2492): split rebuild-queue DAG lines on fan-out points, not just WCC
nodeComponents() split a DAG into weakly-connected components via deps
edges, but post #2476/#2450 every agent's rebuild subgraph hangs off a
shared MetaLock node via AfterOk, so the whole meta-update cascade is one
connected component and rendered as a single wall-of-chips line.

Add a second pass (splitFanOut) that further splits a component's
topo-ordered nodes on out-degree>1 points: a node with more than one
direct dependent renders as its own one-node line, and each dependent
becomes the root of an independent line. Purely deps-structure-driven,
same as the existing WCC split - no agent-field grouping involved. A
component with no fan-out (the common single-agent case) is unaffected.
2026-07-15 23:11:00 +02:00
damocles
c2bd7db998 refactor(#2416): remove the non-pr config-change flow (request_apply_commit / applycommit) 2026-07-15 21:03:52 +02:00
atlas
fbbd5d921c feat(#2485): remove vestigial Noop + StartupSweep residuals
Since the boot sweep (#2450) and meta-update cascade (#2476) became
single DAGs that grow subgraphs in-place, nothing constructs the old
fan-out anchors/parents anymore:

- NodeKind::Noop (the old boot_root grouping anchor) — no constructors.
- Template::StartupSweep / Source::StartupSweep (the old fan-out parent
  template + cascade-child source) — replaced by Template::Boot and
  Source::AutoUpdate/MetaUpdate respectively.

Drops the three variants + their as_str arms + the Noop executor arm, and
refreshes the stale fan-out/anchor doc comments (Boot/MetaUpdate/Source
docs, coordinator.md, dashboard.md). Frontend: the queue-kind glyph moves
from the dead startup_sweep to boot (which had none), and the dead
rqe-source-startup_sweep style is dropped.

No behaviour change — pure dead-variant removal.
2026-07-15 20:54:19 +02:00
iris
a73216e8be fix agent-logs unit dropdown mismatch, add other hive daemons
Dashboard AGENT log tab sent unit=hive-ag3nt.service (the crate/dir
name) but the actual systemd unit is hive-agent.service, so every
fetch 400'd against the backend allow-list. Fixed the dropdown value
and, per the same issue's request, added the other per-agent daemons
(hive-mcp-http, hive-bash-daemon, hive-matrix-daemon) as selectable
units, plus hive-priv.service on the SYSTEM tab's host-daemon list.
Extended both backend allow-lists (post_journal / post_journal_host)
to match, and fixed a stale systemd.services.hive-ag3nt doc reference
in agent-hierarchy.md that had the same crate/unit-name confusion.
2026-07-15 20:41:59 +02:00
atlas
edf9fd036e feat(#2453): remove DAG parent_id now that every op is one DAG
With the meta-update cascade (#2476) and startup sweep (#2450) folded
into single DAGs that grow per-agent subgraphs via append_subgraph,
nothing links parent/child DAGs anymore — parent_id is dead.

hive-c0re: drop parent_id from Dag/DagSpec (+ the DagView copy); delete
append_children and cancel_children (no callers); simplify trim_history
(no more terminal-parent-with-live-children guard — a one-big-DAG is
terminal only when its whole graph settles); drop the rebuild() parent_id
param; QueueDag returns just the polled DAG (no fan-out children to
gather). hive-sh4re: drop the DagView.parent_id wire field.

frontend: a multi-step op is one DAG now, so renderRebuildQueue drops the
childrenOf/orphans cross-DAG grouping and renders each entry flat; its
per-agent subgraphs render as nodes within the one row (split by deps).
Removed the dead rqe-child style + isChild plumbing.

Docs + the child-DAG queue tests updated/removed to match.
2026-07-15 19:50:04 +02:00
iris
44286996ed fix(#2465): split node chain on deps graph, not agent field
mara's review comment: the frontend shouldn't apply its own grouping
logic on top of the DAG — it should render the structure the backend
already provides. The actual structure is the nodes' deps graph, not
the incidental n.agent field.

Replace the group-by-agent heuristic with nodeComponents(): splits
entry.nodes into weakly-connected components via the deps edges
(undirected reachability), then topo-sorts each component (Kahn's
algorithm) so a chain renders in true dependency order. A DAG made of
independent per-agent subgraphs (no cross-agent deps) still comes back
as separate components — same visual result for today's templates —
but the split is now driven by what the backend actually encodes, and
naturally extends to any future non-agent-aligned branching. Agent
name is still shown as a per-component label, but purely as adjunct
info sourced from that component's own nodes, not the grouping key.
2026-07-15 18:23:47 +02:00
iris
45cdd62116 fix(#2465): render multi-agent DAG nodes as per-agent subgraph lines
renderQueueEntry flattened entry.nodes into one arrow-joined chain
regardless of which agent each node belongs to. A multi-agent DAG
(e.g. hivectl restart --graceful with several agents) runs independent
per-agent subgraphs concurrently, no cross-agent deps, so joining them
all into one sequential-looking chain misrepresented the actual DAG
shape (mara's report: 'expected one dag that forks after the start
node into the per agent sub dags').

Group nodes by n.agent (stable, first-seen order) and render one
.rqe-nodes line per agent, with a small agent-label chip when the DAG
spans more than one. Single-agent DAGs (the common case) collapse back
to exactly the prior one-line render — no visible change there.
2026-07-15 18:23:47 +02:00
iris
d8567fc546 add graceful checkbox to restart confirm dialog
Restart now offers the same graceful-vs-hard choice stop already has:
single-agent menu item and the bulk-select action bar both grow a
'restart gracefully' checkbox that routes through the existing
submit::graceful_restart DAG (signal -> drain -> stop -> reconcile)
instead of a hard restart. Backend gains a ?graceful=true query param
on POST /api/restart/<name>, mirroring post_kill's shape (renamed
KillParams -> GracefulParams since it's now shared).
2026-07-15 18:05:47 +02:00
atlas
9a39a54820 refactor(#2449): dashboard + docs for the SetWanted node
- builds.js NODE_KIND_LABEL: 'set wanted' (and the previously-missing
  'noop') so the new head node renders with a label, not the raw kind.
- docs/coordinator.md + templates.rs module doc: the power-op DAG shapes
  now show the head SetWanted node; SetWanted added to the lease-needing
  list with the atomicity rationale.
2026-07-14 22:34:44 +02:00
atlas
de69a9f02c refactor(#2441): live-log header shows the streaming node's agent, not the DAG's
Review (iris/argus): the dashboard rebuild live-log header labels one
specific node's log stream (liveNode, keyed by its build_log_id), so it
should show that node's own agent — entryAgents(running) listed every
agent in the DAG, which would mislabel a single agent's log once DAGs
span multiple. The other entryAgents() sites (row label, cancel-confirm,
fingerprint) are correct whole-DAG summaries and unchanged.
2026-07-14 21:58:40 +02:00
atlas
e2b48d2014 refactor(#2441): update queue consumers + docs for agent-per-node
DagView no longer has a DAG-level agent, so consumers derive it from the
per-node agents:
- hivectl dag_progress.rs: dag_agents(d) helper (distinct node agents,
  comma-joined) in place of d.agent.
- dashboard builds.js: entryAgents(entry) helper likewise for the
  rebuild-queue card + live-log header + cancel confirms.
- docs/coordinator.md: lease prose (node-agent-keyed, global per agent),
  wire shape (NodeView.agent, no DagView.agent), and dropped the removed
  dedup section.
2026-07-14 21:58:40 +02:00
damocles
bf657177d3 fix(#2438): link config approval commit by full sha, not abbreviated 2026-07-14 20:48:35 +02:00
iris
dbf880ac66 extra-forges: fully dashboard-provisioned, no host config
Per mara's feedback on PR #2407 ("better: you can also provide url in
dashboard, same as with matrix, no host config"), drops
services.hyperhive.extraForges and the admin-API mint/revoke flow
entirely. The operator now creates a token on the external forge
themselves and pastes a label + base URL + access token into the
dashboard's FORGES tab, the same shape as the GitHub PAT flow plus the
base-URL field from the matrix extra-account flow. hive-c0re only ever
writes/deletes two local files per account (forge-<label>-token,
forge-<label>.json sidecar for the URL) via hive-priv — no remote
account creation, no admin token, no revoke-on-the-remote-side, no nix
config to enumerate.

- nix/host-modules/hive-forge/default.nix: removed the extraForges
  option, its label-format assertion, and the HYPERHIVE_EXTRA_FORGES
  env forwarding.
- hive-c0re/src/forge/extra.rs: deleted (REST admin-API provisioning,
  no longer needed).
- hive-c0re/src/dashboard/extra_forges.rs: GET /api/extra-forges?
  agent= lists an agent's stored forges by scanning its state dir
  (mirrors matrix_accounts.rs's filename-scan listing), POST
  /api/extra-forge-account (agent/label/base_url/token/
  action=add|remove) stores or removes an account.
- hive-sh4re/priv_proto.rs + hive-priv/main.rs: new
  WriteAgentExtraForgeAccount/DeleteAgentExtraForgeAccount priv
  requests (adds base_url, writes/deletes a JSON sidecar alongside the
  token).
- hive-c0re/src/priv_client.rs: matching wrapper functions.
- frontend/packages/dashboard/src/credentials.{html,js}: FORGES tab is
  a per-agent list + add-account paste form (label/base_url/token), no
  grant/revoke-from-catalog UI.
- docs/web-ui/dashboard.md: FORGES tab section rewritten.

Supersedes the design in PR #2407 (already approved+green on the old
admin-API model) — opening as a fresh PR against the same issues
rather than force-pushing over the approved one.
2026-07-14 18:38:55 +02:00
damocles
436adf6fd0 refactor(#2390): split provision out of the create node in the spawn dag 2026-07-13 16:13:59 +02:00
damocles
16f69ca890 feat(#2392): group boot sweep + reconciles under one boot dag 2026-07-13 11:51:39 +02:00
iris
ef14641b94 feat: add infra container start/stop/restart tab to C0R3 page
New POST /api/infra-container/{name}/{action} dashboard route (start/
stop/restart on hive-ci/hive-forge/hive-gateway/hive-matrix), reusing
the existing priv_client::control_infra_container helper the
infra_admin agent path already uses, plus an audit_log entry per
attempt. Adds infra_containers to the /api/state StateSnapshot (name +
live running status via systemctl is-active). New 1NFR4 sub-tab on the
C0R3 dashboard page: one row per infra container with a running/
stopped badge and start/stop/restart buttons, polled every 5s while
the sub-tab is open.
2026-07-12 03:12:44 +02:00
iris
5c2cae41a2 feat: rename matrix-accounts page to credentials, add github PAT tab
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.
2026-07-11 12:40:13 +02:00
iris
ccc5e631e2 web-ui: sanitize markdown HTML with DOMPurify to fix XSS
Both mdNode implementations (agent UI app.js, dashboard common.js)
assigned marked.parse() output straight to innerHTML with no
sanitizer. marked v5+ dropped its built-in sanitize option, and there
was no DOMPurify anywhere in frontend/, so markdown containing raw
HTML/script tags rendered live in the browser.

Both sinks receive untrusted input in practice: the agent UI's mdNode
renders recv tool_result bodies, assistant prose, and send/ask/answer
payloads sourced from peer agents and matrix-relayed messages (the
documented prompt-injection adversary); the dashboard's mdNode renders
agent-authored .md files served verbatim by GET /api/state-file
(the endpoint validates path, not content). Since the per-agent UI and
dashboard are same-origin behind the gateway with operator-authority
endpoints (approve/spawn/rebuild/destroy/answer-question), injected
script would run with the operator's session.

Fix: DOMPurify.sanitize() the marked.parse() output at both sinks
before assigning to innerHTML. Added dompurify as a dependency to
both the agent and dashboard npm workspaces, recomputed npmDepsHash
in nix/frontend.nix for the updated lockfile. Also corrected
docs/web-ui/shape.md, which claimed the markdown-rendering path was
XSS-safe by construction the same way the text-node-based linkify
path is — it isn't; it's safe because it's sanitized.

CSP hardening for the dashboard (no unsafe-inline) is a separate,
larger backend change (response headers in hive-c0re) and is left as
a fast-follow rather than folded into this fix.
2026-07-10 11:54:50 +02:00
müde
084e12503c fix(hive-c0re): close review findings on the job-DAG queue
- deploy-window gate (meta::exclusive) + path-limited meta commits:
  a perm/lock/topology commit can no longer sweep an ApprovalDeploy's
  staged flake.lock and neuter abort_deploy (regression test included)
- cancel surfaces now buffer terminal roll-ups the scheduler drains,
  so a queued approval DAG cancelled by the operator resolves its
  approval instead of dangling, and cancelled power ops revert their
  wanted flip to the observed state
- hivectl restart / restart-all ride the queue (lease serialization,
  transient guard) and restart sets wanted=Up like the old kill+start
- exactly one Rebuilt event per rebuild DAG, emitted at terminal
- StopForUpdate pre-seeds a missing agent_power row from the pre-stop
  observation so a rebuild can't strand an unknown agent offline
- history trim keeps terminal fan-out parents with live children
- audit_log back on db::open; swarm.js badge for reconcile DAGs
2026-07-06 21:44:43 +02:00
müde
8349e6f621 feat(dashboard): node-aware queue render + buildSlots option
each queue card now shows its DAG's node chain (per-node state, step,
build-log link), fixing 'queue jumps don't show on the dashboard'.
live-log panel keys off the running node's log. new
services.hyperhive.c0re.buildSlots option (default 1) threads the
concurrent nix-build count into serve.json.
2026-07-06 20:36:57 +02:00
iris
9b607e2857 refactor(dashboard): extract SW4RM domain from tabs.js into swarm.js
tabs.js shrinks from 1651 to 457 lines. swarm.js is a new 1217-line
module that owns the containers/selection-bar/peer-hives domain:

- Container-state apply handlers (applyContainerStateChanged/Removed)
- Rebuild-queue sync + apply (syncRebuildQueueFromSnapshot, applyRebuildQueueChanged)
- Transient-state sync + apply (syncTransientsFromSnapshot, applyTransientSet/Cleared)
- renderContainersFromState (re-render from cached snapshot)
- Per-agent context menu (buildAgentMenu, agentMenuPost, closeAllAgentMenus)
- Topology tree builder (buildAgentTree, treePrefixDom)
- Container row (containerRowFingerprint, buildContainerLi, renderContainers)
- Selection bar (renderSelectionBar, addMoveActions, validReparentCandidates, addBulkButton)
- Peer hives section (renderPeerHives)
- Status-age ticker (30s setInterval for .status-age[data-set-at] spans)
- CTX_WARN/CTX_CAUTION constants
- Selection event listeners (Esc to clear, click on selection-clear)
- Agent-menu event listeners (click outside to close, Esc to close)

tabs.js (coordinator) retains:
- notifyDeltas + seenApprovals/seenQuestions/seededNotify
- bindAsyncForms
- Cross-domain ticker (approval request-age + reminder/schedule due-at)
- refreshState, pollTimer, operatorIsTyping, snapshotOpenDetails
- MUTATION_HANDLERS dispatch + bindDashboardStream
- activateTab, createTabStrip, initCall, refreshTabCounts, setTabCount

Behaviour-preserving code-move. Build verified.
2026-07-04 11:37:39 +02:00
damocles
cafef519a9 dashboard frontend: consume rfc3339 timestamps from the api 2026-07-02 22:28:30 +02:00
iris
1e15c8cc6b fix(dashboard): replace stale 'manager' with 'submitter' in approval UI text
Two operator-visible strings and one code comment still referenced 'the
manager' in the approval card UI:

- init_config card description: 'manager customises agent.nix before
  spawn' -> 'submitting agent customises agent.nix before spawn'
- deny form data-prompt: 'sent to manager' -> 'sent to submitter'
  (the deny note goes to the ApprovalResolved submitter, not a fixed
  manager role)
- code comment: 'surfaced to the manager via' -> 'surfaced to the
  submitting agent via'
- builds.html meta text: 'manager learns each outcome' -> 'the
  submitting agent learns each outcome'

The 'manager' role is no longer structural — root-ness is topological.
Any agent with the approvals tool group can submit; the note/event routes
to the submitter.
2026-07-02 14:15:07 +02:00
iris
688531030b fix(builds): add missing start/stop/graceful_stop kind glyphs to QUEUE_KIND_GLYPH
The QueueKind enum has graceful_stop, start, and stop variants but the
dashboard QUEUE_KIND_GLYPH map only covered rebuild/meta_update/spawn/
destroy/restart/startup_sweep/perm_change — the three missing kinds
rendered as '?' in the build queue UI.

This became visible with the deferred start-after-rebuild change: a fast-lane
Start entry now appears as a child of its Rebuild parent, but showed the
fallback '?' glyph.

Glyphs assigned: graceful_stop=⏹, start=▶, stop=■.

Also update the dashboard.md kind-glyph list to include all nine kinds and
note that deferred start-after-rebuild entries also use parent_id grouping.
2026-07-02 14:15:04 +02:00
iris
5264828091 fix: view-queue link + agent inbox shows unread messages only
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.
2026-07-01 18:41:43 +02:00
iris
95b866a53d refactor(fe): trim >30-line comment blocks (second pass, #2077) 2026-06-29 01:05:53 +02:00
iris
ccc69bbc7b fix(dashboard): contain rebuild spinner within icon boundary 2026-06-29 01:05:34 +02:00
iris
fe6685a155 refactor(fe): fix doc pointer in stream-worker.js to shape.md 2026-06-29 00:17:19 +02:00
iris
878aade8fa refactor(fe): trim impl-history and duplicated-docs prose from comments
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.
2026-06-29 00:12:30 +02:00
iris
c7c4a40e49 fix(builds): remove unused form import (copy-paste residue from core.js) 2026-06-28 23:56:11 +02:00
iris
beb28d5c37 feat(builds): add /builds.html — build lifecycle hub (closes #1999)
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'.
2026-06-28 23:56:11 +02:00
iris
4df286345a refactor(permissions): move ghost-perm detection server-side
Add GET /api/permissions/stale endpoint that returns agent names with
explicit capability/tool-group JSON entries but no live container AND
no kept-state tombstone. Ghost detection is now entirely server-side —
one authoritative call, no client-side roster cache, no staleness window.

The previous client-side approach in core.js made three parallel API
calls (GET /api/capabilities, GET /api/tool-groups, GET /api/state) and
filtered the result against a module-level `liveContainerNames` Set
populated only on cold load and form submits. Any container lifecycle
event (spawn, destroy) while core.html was open left `liveContainerNames`
stale, risking a false-positive ghost entry for a live container.

Changes:
- permissions.rs: add `get_stale_permissions` handler + `StalePermsResponse`
  struct. Computes live roster (containers_snapshot), tombstone set
  (Coordinator::kept_state_names), explicit perm names (capabilities::read
  + tool_groups::read), then returns the difference sorted.
- dashboard.rs: register GET /api/permissions/stale.
- core.js: replace the three-call client-side logic in
  `fetchAndRenderStalePerms` with a single fetch to /api/permissions/stale.
  Remove `liveContainerNames` state + its syncFromSnapshot population.
2026-06-28 23:29:51 +02:00
iris
6ab0757cc6 feat(core): add stale permission entries sub-section to K3PT ST4T3
Adds a "stale permission entries" sub-section within the K3PT ST4T3 pane
on /core.html showing agents with explicit capability/tool-group JSON
entries but no live container — typically renamed or manually-deleted agents
whose entries persisted (e.g. the old "root" manager name after rename to
"ruth").

Each ghost agent gets a "✕ clear perms" button that calls
DELETE /api/permissions/{agent} (added in the prior commit). Lazy-loaded
on first K3PT ST4T3 tab activation; auto-refreshes on capabilities_changed
and tool_groups_changed SSE events.

core.js: track liveContainerNames from /api/state.containers; add
renderStalePerms + fetchAndRenderStalePerms; hook tab onShow + SSE handlers.

system-sections.css: new .tombstones-stale-* selectors for the ghost list
rows and error message.

core.html: add #tombstones-stale-perms div inside the K3PT ST4T3 pane;
expand comment to describe both sub-sections.
2026-06-28 23:29:51 +02:00
iris
4375ab6246 feat(dash): show active model badge on agent cards (closes #2069)
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)
2026-06-27 22:59:40 +02:00
iris
2bfa5bc1a8 feat(schedules): make schedules pausable
Adds pause/resume support for scheduled prompts.

Backend:
- New paused_at_unix column on scheduled_prompts table (added via
  ALTER TABLE migration so existing databases are upgraded on first
  start). The due-rows index is dropped and recreated to also exclude
  paused rows so the worker never fires them while paused.
- Worker's due() query gains AND paused_at_unix IS NULL filter.
- New pause(id) and resume(id) methods on ScheduledPrompts; both are
  idempotent and refuse cancelled rows.
- New POST /api/schedules/{id}/pause and /api/schedules/{id}/resume
  dashboard endpoints (operator-direct, no approval gate). Both emit
  a schedules snapshot on success so the tab updates live.
- WireSchedule gains paused_at_unix: Option<i64> so the frontend can
  render the state without an extra fetch.

Frontend:
- Paused rows render with a distinct row class + muted opacity.
- The next-fire cell shows a yellow pause glyph + tooltip with the
  paused-since timestamp and the would-have-fired time.
- Actions column: pause/resume toggle button (⏸/▶) beside fire/edit/cancel.
  Fire-now is disabled while paused (resume first).
- Sort order: active → paused → cancelled (paused slot keeps schedules
  visible without mixing them into the active top section).
- pauseSchedule() / resumeSchedule() async functions POST to the new
  endpoints and refresh the table on success.
2026-06-27 20:01:16 +02:00
iris
31433da3aa fix(permissions): propagate I/O errors as 500 from delete_agent_permissions
Both remove_agent() calls now run unconditionally for maximum partial
cleanup, but any I/O error is returned as HTTP 500 instead of silently
200-ing — so the frontend's !resp.ok path fires and the operator sees a
meaningful error rather than the stale row reappearing unchanged.

Also add a clarifying comment on isStale in permissions.js explaining
that containersState is keyed from nixos-container list (which includes
stopped-but-configured containers), so a temporarily-stopped agent is
not treated as stale — only destroyed/renamed agents are absent.
2026-06-27 13:44:11 +02:00
iris
a8fb33e2ee fix(permissions): mark stale agents and allow removing their explicit entries
The P3RM1SS10NS tab showed agents that no longer exist in the live
container roster — e.g. an agent named 'root' that was renamed or
destroyed but still had explicit entries in tool-groups.json and/or
capabilities.json. The roster-union behaviour is intentional for
temporarily-stopped agents, but stale entries from renamed/destroyed
agents are confusing.

Backend (dashboard/permissions.rs):
- New DELETE /api/permissions/{agent} handler that bypasses the live-
  roster guard (intentionally — that's the point). Calls
  tool_groups::remove_agent + capabilities::remove_agent to clear both
  JSON files, then emits live SSE snapshots so the tab updates without
  a page reload. Format-checks the agent name but does not require it to
  be in the containers snapshot.

Frontend (permissions.js):
- renderCapabilities / renderToolGroups now cross-reference agentNames
  against containersState (the live roster, already imported). Agents
  not in the live roster get an isStale flag.
- Stale rows get a '(not running)' label and a '✕ remove' button that
  calls clearStaleAgent() — a new async helper that DELETEs the stale
  entry and re-fetches both perm tables.
- Non-stale agents without explicit assignments still get '(default)'.

CSS (dashboard.css):
- .perm-row-stale (reduced opacity), .perm-stale-label (muted small
  text), .perm-remove-btn (small red-bordered button) + disabled state.
2026-06-27 13:44:11 +02:00
iris
145f58b0b1 fix(core): add rebuild-live-log class to live-log container div
The CSS for the rebuild-queue live-log panel targets .rebuild-live-log
(border, border-radius, margin-top, background) but the HTML element only
had id="rebuild-live-log" — no class. As a result the panel box styles
never applied and the live log rendered unstyled (no border, no background,
no visual separation from the queue rows).

Fix: add class="rebuild-live-log" to the element so the CSS selector
matches.
2026-06-27 13:28:23 +02:00
iris
c65201ed7c fix(dashboard): render update_meta_inputs + schedule_prompt approval cards
Both kinds fell through to the spawn branch in renderApprovals, showing
a misleading 'spawn' chip and agent-spawn body text. Mara saw a meta-input
bump render as a spawn card for agent damocles and denied it.

Backend (dashboard.rs):
- Add commit_ref: None to the MergeConfigPr arm (struct was incomplete).
  All arms of ApprovalView now initialise every field.

Frontend (call.js):
- Add isUpdateMeta / isSchedule booleans alongside the existing kind flags.
- Glyph: update_meta_inputs gets ↻, schedule_prompt gets ⏱.
- Kind chip: 'meta-update' / 'schedule' (no kind-spawn class for either).
- Body: update_meta_inputs parses commit_ref as JSON Vec<String> and shows
  'bump flake inputs: foo, bar' or 'bump all flake inputs'; schedule_prompt
  parses SchedulePromptPayload and shows targets + first-fire time + cadence
  + a truncated body excerpt.
- History row: add 'meta-update' and 'schedule' cases (were both 'spawn').
- Import fmtDuration from util.js (needed for schedule cadence display).
2026-06-27 13:03:41 +02:00
damocles
09dc3682d7 fix(#1995): send {} body on schedule cancel-all (was empty body 400) 2026-06-26 01:23:06 +02:00
iris
211dab6f90 fix(dashboard): carry pr_number on live approval_added
Completes the merge_config_pr approval-card link for live-added approvals.
The ApprovalAdded SSE event now carries pr_number (only for
merge_config_pr); applyApprovalAdded was dropping it, so a live-added
merge_config_pr card showed the sha but not the 'review PR on forge' link
until a cold /api/state reload. Carry pr_number through the same way as
sha_short.
2026-06-23 18:44:13 +02:00
iris
efb34ef677 fix(dashboard): drop stale manager destroy-hiding comment + vestigial block
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).
2026-06-23 18:33:45 +02:00
iris
d1125207b4 fix(#1940): render merge_config_pr approval cards
renderApprovals had no merge_config_pr case, so those approvals
mis-rendered as generic spawn cards (spawn chip, no sha, 'container will
be created' body). Add a dedicated branch:
- distinct glyph + 'merge-pr' kind chip
- show the reviewed PR-head sha (sha_short), like apply_commit
- a 'review PR on forge' link built from pr_number (now on ApprovalView),
  gated on forge_present, mirroring the apply_commit 'commit on forge' link
- no config-diff side-panel (apply_commit-only for now)
History rows get the 'merge-pr' chip too.
2026-06-23 17:24:03 +02:00