Commit graph

74 commits

Author SHA1 Message Date
iris
7f97acf19e dashboard: render META INPUTS as a full tree with bulk select
Remove the depth-2 cap in walk_meta_inputs so every fetched input
at every depth is surfaced, not just two levels (issue #275). The
uncapped walk needs a guard: a visited-node set makes it a spanning
tree — each fetched node walked once, at its shallowest path — so
shared subtrees don't re-walk and a cycle can't recurse forever.
A two-pass walk (claim a node's direct inputs before descending)
keeps shallow inputs at a shallow path.

Frontend: renderMetaInputs indents each row by its slash-path depth
and shows the leaf segment (full path on hover), plus a select-all /
select-none control so a long input list isn't ticked box by box.
2026-05-22 23:50:20 +02:00
iris
4a27ef7304 dashboard: derive ctx badge thresholds from the model context window 2026-05-22 22:32:32 +02:00
iris
2f1b846baf dashboard: show meta-update progress in the META INPUTS panel
post_meta_update returns 200 immediately and runs the nix flake
update + agent-rebuild ripple in a background task, so the META
INPUTS panel looked idle for the whole multi-minute window (#259).

Track in-flight runs with a Coordinator atomic counter, exposed via
an RAII MetaUpdateGuard held across run_meta_update. Surface it as
the meta_update_running snapshot field plus a MetaUpdateRunning SSE
event (flipped only when the count crosses 0, so concurrent runs
flip the flag once). The panel shows a pulsing in-progress banner
and disables the update button while a run is active.
2026-05-22 21:54:28 +02:00
iris
a9a10b631f dashboard: show when an approval was requested (closes #272) 2026-05-22 20:06:45 +02:00
iris
15e44955a8 web: clickable links in terminal rows and dashboard messages (issue #233) 2026-05-22 01:17:38 +02:00
damocles
66c481a07a harness+dashboard: declarative dashboardLinks option (closes #191) 2026-05-21 23:20:13 +02:00
iris
ab1f8d6e33 dashboard: icon fallback on real img load failure, not container-state guess 2026-05-21 22:01:38 +02:00
iris
16f614f45d dashboard: dimmed default icon for unreachable containers
A stopped or mid-transient (restarting / rebuilding) container's
web server isn't answering, so its <url>/icon background-image
just failed to an empty box on the card.

When the container isn't reachable (not running, or a transient
is in flight) the icon now falls back to the dimmed hyperhive
mark — /favicon.svg, served by the dashboard itself so it's
always loadable — greyscaled + lowered opacity via the
.icon-unreachable class.

closes #195
2026-05-21 21:56:39 +02:00
iris
f42ba9b561 dashboard file preview: markdown tabs + raster image rendering
Follow-up to #188. Two additions to the side-panel file preview:

- Markdown files get a rendered/plain tabbed view (was: always
  rendered, no way to see source) — same tab pattern as SVG.
- Raster images (png/jpg/gif/webp/bmp/ico/avif) render as an
  <img>. /api/state-file previously from_utf8_lossy-stringified
  every file and served text/plain, which corrupts binary; it
  now serves image files as raw bytes with their real
  content-type (over-cap images are rejected, not truncated —
  a clipped binary is corrupt).

buildSvgPanel generalised to buildTabbedPreview, shared by SVG +
markdown. .svg-host/.svg-render renamed .preview-host/.img-preview
since they now back images + md too.

closes #192
2026-05-21 21:49:15 +02:00
iris
a8ab91ecd8 dashboard: render SVG file previews
SVG files in the side-panel file preview showed only raw source.
Add a rendered/source tabbed view: 'rendered' (default) shows the
image, 'source' shows the markup.

The image loads via an <img> data: URI — <img>-loaded SVG runs in
the browser's secure static mode (scripts + external fetches
disabled), so an untrusted SVG from an agent's state dir can't
execute code in the dashboard origin. Tabs reuse the existing
diff-base-tab styling; a checkerboard backs the image so
transparent regions read clearly.

closes #188
2026-05-21 20:40:30 +02:00
iris
9abcda280a dashboard: full-height square agent icon, icon-left card layout
The agent icon was a 26px <img> inline in the card head, hidden via
onerror when a stopped container's web server didn't answer — which
collapsed the slot and shifted the row.

Restructure the live container card as icon-left / body-right:
- the icon is a background-image div with aspect-ratio 1 and
  align-self stretch — full card height, square, and (being a
  background) it has no intrinsic size, so loading or failing it
  can never reflow the row;
- a failed load (stopped container) falls through to a placeholder
  fill instead of collapsing;
- the three content lines move into a .card-body column.

Tombstone rows keep the plain stacked layout (:not(.tombstone)).

closes #177
2026-05-21 19:19:47 +02:00
iris
32f4796a7f dashboard: re-sync /api/state on SSE (re)connect
The dashboard cold-loaded its derived stores (approvals, questions,
containers, …) from /api/state once, then relied solely on live SSE
events. Events that fired during a disconnect window (reconnect,
hive-c0re restart) are never replayed, so the dashboard drifted stale
until a manual reload.

- terminal.js: add onStreamOpen, fired on every EventSource open
  (initial + reconnect); the dashboard wires it to refreshState() so
  every connection epoch re-syncs the authoritative snapshot.
- terminal.js: seq-dedupe only event kinds that actually appeared in
  the history replay. Mutation events are never in /dashboard/history,
  so deduping them against the broker-history seq wrongly dropped ones
  that fired between the /api/state snapshot and the history fetch.
- app.js: make applyApprovalResolved / applyQuestionResolved
  idempotent (guard the history unshift by id) so a re-sync
  overlapping a live event can't double a history row.

closes #163
2026-05-21 18:25:42 +02:00
iris
189fc587a4 fix: handle init_config approval kind in row deserializer
row_to_approval matched only apply_commit + spawn, so any approvals
row with kind=init_config (added by 80dd5bb's two-step spawn) failed
to deserialize. pending() / recent_resolved() collect all-or-nothing
via collect::<Result<Vec>>(), so one bad row errored the whole query;
api_state's log_default then swallowed the error and returned an empty
list — every pending approval vanished from the dashboard (issue #160).

- add the missing init_config arm to row_to_approval
- collect_lenient(): skip + log unparseable rows so a single bad row
  can never blank the whole approvals list again
- dashboard: label init_config approvals 'init' (was mislabeled
  'spawn' by the apply-vs-other fallthrough)

closes #160
2026-05-21 18:14:53 +02:00
iris
62aa3bb3ec agent icon: render on dashboard + per-agent web UI
Consumes the GET /icon endpoint from #139:

- Dashboard: each container card shows the agent's icon next to its
  name (26px). Loaded from <agent-url>/icon; onerror hides it for a
  stopped container whose web server isn't answering.
- Per-agent web UI: the agent's icon next to the page title (40px),
  and /icon as the favicon on the index, stats, and screen pages.

/icon always returns an image (configured SVG or the default
hyperhive logo), so no presence check is needed.

Closes #140
2026-05-21 15:36:58 +02:00
iris
b1f10b1d1b render message reply threads in dashboard and per-agent inbox
- MessageEvent and DashboardEvent Sent/Delivered now carry id and in_reply_to
- broker.send() includes last_insert_rowid in the emitted event
- recent_all() and recv_batch() include id and in_reply_to from the DB
- deliver_reminders_batch() tracks per-row rowids within the transaction
- dashboard message flow: reply rows are indented with a border-left and a
  clickable '↳ reply' tag that scroll-jumps + briefly highlights the parent
- per-agent inbox: reply messages get a '↳ reply ·' prefix and indent

Closes #26
2026-05-20 15:29:47 +02:00
iris
804875d670 surface rate_limited status as red badge on per-agent page and dashboard
- add rate_limited: Arc<AtomicBool> to Bus; set/cleared by emit_status
- write/remove sentinel file hyperhive-rate-limited in state dir so host-side
  dashboard can detect it without a live socket call
- api_state returns status=rate_limited when flag is set (cold-load accurate)
- ALIVE_LABELS gains rate_limited entry (⊘ red chip) on per-agent page
- ContainerView gains rate_limited: bool read from sentinel file
- dashboard container row shows ⊘ rate limited badge (red) ahead of needs_login

Closes #24
2026-05-20 15:16:00 +02:00
iris
808b9cbe1a extract ctx token thresholds into named constants 2026-05-20 15:05:42 +02:00
iris
270ef19920 dashboard: add per-agent ctx-window usage chip to container rows
Reads the most recent turn's context-window token count directly from
each agent's hyperhive-turn-stats.sqlite (same path the host-side
stats_vacuum uses). Adds ctx_tokens: Option<u64> to ContainerView;
populated in build_all via a single best-effort SQL query.

Dashboard app.js renders a 'ctx·Nk' badge colour-coded by harness
watermarks: green <100k (safe), yellow 100-150k (near auto-reset),
red ≥150k (compact territory). Badge only shown when ctx_tokens
is present (agent has run at least one turn).

Closes #17
2026-05-20 15:01:28 +02:00
müde
49f4e9cc89 dashboard: forge-linked config + approval card + 3-way diff base
- forge nix option moves to hyperhive.forge.enable, defaults true;
  hive-c0re imports the forge module so it's on by default.
- drop the agent.nix container-row viewer + /api/agent-config; link
  to the agent-configs forge repo instead.
- restructure pending approvals into a card (identity header /
  what-changed body / decision actions) with a link to the proposal
  commit on the forge.
- diff opens in the side panel with a 3-way base toggle: vs applied
  (running) / vs last-approved / vs previous proposal, served by the
  new /api/approval-diff/{id}?base= endpoint.
2026-05-20 11:22:28 +02:00
müde
0c62bbf1cd dashboard: render markdown file previews in the side panel
clicking a .md / .markdown path reference now opens a marked-rendered
view in the slide-in panel instead of raw text; other files stay raw
in a <pre>. serves the vendored marked bundle at /static/marked.js and
scopes a .md stylesheet to the panel body.
2026-05-20 11:01:16 +02:00
müde
7ce3da1e21 dashboard: open long content in a slide-in side panel
file previews, approval diffs, journald logs and applied config no
longer expand inline — they open in a drawer that swipes in from the
right, with a title naming what's open and a close button (esc /
backdrop also close). path references in messages become plain inline
links that open the file in the panel; the sibling-<details> dance in
appendLinkified is gone.

also: the question-answer free-text field is now a textarea — enter
submits, shift+enter inserts a newline.
2026-05-20 10:43:23 +02:00
müde
d3f90f4cc0 stats: per-agent /stats page with chart.js trends + breakdowns
new hive-ag3nt::stats module reads turn_stats.sqlite read-only and
aggregates over 24h/7d/30d windows (hourly/daily buckets) — turn
rate, p50/p95/avg duration, ctx tokens (avg/max), cost token
components, top tools, wake mix, result mix. served by the agent
itself so per-MCP extensions can register more providers without
the host knowing their schemas.

/stats route + /api/stats?window=... on the per-agent web ui.
chart.js v4.4.4 pulled from jsdelivr (SRI hash deferred). nav
links: 📊 chip on the dashboard container row + 📊 stats → on
the per-agent header.

todo housekeeping: softened damocles-area note at the top,
new reverse-proxy + deferred reminder-rollup items, removed
the two telemetry-ui items absorbed by this page.
2026-05-19 00:27:01 +02:00
müde
978a3cf391 reminders: persist + surface delivery failures
Broker schema gains attempt_count INTEGER + last_error TEXT
columns via idempotent ALTER TABLE migration (pragma-probed so
fresh + existing dbs converge). reminder_scheduler::tick calls
record_reminder_failure on every deliver_reminder error,
bumping the counter + stashing the message. get_due_reminders
filters out rows where attempt_count >= MAX_REMINDER_ATTEMPTS
(5) so the scheduler stops retrying a stuck row until the
operator intervenes.

new POST /retry-reminder/{id} → reset_reminder_failure clears
the counters; next 5s tick re-attempts. cancel-reminder
unchanged (hard-delete).

dashboard renders failed rows with a red left rule, the error
text inline, and a ⚠ N failed badge. ↻ R3TRY button appears
when attempt_count > 0 — sits next to ✗ C4NC3L in a small
actions row below the body.
2026-05-18 00:08:09 +02:00
müde
4ec401a6c7 question/answer text: server-side file_refs
DashboardEvent::QuestionAdded gains question_refs and
QuestionResolved gains answer_refs — both populated via
scan_validated_paths at emit time, same helper the broker
forwarder uses for Sent/Delivered. cold-load snapshot wraps
each OpQuestion in QuestionView with the same fields computed
once per /api/state.

client threads refs through questionsState rows (pending +
history) and passes them to appendLinkified at every render
site (live pane, history details). path tokens in question and
answer bodies now linkify with the same server-vouched
guarantee broker messages already enjoyed.
2026-05-17 23:54:35 +02:00
müde
087a5366fb container row: pending-reminder count chip ( N)
ContainerView gains pending_reminders: u64; computed during
build_all via Broker::count_pending_reminders_for, mapping
manager → MANAGER_AGENT recipient + sub-agents → logical name.
Updates on every rescan (mutation sites + crash_watch's 10s
poll); accept 10s staleness on background remind / scheduler
delivery — live updates on operator cancel via /api/state path.

client renders a small cyan chip on the row when the count > 0;
tooltip points the operator at the reminders section to view
or cancel.
2026-05-17 23:52:56 +02:00
müde
aed43ce4df dashboard: tombstones + meta_inputs events — last /api/state refetches drop
new DashboardEvent::TombstonesChanged + MetaInputsChanged carry
full snapshots (lists are tiny; snapshot beats diff for race
avoidance). Coordinator-side helpers
emit_tombstones_snapshot + emit_meta_inputs_snapshot fire from
every mutation site: actions::destroy + post_purge_tombstone +
actions::approve (spawn finalise consumes tombstone) +
run_meta_update + auto_update::rebuild_agent (lock bumps).

client adds derived stores + apply* handlers + drops the
post-submit refetch on PURG3 (container row + tombstone row)
and meta-update.

after this commit /api/state is fetched exactly once per page
session (cold load); every other change rides the SSE channel.
2026-05-17 23:52:12 +02:00
müde
76e4034e01 path linkify: server attaches file_refs at message ingest
drop the /api/state-file/check probe endpoint (which let any
dashboard visitor enumerate filesystem layout by feeding paths)
and the client's optimistic-then-downgrade dance. instead, the
broker forwarder calls scan_validated_paths(body) — same
allow-list helper as the read endpoint — and attaches the
verified file tokens to DashboardEvent::Sent/Delivered as
file_refs: Vec<String>. /dashboard/history backfill does the
same per-row.

client appendLinkified takes a (text, refs) pair, walks
left-to-right linkifying every occurrence of any ref token,
longest-first tie-break. no regex, no probe, no cache, no
queue. when refs is empty/absent the body emits as plain text
(question/answer/reminder rendering — refs for those are a
follow-up).

operator inbox stores file_refs from the sent event so its
renderer gets the same anchors as the message-flow terminal.
2026-05-17 23:44:50 +02:00
müde
6e098fad29 path linkify: server-side validation via /api/state-file/check
regex back to permissive ("looks like a path") — the server is
authoritative on whether each match is a file. anchors render
optimistically, paths queue for batch validation (50ms coalesce),
non-files downgrade to plain text + the sibling <details>
preview is dropped. session-scoped cache (pathValidity Map) so
repeated paths skip the roundtrip.

new endpoint POST /api/state-file/check accepts { paths } and
returns { results: {<path>: bool} }. shares resolve_state_path
helper with the read endpoint so security rules can't drift —
both refuse anything outside the allow-list, anything resolved
outside via symlink, or anything in a per-agent subdir other
than state/. capped at 64 paths/request.

drops the brittle client-side filename heuristic (the .ext-
required rule that missed README/Makefile and still matched bare
dirs without trailing slash). single source of truth.
2026-05-17 23:36:44 +02:00
müde
0e2d26304e path linkify: require last segment to look like name.ext (skips dir refs) 2026-05-17 23:32:37 +02:00
müde
e772182724 path linkify: skip trailing-slash (directory) matches 2026-05-17 23:25:51 +02:00
müde
1db6b8ffed dashboard: queued reminders surface
new 'qu3u3d r3m1nd3rs' section between approvals and operator
inbox. lists every pending reminder with agent, due-relative
timestamp, body, payload path (path-linkified), and a cancel
button. drives off a new /api/reminders endpoint and a
POST /cancel-reminder/{id} that hard-deletes the row.

failure surface (last_error / attempt_count + retry) deferred —
needs a sqlite migration; tracked in TODO.md.
2026-05-17 22:10:02 +02:00
müde
cb71a07300 dashboard: clickable file-path previews
agents constantly emit pointer strings to /agents/<n>/state/foo.md
since broker bodies cap at 1 KiB. now those tokens linkify in the
message flow, question bodies, answer text, and operator inbox;
clicking expands an inline <details> that lazy-fetches via the
new /api/state-file?path=... endpoint.

endpoint allow-list: per-agent state dirs + shared docs, both
in their container-mount form (/agents/<n>/state, /shared) and
host form (/var/lib/hyperhive/...). 1 MiB read cap; canonicalises
before the prefix check so `..` / symlinks can't escape.

legacy bare `/state/...` is deliberately not matched — ambiguous
from the host's perspective (we'd need to know which agent the
message references to translate). agents should use the qualified
form going forward.
2026-05-17 22:08:15 +02:00
müde
a15fafb5de dashboard: surface peer questions + operator override
questions pane now shows both operator-targeted threads
(target IS NULL) and agent-to-agent threads (target = some
agent). filter chips above the list: all / @operator / @peer /
per-participant. peer rows get a mauve left rule + a 0V3RR1D3
button that POSTs the same /answer-question endpoint
(OperatorQuestions::answer already permits the operator as
answerer on any target).

wire changes: OperatorQuestions gains pending_all +
recent_answered_all; QuestionAdded + QuestionResolved events
carry target: Option<String>; emit sites drop their
target.is_none() guard. answered-history rows show the
answerer prefix so override answers are auditable at a glance.
2026-05-17 22:06:53 +02:00
müde
e7ce35c503 phase 6: container events + drop the 5s /api/state poll
new DashboardEvent::ContainerStateChanged + ContainerRemoved
close the last refetch loop on the dashboard. Coordinator's
rescan_containers_and_emit diffs a fresh container_view::build_all
against a cached last_containers map and fires per-row events.
called from actions::approve (post-spawn), actions::destroy,
the lifecycle_action wrapper, auto_update::rebuild_agent, and
the existing 10s crash_watch poll.

ContainerView extracted to its own module so coordinator and
dashboard can both build it. dashboard endpoints flip to 200;
container-lifecycle forms carry data-no-refresh. client drops
the periodic poll entirely — initial cold load + SSE for
everything afterwards. pending overlay reads from the existing
transientsState since the new event payload doesn't carry it.

PURG3 + meta-update keep the post-submit refetch since
tombstones + meta_inputs aren't event-derived yet; tracked in
TODO.md.
2026-05-17 22:01:15 +02:00
müde
f559441a06 dashboard: /approve, /deny, /answer-question, /cancel-question, /request-spawn return 200; matching forms opt out of refetch 2026-05-17 14:26:53 +02:00
müde
7956e1c627 dashboard: transient_set / transient_cleared mutation events + client derived state 2026-05-17 14:20:51 +02:00
müde
1879b2f485 dashboard: question_added / question_resolved mutation events + client derived state 2026-05-17 13:33:02 +02:00
müde
56d615b51f dashboard: approval_added / approval_resolved mutation events + client derived state 2026-05-17 13:30:25 +02:00
damocles
82b0877c47 ask: rename ask_operator → ask + optional 'to' for agent-to-agent Q&A 2026-05-17 13:20:32 +02:00
müde
616ca38199 dashboard: /op-send returns 200; client relies on SSE for visual update 2026-05-17 12:41:04 +02:00
müde
a478792914 dashboard events: unified coord channel + /dashboard/{stream,history}; broker forwards 2026-05-17 12:39:48 +02:00
müde
d48cee7c2d approvals: ship raw diff text instead of pre-rendered html; client classifies per-line 2026-05-17 12:30:45 +02:00
müde
fb669c17c8 dashboard: derive operator inbox from message stream (drop snapshot field + refetch workaround) 2026-05-17 12:28:04 +02:00
müde
8c186d4fb7 dashboard: msgflow uses shared terminal + backfill via /messages/history 2026-05-17 11:56:29 +02:00
müde
9703753a4c dashboard: fan out op-send when recipient is * 2026-05-17 11:09:20 +02:00
damocles
6ba4241a45 show answered question history on dashboard 2026-05-17 01:41:59 +02:00
damocles
4a8a668348 feat: add optional description to request_apply_commit and request_spawn 2026-05-16 15:18:32 +02:00
müde
fe8fb15f8f Revert "dashboard: 3-column layout — swarm / 0per4t0r 1n / m3ss4g3s"
This reverts commit 74ba8a63e1.
2026-05-16 03:54:02 +02:00
müde
74ba8a63e1 dashboard: 3-column layout — swarm / 0per4t0r 1n / m3ss4g3s
regroups the 7 stacked sections into three semantic columns
backed by a CSS grid (single column under 1400px, 3 columns
above). column headers are sticky so vertical scrolling
inside a column doesn't lose context.

- SW4RM (left, slightly wider): containers + kept-state +
  spawn-agent form + meta-input update form. all
  swarm-mutating operator knobs live here.
- 0PER4T0R 1N (middle): mind-questions + pending approvals.
  the two things waiting on operator action.
- M3SS4G3S (right): operator-inbox + msg-flow tail + the
  @-mention compose box. broker traffic in one place.

spawn form moves out of renderApprovals into static HTML
under sw4rm; renderApprovals no longer injects it.

cosmetic: per-section h2/divider replaced with smaller cyan
sub-heads + a dashed underline so each column reads as one
cohesive unit instead of seven competing banners. body
max-width grows 70em → 110em to actually use the new
horizontal real estate.
2026-05-16 03:47:16 +02:00
müde
266c2c7a77 dashboard: meta flake inputs UI + sequential rebuild loop
new section 'M3T4 1NPUTS' between approvals and message flow:
one row per input in meta/flake.lock (hyperhive first, then
agent-<n> alphabetically). each row shows the input name, the
first 12 chars of the locked sha, a relative timestamp from
locked.lastModified, and the original.url when available.
checkbox per row; submit button is disabled until at least one
box is checked; submitting confirms then POSTs the selected
names to /meta-update.

backend:
- meta::lock_update(inputs: &[String]) — runs 'nix flake update
  <names>' in the meta dir, commits the lock change with a
  combined message ('lock update: hyperhive, agent-coder').
  preserves the existing META_LOCK serialization. existing
  lock_update_for_rebuild / lock_update_hyperhive stay for
  their single-input callers.
- POST /meta-update — comma-separated 'inputs' form field
  (JS joins checkboxes since axum::Form doesn't natively
  decode repeated keys); spawns a background task that runs
  the lock update + per-agent rebuild loop. hyperhive
  selection fans out to all agents; agent-<n> selection only
  rebuilds <n>. each rebuild fires Rebuilt to the manager
  exactly like dashboard / admin-CLI / auto-update.

rebuild loop is sequential — auto_update::run too (was
parallel via tokio::spawn). parallel rebuilds collide on
nix-store's sqlite cache ('sqlite db busy, not using cache')
and the meta META_LOCK contention. nix-daemon serializes the
heavy build steps anyway, so this isn't a throughput loss.
2026-05-16 03:38:07 +02:00