Commit graph hyperhive/frontend
Author SHA1 Message Date
iris
e7f4a19939 swarm-ui: real hive-roster overview page
Fixes hyperhive#3223.

swarm-controller: GET /api/hives (utoipa-annotated same as /health),
serving the swarm's hive directory (name + domain) loaded once at
startup from a new SWARM_CONTROLLER_HIVES env var. The controller's
NixOS module sets it from services.hyperhive.swarm.hives, JSON-encoded
the same way hive-c0re already builds HYPERHIVE_PEERS for its own peer
list (environment.nix) — the full directory here rather than
peers-minus-self, since a swarm-level daemon has no 'self' hive to
exclude. Unset/malformed both fall back to an empty list with a
warning rather than failing startup, so /health stays answerable even
if this one env var is wrong.

swarm-ui: App.tsx's Home route fetches /api/hives and renders it
through the already-merged <Table>/<StatusChip>/<Panel> primitives —
name, domain (linking out to that hive's own gateway-routed
dashboard), and a static "configured" status chip until a real
online/stale/offline rollup exists server-side. Also gave swarm-ui a
base <a> color (theme's --blue) — base.css covers body/typography but
not links, and this is genuinely page-level rather than any one
component's concern.

Verified end to end, not just source-reading: ran the real
swarm-controller binary with SWARM_CONTROLLER_HIVES set, curled
/api/hives + /health over its actual unix socket; separately served
the real swarm-ui dist against a mock /api/hives and screenshotted the
rendered table. Also re-verified the nginx wiring evaluates (same
throwaway nixosSystem eval technique as #3212) — SWARM_CONTROLLER_HIVES
resolves to the expected JSON shape.

cargo test/clippy -p swarm-controller clean (2 tests, including a new
load_hives one covering missing/malformed/valid env var states). npm
run build + typecheck clean.
2026-08-13 11:21:30 +02:00
iris
8642d4acf6 swarm-ui: colocate component CSS as JS-side-effect imports
Per mara's review question on PR#3219 ('shouldnt the jsx files import
their css?'): each component now does its own import ('./Shell.css',
'./Panel.css', ...) instead of swarm-ui.css centrally @import-ing
every component's stylesheet. esbuild folds every .css reachable from
main.tsx's import graph into one main.css companion output next to
main.js — no separate build step, this is bundle:true's existing
behavior, just not exercised until now.

swarm-ui.css keeps only the shared base reset (@hive/shared/base.css)
since that isn't any one component's concern. Added src/css.d.ts
(ambient '*.css' module) since tsc otherwise rejects a side-effect
import of a non-JS/TS specifier.

Side benefit: a component nothing imports (yet) no longer ships its
CSS either — StatusChip/Table aren't referenced from App.tsx today,
and main.css correctly only carries Shell.css + Panel.css. The old
central-import approach shipped all four unconditionally.

npm run build + typecheck clean. Re-screenshotted the real dist —
pixel-identical to before this change.
2026-08-12 21:33:44 +02:00
iris
55f9e6c15f swarm-ui: one subdirectory per ui/ component
Per mara's PR review request: Panel/StatusChip/Table each move into
their own subdir (ui/panel/, ui/status-chip/, ui/table/) colocating
the component with its stylesheet, matching shell/ (Shell.tsx +
Shell.css already lived this way). Import paths in App.tsx and
swarm-ui.css updated to match; no behavior change.

npm run build + typecheck both clean.
2026-08-12 21:33:44 +02:00
iris
67eb0c660d swarm-ui: page shell + layout primitives
Structural foundation split out of hyperhive#3118 per mara's steer:
structure first so the real overview page (hive roster) and later
routes (swarm-wide agent roster) land as content changes rather than
each reinventing chrome + nav + a table/panel/chip shape.

- <Shell>: header bar (branding) + nav row, wraps every route. Route
  list lives in Shell itself (one small SPA, one place to know its
  own nav).
- ui/Panel, ui/StatusChip, ui/Table: the three primitives the
  overview page's actual scope (hive roster: name/domain/status,
  linking out to each hive's own dashboard) calls for, nothing
  speculative beyond that.

Preact-native styling (plain CSS files imported via swarm-ui.css, no
shadow DOM — this package renders into light DOM) — not
@hive/shared's chrome.css, which is the old MPA dashboard's visual
language. Same base16/Catppuccin color tokens via theme.css/colors.css
so it still reads as hyperhive.

npm run build (whole frontend workspace) + npm run typecheck both
clean. Verified with a real headless-chromium screenshot against the
built dist, not just source-reading.

Fixes hyperhive#3211
2026-08-12 21:33:44 +02:00
iris
4b71a76b12 agent: drop dead .header-pill-loose CSS
Styled variant with zero HTML/JS reference anywhere in the frontend
tree, found while surveying pill/chip/badge sites for the "extract
pill component" issue. The extraction it was originally paired with
(PR#3197) got closed as not worth the churn for 2 instances, but this
one finding stands on its own — no reason to carry dead CSS forward.

npm run build clean.
2026-08-12 19:33:07 +02:00
iris
8dd6d05d6c frontend: unify pill/chip/badge shape into shared CSS
Per mara's steer on #3053 ("chip/pill/badge is the same if you squint
... theme unification is part of the goal" then "make it common css
instead of component, thats fine. but make them look unified (not as
much per usage css)"): shared CSS, not a JS component.

New @hive/shared/pill.css defines two classes, `.hive-pill` (primary
state chips) and `.hive-pill-sm` (secondary meta chips) — border/
border-radius/padding/font-size/letter-spacing (colour stays per-site,
the meaningful semantic part). Every render call site across dashboard
(swarm.js/core.js/builds.js) and agent (index.html/app.js) now carries
one of the two shared classes directly, alongside its own existing
semantic-colour modifier class.

Second cut of this PR, per argus's approve + mara's follow-up review
comment on the first cut: the first version instead enumerated every
legacy classname (`.badge`, `.status-badge`, `.header-pill`, etc.)
straight into pill.css's own selector groups so no call sites needed
touching. Mara's correction: that just relocates the duplication
rather than removing it, and the shared CSS shouldn't have to keep
naming every consumer. This version does the real rename instead.

Most visible consequence, unchanged from the first cut: dashboard's
`.badge` family moves off its own shape (2px square corners, uppercase,
tighter padding) onto the shared rounded-pill shape + agent's "sm" tier
sizing. `npm run build` clean across all three packages; verified the
compiled bundles carry the new classnames at every call site (dashboard
JS, agent index.html + app.js), not just the source tree.

Fixes #3053
2026-08-12 19:25:45 +02:00
iris
92b9e67327 frontend: unify pill/chip/badge shape into shared CSS
Per mara's steer on #3053 ("chip/pill/badge is the same if you squint
... theme unification is part of the goal ... make it common css
instead of component, thats fine. but make them look unified"):
shared CSS, not a JS component.

New @hive/shared/pill.css defines the shape (border/border-radius/
padding/font-size/letter-spacing — color stays per-site, that's the
meaningful semantic part) in two tiers matching what was already
organically in use: `.hive-pill` (primary state chips) and
`.hive-pill-sm` (secondary meta chips). Every pre-existing classname
that drew its own copy of this shape (dashboard's `.badge` family,
agent's `.status-badge`/`.state-badge`/`.header-pill`/`.ctx-badge`/
`.model-chip`/`.effort-chip`) is folded straight into the same
selector groups, so no markup or JS changes were needed anywhere —
every render call site keeps constructing the exact same classnames
it always did.

Most visible consequence: dashboard's `.badge` family moves from its
own shape (2px square corners, uppercase, tighter padding) onto the
shared rounded-pill shape + agent's "sm" tier sizing, matching the
"look unified" ask directly. `npm run build` clean across all three
packages.
2026-08-12 19:25:45 +02:00
iris
7d1b18d2c8 swarm-ui: bootstrap new swarm-level frontend (Preact + wouter + TS + JSX)
Project-bootstrap scope per the issue: static build as a nix pkg,
empty start page for now, functionality deferred until auth against
authelia is figured out. Stack (Preact + wouter + TypeScript + JSX)
matches the shell decision from the earlier framework-paths thread —
a real SPA shell with a router and deep links, distinct from the
per-hive dashboard's vanilla-JS + custom-element MPA.

- New npm workspace frontend/packages/swarm-ui: one route (/), a
  wouter Switch/Route shell, a 404 fallback. Reuses @hive/shared's
  colors.css/theme.css/base.css for visual consistency; no other
  shared JS (the vanilla-JS el()/dom.js helpers are superseded by
  Preact in this shell).
- nix/packages/swarm-ui.nix: its own buildNpmPackage derivation
  (scoped to just this workspace via an explicit buildPhase), not
  folded into nix/packages/frontend.nix's packages.default closure —
  same reasoning swarm-controller/swarmctl already use for staying out
  of daemonBins: a hive that doesn't run the swarm controller
  shouldn't carry swarm-ui bytes.
- npmDepsHash recomputed in both frontend.nix and swarm-ui.nix (same
  shared lockfile, new deps: preact, wouter-preact, typescript).
- Added swarm-ui to nix/checks.nix alongside frontend, for the same
  FOD-staleness reason plus being the only thing that actually builds
  it in CI (not in packages.default's closure like frontend is, so
  nix flake check wouldn't otherwise touch it).
- npm run typecheck (tsc --noEmit) is available locally; not yet wired
  into CI — esbuild transpiles TS without type-checking, so that's a
  real gap, left as a follow-up rather than growing this bootstrap PR
  with a new CI workflow step.

Verified: nix build .#swarm-ui and .#frontend both succeed; npm run
build (root, all workspaces) succeeds; tsc --noEmit clean;
scripts/check-issue-refs.sh clean.
2026-08-11 21:31:44 +02:00
iris
5bd085fbac web-ui: expose per-agent paused status, add pause/resume to the agent page's own overflow menu
hive-agent's own web_ui module never exposed the agent's own paused
status to its own /api/state — the dashboard's cross-container view
knew it, but a per-agent page had no way to know it's paused. Added
StateSnapshot.paused (a direct stat of the same harness-local pause
marker hive-c0re's Coordinator::is_paused checks).

The per-agent page's ⋯ overflow menu now has a pause/resume item that
POSTs to hive-c0re's existing /api/pause/<name> / /api/resume/<name> —
the same endpoints the dashboard's <hive-agent-menu> already uses,
same cross-origin form-submit pattern the existing rebuild-container
item uses. The item's label tracks state.paused on every /api/state
refresh so a pause/resume triggered from the dashboard while this page
is open doesn't leave a stale action showing.
2026-08-11 20:42:53 +02:00
iris
ecf9ff4d80 web-ui: remove dead ask->operator inline-answer binding from the per-agent terminal
hyperhive#2922: the inline-answer slot (pendingAskBinds/reconcileAskBinds/
buildAnswerForm) depended on a since-removed /api/loose-ends endpoint
and had been silently non-functional the whole time — nothing ever
reassigned lastLooseEnds, so the reconciler always no-op'd.

Per mara's call on the issue (ask/answer is deprecated per #2850, only
the main dashboard UI needs to work, remove the broken per-agent inline
view): ripped out the dead JS (binding + form + CSS) rather than
restoring the endpoint. An ask tool call now renders like any other
tool call, no inline answer affordance; the operator answers via the
dashboard's own question surfacing.

Also fixed adjacent doc staleness this same removal made newly
contradictory (docs/web-ui/agent.md, docs/terminal-rendering.md):
the doc already described the loose-ends/bash-tasks flyouts and the
ask-binding as recently dead, but the endpoint reference table and a
header-pills bullet list still described them as live.
2026-08-11 20:21:52 +02:00
iris
b785f96d30 dashboard: distinct "gave up" badge for a crash-looped container
Frontend half of the two-PR split on the systemd restart-bound work
(clause 2): once a container's unit hits systemd's bounded restart
limit and stops on its own, that read exactly like a deliberate stop
("not running") — no way to tell "gave up" from "off on purpose".

Renders ContainerView.failed as a distinct red "gave up" badge on the
container row, in place of the muted "not running" badge. Both states
have running: false; failed is the new orthogonal fact that tells them
apart, same "independent flags, no state machine" shape as
paused/needs_update/needs_login. An older backend without the field
serves failed: undefined, which reads falsy, so this degrades cleanly
to the existing single "not running" badge.

Frontend-only — no Rust changes. Safe to merge in either order
relative to the backend PR carrying ContainerView.failed itself.
2026-08-10 23:14:36 +02:00
iris
1e13b88c8c jobq graph: state filter on /api/jobq/graph + multi-select checkboxes
GET /api/jobq/graph gains a states query param (comma-separated
hive_jobq::State names): narrows the served root groups to the named
states, keeping a group whole (filtering by a root's own state, which
is already its subtree's rolled-up answer). Absent, empty, or fully
unrecognised is the identity filter, matching prior behaviour.

hive-jobq-graph.js gains a row of per-state checkboxes above the tree,
re-fetching the endpoint with the selection on toggle. Default
selection hides Done and Skipped.

Server-side filtering (not client-side hiding) so hive-jobq-graph-update's
node list, and everything downstream of it in builds.js (count pill,
live-log panel), only ever sees what's actually shown.
2026-08-10 23:05:03 +02:00
iris
e525dcb6d4 agent icon: 404 when unconfigured, client-side fallback
hive_sh4re::assets::branding_svg() resolved a server-side default
icon at runtime from HIVE_ASSETS_DIR — the only consumer was
serve_icon(), which fell back to it whenever the agent had no
`hyperhive.icon` override. Removed both the fallback and the
function: serve_icon() now 404s when /etc/hyperhive/icon.svg is
absent, and the per-agent web UI (app.js) picks up the existing
dashboard swarm.js pattern — swap the <img> src to the
frontend-bundled /favicon.svg on load failure, guarded against
looping if the fallback itself 404s.

Updated the doc/comment claims that said the server always returns
an image (docs/web-ui/agent.md, nix/agent-modules/default.nix, the
hive-c0re/forge/users.rs comment referencing the old shared-asset
set). forge-avatar-sync and the matrix avatar sync are unaffected —
both are gated on hyperhive.icon != null and never depended on the
removed fallback.
2026-08-10 20:59:59 +02:00
iris
aa14339be7 jobq-graph: re-add per-node cancel button
Fixes #3067.

<hive-jobq-graph> gains a `cancellable` attribute: any non-terminal
node (Pending/Running/Finishing) gets a small cancel button, and a
click dispatches `hive-jobq-graph-cancel` (`detail: { id }`) rather
than POSTing anything itself -- which endpoint actually cancels a
node is the host's domain concept, same "push data out, host decides"
shape `hive-jobq-graph-update` already uses.

builds.js turns it on for R3BU1LD QU3U3, confirms via themedConfirm,
then POSTs the existing `/api/rebuild-queue/{id}/cancel` endpoint.
No manual refresh needed -- cancelling flips node state, which
already fires rebuild_queue_changed over SSE, and the page's existing
handler for that tick already calls jobqGraphEl.refresh().

Also removed ~130 lines of dead `.rqe-*` CSS in system-sections.css
left over from the bespoke pre-<hive-jobq-graph> queue renderer
(confirmed zero JS references before deleting each rule; kept the
still-used `.rqe-kind`/`.rqe-agent`/`.rqe-source*`).

docs/web-ui/dashboard.md's R3BU1LD QU3U3 section updated to match
current behaviour (cancel button, waits-on text instead of the old
"no per-node actions" note, sibling order no longer implies anything
since deps render as text not a reordered rail).
2026-08-09 17:40:39 +02:00
iris
a858739b28 jobq-graph: replace the dep-edge gutter rail with a plain text line
mara reported the rail still didn't make dependencies visible
(screenshot on the PR). Root cause: the rail spans by sibling-array
position, but a sibling with its own nested subtree renders many
pixel-rows for one array slot -- in a real queue (agent_window with
7-ish children between two top-level siblings), the "line" breaks
into disconnected ticks around every real subtree, never reading as
a connection at all. My verification fixtures never tested a nested
subtree sitting between two related siblings, so this never showed up
before.

Replaced with a "waits on: <label>" text line under the dependent
node, resolved once in buildTree via a global id lookup (not scoped to
siblings, so a label still resolves even if a dep ever does cross a
group boundary despite the product decision that it shouldn't). Text
has no positional-gap failure mode: it's legible regardless of how
tall the node above or below it renders. No reordering, no rail, no
interleaving-of-independent-pairs class of bug possible anymore --
this is close to the original design proposed on the issue before the
heavier visual version was tried.

Re-verified against 6 fixture checks including the exact shape from
the screenshot (a dependency target with its own nested subtree
rendered between it and the dependent node).
2026-08-09 17:33:00 +02:00
iris
6f87821110 jobq-graph: scope dep-edge reorder per connected component
Fixes a real bug argus caught: the flat batch-round topo sort could
interleave two fully independent dep pairs in the same sibling list
(e.g. W, X after_ok(W), Y, Z after_ok(Y) reordered to W, Y, X, Z), and
the single-column rail then drew one continuous line across rows that
have no relationship at all.

Reorder is now scoped per connected component of the local dependency
graph -- each component renders as a contiguous block (first-seen
order, so an already-correct list doesn't reorder needlessly), so two
unrelated pairs can never weave together. Within one component,
overlapping ranges are still correct: they mean the nodes really are
related (a diamond, for instance).

Also: a pass-through row's tooltip now names what's passing through it
(not just the edge it's itself an endpoint of) -- addresses the same
disambiguation gap argus flagged as a secondary note.

Re-verified against a wider fixture set including the exact
interleaving case from the review (17 checks: prior 6 unaffected +
argus's regression case, a shuffled-order variant, three simultaneous
independent pairs, and a genuine diamond that's expected to overlap).
2026-08-04 19:54:40 +02:00
iris
86a7a62519 jobq-graph: render Node-kind dep edges between siblings
<hive-jobq-graph> only ever drew the parent/child containment tree --
a dependency between two sibling nodes (same parent, e.g.
prebuild.after_ok(meta_sync)) was invisible on screen. Confirmed with
the operator that a Node-kind dep never crosses a group boundary
(always a sibling under the same parent), so this is a purely local
problem per sibling list, not a whole-graph layout question.

Each sibling list is reordered dependency-first (stable topo sort,
falls back to original order on ties or an unexpected cycle) and gets
a small connecting rail in its left gutter marking dep edges, with a
tooltip naming what a waiting node is blocked on. Groups with no deps
render exactly as before -- no extra markup, no cost.

Verified the ordering + rail-classification logic standalone against
constructed fixtures (9 + 12 checks) before trusting it in the real
component.
2026-08-04 19:54:40 +02:00
damocles
642377c5e0 docs: stop implying matrix/bash/forge is a closed todo-producer list 2026-08-04 00:06:19 +02:00
atlas
f707c60f90 jobq: delete DagView/NodeView, the second projection of one graph
Two views of the same graph existed: the typed `DagView`/`NodeView`
(`/api/state.rebuild_queue`, the `QueueDag` socket request, and the
`RebuildQueueChanged` payload) and `hive-jobq-wire`'s generic
`GraphNode` (`/api/jobq/graph`, `QueueNodes`). Every consumer has moved
to the generic one, so the typed pair is deleted rather than kept in
agreement with it.

What that removes, beyond the types: the `QueueDag` request and
`HostResponse::dags`; `Queue::snapshot`; `dag_view`, `visible_dags`,
`shown_on_wire`, `dag_finished_at` and `containers`; and the
`rebuild_queue` field on `/api/state`. `RebuildQueueChanged` keeps its
seq and loses its payload — nothing read it, and shipping the graph
both on an event and on an endpoint is the duplication this issue is
about. It stays an event rather than becoming a poll because
push-on-change is what every other live surface here does.

Two behaviours came out simpler for a structural reason. `await_dags`
needed two rules — settled means "gone from the snapshot" *or* "present
with every node terminal" — because the typed view evicted finished
groups; the generic view doesn't, so pending is just "some node isn't
terminal". And `state_of` in the tests no longer derives a roll-up at
all: a group root's own state is the scheduler's answer.

That second one found a bug. `cancelled_dag_still_runs_its_approval
tail` asserted the group reads `Cancelled` while the tail it exists to
protect was still pending — `rollup_state` flattened the surviving
child away and called the group settled. The root reads `Finishing`,
which is what the scheduler documents: own logic done, children still
running. The test now asserts that, with the reasoning inline so it
doesn't get "fixed" back.

Kept: `Source`, `State`, `PermPayload` and the `NodeId` alias in
`hive-host-sock::jobs` — shared vocabulary, still used by hivectl.
2026-08-03 21:25:07 +02:00
iris
4ec1c61d52 swarm.js: restore the queue-summary banner on GET /api/jobq/rollup
hyperhive#3033 (jobq rollup endpoint) merged, unblocking hyperhive#3036.

The banner (removed on PR#3031 rather than ship it on an interim
GET /api/jobq/graph client-side derivation) is back, now reading
GET /api/jobq/rollup — hive-jobq-wire::state_rollup's pre-tallied
Vec<StateCount>, not the full graph. running sums the Running and
Finishing entries' roots (Finishing = own work done, subtree still
going); queued reads the Pending entry's roots. roots specifically,
matching the banner's established "N whole operations" meaning, not
the endpoint's parallel nodes count (~7 nodes per rebuild, 1 root).

Re-adds the rebuild_queue_changed SSE subscription dropped alongside
the banner, wired as a payload-less refetch trigger — confirmed with
atlas on the DagView-deletion tracker that this is the intended final
shape (keep the event, drop the payload) rather than deleting it and
falling back to polling.

Verified the running/queued derivation against constructed
StateCount[] fixtures (running+queued mix, Finishing-counts-as-running,
settled states never contribute, multi-state sums) before touching
swarm.js — 6/6 checks passed. npm run build clean, tracker-tag +
comment-block pre-push lints clean. docs/web-ui/dashboard.md updated
to describe the restored banner + the two prior shapes it went
through.
2026-08-03 21:07:53 +02:00
iris
40cc115a0a swarm.js: drop the queue-summary banner rather than ship it on an interim jobq fetch
mara, on the already-approved PR: "dont replace one legacy thing with
another. then we will have to either wait with this pr or split it
into what can and cannot be done now."

Splitting: the transients-only per-agent badge fix is real, correct,
and fixes a live regression (the old DagView fields it read no longer
exist) — nothing about it depends on job-queue data at all, so it
ships as-is. The queue-summary banner is the part that doesn't belong
in this shape: it was reading GET /api/jobq/graph directly and
deriving counts client-side as an interim stand-in for the dedicated
rollup endpoint mara separately asked for — exactly the kind of
stopgap-on-a-stopgap her comment is calling out, since the endpoint
that should serve it doesn't exist on main yet.

Removes jobqNodesState, refreshJobqGraph(), the rebuild_queue_changed
SSE subscription, and the banner's render block from swarm.js/tabs.js
entirely — swarm.js now reads no job-queue state of any kind, fully
satisfying "swarm.js should not need to pull in the jobq to do its
job." The banner comes back once the rollup endpoint
(hyperhive#2985's follow-up) exists, reading that directly instead of
the full graph. Until then the per-agent transient pills still show
what's actually running on each card; only the hive-wide "N running /
M queued" summary line is temporarily gone.

CSS classes for the banner (.queue-summary/.queue-summary-link) kept
in dashboard.css rather than deleted-then-restored — commented as
currently unused, expected to come back unchanged.

docs/web-ui/dashboard.md updated to match (Container-row pending-
badge section, the removed Build-queue-summary-banner section, and
the BU1LDS-page note that used to describe SW4RM's now-removed
parallel fetch).
2026-08-03 20:03:49 +02:00
iris
7b05656e17 tabs.js: fix stale comment — rebuild_queue_changed feeds the queue-summary banner only 2026-08-03 20:03:49 +02:00
iris
17f61d1da6 swarm.js: drop per-agent pending-badge fallback, transients-only now
mara, on review: "swarm.js should not need to pull in the jobq to do
its job" followed by "remove the per agent pending stuff - only show
what is running."

Deletes queuedOpsByAgent() entirely — no more per-agent badge derived
from Pending-state job-queue nodes. A card's pending badges are now
driven exclusively by transientsState (i.e. actually-running work);
queued-but-not-started work shows nothing on the card until a node
starts. jobqNodesState + refreshJobqGraph() stay, now feeding only
the queue-summary banner (a separate, still-open question — mara
separately asked for a dedicated rollup endpoint for that, tracked
apart from this PR).

Collapses the now-always-coincident `pending`/`pending-running` row
classes into one (`pending-running`) — there's no more queued-only
row state to visually distinguish it from.

docs/web-ui/dashboard.md's Container-row section rewritten to match:
the two-store priority-fallback description is gone, replaced with
"transients only."
2026-08-03 20:03:49 +02:00
iris
45710ab739 swarm.js: migrate pending-row fallback + queue-summary banner off DagView
hyperhive#2822/PR#3026 moved swarm.js's per-agent in-flight status off
the rebuild queue. Two other reads of the same rebuild_queue field
survived that PR by design (a different feature, atlas flagged it on
#2985) and are the last DagView/NodeView consumers on the frontend:
queuedOpsByAgent()'s pending-row fallback and the SW4RM queue-summary
banner. Both now read GET /api/jobq/graph (hive-jobq-wire's generic
GraphNode shape) instead, matching the pattern builds.js already
established for <hive-jobq-graph>.

Along the way: DagView no longer carries state/kind fields (removed
in an earlier refactor that pushed roll-up derivation client-side),
so both migrated functions were silently reading undefined fields and
had become permanent no-ops — the pending-badge fallback never lit
and the queue-summary banner never rendered. This restores real
behavior rather than porting broken logic forward.

The queue-summary banner's node-count-vs-group-count question (flagged
on hyperhive#3028 as needing a decision) resolves cleanly: a GraphNode
group root (parent: null) is an ordinary node whose own state already
IS the group's roll-up per hive-jobq-wire's contract, so counting
roots by state is a direct filter, not a parent-chain walk or a
client-side rollup calculation.

Verified the derivation logic against constructed GraphNode fixtures
(multi-step chains, settled history that must not count, Finishing
roots, multi-agent single-DAG groups) before wiring it in — 13/13
checks passed.

docs/web-ui/dashboard.md's Container-row + BU1LDS sections updated to
match.
2026-08-03 20:03:49 +02:00
iris
e269af7882 frontend: drop the queued-badge label lookup, show the raw kind
mara, PR #3026 review: "drop queuedLabelFor - just show what the
backend sends".

`queuedLabelFor` translated the rebuild-queue entry's `kind` field
through a curated English-phrase table ("meta_update" -> "meta-update
queued", "graceful_stop" -> "stop queued", etc.) for the one fallback
case where no transient exists for an agent. Removed the lookup
entirely -- `queuedOpsByAgent()` now returns the raw `kind` string
directly, and the render loop uses it as-is, the same opaque-string
treatment a transient's own `kind` already got (never run through a
lookup, per docs/web-ui/dashboard.md's existing "treat it as an opaque
display string" note). The queued-vs-running visual distinction still
comes entirely from the row's CSS classes (no ring/tint for queued,
amber ring + tint for running) -- the text was never carrying that
signal on its own, so nothing is lost by not reformatting it.

Updated docs/web-ui/dashboard.md's Container-row section to match.

npm run build clean, standalone verification re-run (17/17 checks,
one updated for the new raw-string expectation).
2026-08-03 18:47:51 +02:00
iris
d1f82e725e frontend: swarm.js off rebuild-queue-derived in-flight status onto transients
Fixes #2822.

`swarm.js` had two independent per-agent "is this in flight" sources:
`transientsState` (operator/worker-initiated ops the backend chose to
flag) and `inFlightOpsByAgent()`, a separate derivation straight from
`rebuildQueueState` covering everything else. Since #3010/#3016,
`running_transients()` is a status-only test — any `Running` job-queue
node naming a non-empty agent lights a transient pill, not just a
curated subset — so the second source's Running-state handling is now
provably redundant: a Running node with an agent always already has a
transient by the time `queuedOpsByAgent()` (renamed from
`inFlightOpsByAgent`) would be consulted.

## What changed

- `transientsState`: `Map<name, {kind, since_unix}>` (one pill per
  agent) -> `Map<name, Map<kind, since_unix>>` (several pills per
  agent). `applyTransientSet`/`applyTransientCleared` now add/remove
  by `(name, kind)` rather than overwrite/delete by name alone, using
  `TransientCleared`'s `transient_kind` field (landed in #3016) to
  know which pill cleared. `syncTransientsFromSnapshot` groups the
  now-flat `TransientView` list by name instead of assuming one row
  per agent.
- `inFlightOpsByAgent()` -> `queuedOpsByAgent()`: trimmed to the
  `Pending` (queued, not yet started) case only. The `Running` branch
  and its "running beats queued" priority logic are gone entirely —
  dead weight now that transients cover every running case
  unconditionally.
- Render loop: an agent's transients win outright whenever any exist
  (rendered as **one badge per pill**, not collapsed into one label —
  mara: "show all running nodes that name the agent"); the queued
  fallback only applies when a agent has zero transients. `opRunning`
  simplifies to "does this agent have at least one transient".
- `docs/web-ui/dashboard.md`'s Container-row section rewritten to
  match — it described a "transient, then in-flight-queue, in
  priority order" model that's no longer accurate now that the second
  source only ever fires for the one case the first can't represent.

## Verification

`npm run build` clean for both packages (dashboard + agent). Standalone
re-derivation of the transient-map + queued-fallback logic
(`/tmp/verify-swarm-transients.mjs`, not part of this diff) run against
constructed event sequences: single-pill lifecycle, two simultaneous
pills on one agent with independent clear-by-kind, clearing an unknown
kind is a safe no-op, a flat snapshot with duplicate agent names groups
correctly, the queued fallback only fires when no transient exists and
steps aside the instant one arrives, and a Running-state rebuild-queue
entry produces no queued badge (confirming the Pending-only trim is
correct, not just assumed). All 17 checks passed.

Verified directly against the merged backend rather than trusting
summaries: `job_queue/mod.rs::running_transients()` filters
`State::Running` only (not Pending — an earlier note of mine claiming
otherwise was imprecise paraphrasing), and `NodeView.agent` /
`running_transients()`'s agent both resolve through the same
`payload.agent()`, so a Running node's presence in `rebuild_queue`
and its presence as a transient are guaranteed consistent, not just
usually so.

#2985 (DagView/NodeView deletion) unblocks once this merges — atlas is
waiting on a ping.
2026-08-03 18:47:51 +02:00
iris
2ff4cddf9d frontend: hive-warn — replace severity+pulse with info/warning/error levels
mara, PR review: "i dont like that they still have different styling.
if anything, there should be distinction between info,warning,error
(semantics). all warnings should be styled identically."

Replaces the severity ('amber'|'red') + standalone pulse boolean with
a single level ('info'|'warning'|'error') attribute -- a fixed
three-tier ladder instead of an open combination. Colours reuse
theme.css's own already-documented semantics rather than inventing new
ones: --cyan is already "info accents", --amber already "warnings",
--red already "errors, fail state". Pulse is now baked into `error`
specifically rather than a separate opt-in knob, since the one call
site that wanted attention-grabbing (an active incident) is also the
one that's semantically error -- tying the two together removes a
combination that shouldn't exist independently of the tier.

Reclassified the three call sites explicitly (no implicit default
relied on): credentials.html's GitHub PAT advisory and core.js's K3PT
ST4T3 caveat are both `level="warning"` (same as before, and now
identical to each other by construction, not by coincidence);
swarm.js's port-collision banner is `level="error"` (an active
incident needing operator action now, not a standing caveat).

npm run build clean both packages, grepped for leftover
severity/pulse references (only prose mentions describing what this
replaces).
2026-08-03 18:34:31 +02:00
iris
dfd92a7da9 frontend: shared <hive-warn> component for inline warning banners
Consolidates three independently-written instances of the same thing:
.cred-warning (credentials.html, static markup), .tombstone-warn
(core.js, JS-built), .port-conflict (swarm.js, JS-built) -- the first
two differed only by an undeliberate 10% vs 8% tint, the strongest
argument this was drift rather than three genuinely different needs.

New autonomous custom element, frontend/packages/shared/src/hive-warn/,
matching the established one-component-one-dir shadow-DOM pattern
(hive-btn, hive-toast, hive-dialog). Purely presentational -- no
lifecycle beyond attaching its shadow root once, no
attributeChangedCallback needed since severity/pulse are read directly
by :host([...]) CSS selectors rather than mirrored onto an inner
element. Content passes through via a single default <slot>, so every
call site keeps writing its existing <strong>/<code> markup unchanged.

API: <hive-warn> (amber, default) / <hive-warn severity="red"> for an
active incident vs a standing caveat, plus an opt-in pulse boolean
modifier (only the port-collision banner wants it -- a banner that's
always present and always pulsing just trains you to stop seeing it).
Tint is a single canonical 8% for both severities now, replacing the
10%/8% split.

Registered once in dashboard's common.js (same side-effect-import
pattern as <hive-side-panel>) so every dashboard page picks it up
without a per-file import, since all three call sites (core.js,
swarm.js, credentials.js) already transitively import it.

Verified: npm run build clean for both dashboard and agent packages,
grepped source for leftover cred-warning/tombstone-warn/port-conflict
references (none), confirmed hive-warn/HiveWarn/hive-warn-pulse present
in every affected dist bundle, confirmed the still-live
questions-pulse keyframe (.questions) untouched.
2026-08-03 18:34:31 +02:00
atlas
6a8a729f58 fix(#3020): K3PT ST4T3 says what it shows, and stops reading the job graph
`build_tombstone_views` folded `transient_snapshot`'s keys into its `live`
set, so an agent with in-flight transient work was treated as not-a-tombstone.
Since #3010 that set is derived from the running job graph, which made a
page about on-disk state a function of the scheduler.

Dropping the filter exposes what was always true underneath: nothing records
a destroy. Every definition-side artifact — state subvolume, proposed +
applied repos, `deployed/0`, meta registration, topology entry — is written by
`Provision` before the container exists and survives `lifecycle::destroy`. So
a mid-spawn agent is byte-identical on disk to a tombstone.

Per mara on #3020: remove the filter, warn on the page, keep the issue open
for the swarm-controller / snapshot-storage rework where the problem shape
changes anyway.

- dashboard/tombstones.rs: drop the param + the chain; document the real
  semantics
- core.js: amber caveat banner above the rows; row badge `destroyed` ->
  `offline`, which is what an absent container actually proves
- dashboard.css: `.tombstone-warn`, modelled on `.port-conflict` but amber and
  without the pulse — a permanent banner that pulses trains you to ignore it
- docs/web-ui/dashboard.md: the pane was described as "destroyed-but-state-kept
  agents", now the exact wrong claim
2026-08-03 18:07:04 +02:00
iris
aa149a7a62 builds: mount <hive-jobq-graph> directly, drop the hand-rolled queue renderer
Per mara's explicit steer on hyperhive#2812 ("also replace the build
queue tab with this component" + "graph fetching should live in the
component, not build.js" + "dont replicate the grouping by dag"):
R3BU1LD QU3U3 is now a mounted <hive-jobq-graph endpoint="/api/jobq/graph">
element. builds.js no longer renders the queue itself, does its own
fetch, or hand-rolls a per-root tree/roll-up/cancel-button — all of
buildNodeTree/topoSort/entryFingerprint/renderQueueEntry/
firstFailedNode/rebuildQueueRowCache/QUEUE_STATE_GLYPH/rollupState is
gone.

builds.js's remaining job is listening for the component's
hive-jobq-graph-update event (added to the component in the prior
commit) to keep a flat jobqNodes array in sync, and using that for the
two things the generic view doesn't render: the count-pill and the
live-log panel. On the rebuild_queue_changed SSE tick, calls the
mounted element's .refresh() instead of doing its own fetch — that
event still carries its own queue payload on the wire (tabs.js/SW4RM
still reads it for the badges, untouched), this page just ignores it
now.

Also removed, now genuinely dead: the two elapsed/finished-time
tickers (nothing produces the .rqe-when spans they targeted anymore),
stateSlug and isoToSecs (no callers left), fmtElapsed import (no
callers left).

New @hive/shared/jobq-graph.js export entry in packages/shared's
package.json, alongside the existing hive-tab-strip.js/hive-menu.js/
etc. pattern.

docs/web-ui/dashboard.md's R3BU1LD QU3U3 section rewritten to match:
mounted-component shape, no source/reason/cancel-button/deep-link on
rows (generic component has none), settled entries show their full
step tree (Done nodes aren't filtered off this wire, unlike the old
DagView projection).

Verified against real production data again (this hive's own live
/api/jobq/graph, now settled — no in-flight build at test time) plus
a synthetic running-build case to exercise findLiveBuild's happy path:
correct live-node detection (build_log_id gate), correct in-flight
root count. Confirmed the built dist bundle actually registers
customElements.define("hive-jobq-graph", ...) — the new shared
package export resolves correctly through esbuild.

Branch reused per mara's explicit "dont rework #3000 - continue
working on _this_ pr [#2996], it already has the component that
replaces 90% of build.js" — this ships as part of PR #2996, not a
separate PR.
2026-08-03 01:53:39 +02:00
iris
98957b48e9 hive-jobq-graph: dispatch hive-jobq-graph-update on every render
Fetching lives in the component (mara's steer on hyperhive#2812) — a
host that needs the raw node list for something the generic tree
doesn't show (a count badge, a live-log panel keyed on a specific
node) now listens for this bubbling/composed CustomEvent instead of
doing its own parallel fetch. Fires from both the self-fetch path
(refresh()) and a host-pushed render(nodes) call, so a listener sees
every update regardless of source.

Verified via jsdom: both paths dispatch with the correct nodes in
detail.
2026-08-03 01:53:39 +02:00
iris
123eec80c4 fix tracker-tag lint: reword #2893 reference to prose 2026-08-03 01:53:39 +02:00
iris
57c9ff6d6c shared: add <hive-jobq-graph>, a generic renderer for /api/jobq/graph
Shadow-DOM custom element (attachShadowCss, own <style>, matches the
<hive-dialog>/<hive-toast> pattern) that renders any hive_jobq graph
from the wire shape hive-jobq-wire serves: a tree from parent/child
structure, a state glyph per node, payload.label verbatim, and
payload.data as a generic key/value list. Never branches on what a
label or data key means, per the wire type's own opaque-payload
contract.

Fetch endpoint is a configurable attribute
(<hive-jobq-graph endpoint="/api/jobq/graph">) rather than
hardcoded, and a public render(nodes) method lets a host push
pre-fetched data (e.g. from its own SSE stream) instead. No transport
of its own beyond the initial self-fetch — refresh() is public so the
host decides its own refresh cadence.

Verified against real production data (61-node live rebuild-queue
graph, fetched from this hive's own /api/jobq/graph) via a jsdom
render: correct tree shape, correct state glyphs, correct data-list
presence count, both the self-fetching and render()-pushed paths,
and the empty-graph path.

Not wired into any page yet — the builds.js migration (hyperhive#2812)
follows once the open payload-gap question there is settled.
2026-08-03 01:53:39 +02:00
iris
3512e4b019 dashboard: hide forge links instead of guessing <hostname>:3000
Adds services.hyperhive.forge.publicUrl (defaults to the gateway vhost
URL when behindGateway=true, null otherwise). HIVE_FORGE_PUBLIC_URL is
now sourced from it instead of hardcoding https://${forge.domain}
whenever behindGateway is on.

The 4 frontend call sites that built a forge link from
state.forge_public_url now hide the link when that's absent, rather
than guessing http://<browser-hostname>:3000 — a guess that's only
correct by accident once the operator isn't on plain localhost. Fixes
the dashboard H0M3 tile, per-agent-row forge links + agent menu, the
approval-queue PR link, and the per-agent page's own meta-nav forge
link (found during this pass, same defect, not in the original
3-site inventory).

Docs + doc-comments updated to match.
2026-08-03 01:21:11 +02:00
iris
f60e8a8470 dashboard: pass kinds= on the 4 unfiltered /api/dashboard/stream subscribers
Part 1 of the dashboard-event-stream-split epic: the server-side
kinds= allow-list already exists and flow.js already uses it
(hive-c0re/src/dashboard/state_snapshot.rs). tabs.js, builds.js,
core.js, and logs.js were the remaining 4 subscribers still taking
every wire kind unfiltered — pure subscription discipline, no new
endpoint needed, per the investigation on the tracking issue.

Each kinds= list is read directly off that page's own existing
MUTATION_HANDLERS/SSE_HANDLERS dispatch table (tabs.js also needs
sent, checked separately for the operator inbox) — a kind not in a
page's table was already a silent no-op today, so this only removes
wire/parse/dispatch-lookup cost for kinds a page never acted on, zero
behavior change.

Note for reviewers: the SharedWorker (stream-worker.js) multiplexes
by exact URL string, so pages that used to share one unfiltered
upstream connection when open simultaneously (e.g. dashboard.html +
builds.html in two tabs) will now each hold their own filtered
connection instead, since their kinds= differ. Each connection is
still a single cheap SSE stream carrying only what that page acts on
— net win over the shared-but-bloated connection this replaces.
2026-08-02 22:21:24 +02:00
damocles
af3976a76a hivectl/dashboard: add --paused / ?paused=1 to agent start 2026-08-02 19:52:11 +02:00
iris
3f2fdeac70 docs: document the OpenAPI spec + Swagger UI, add H0M3 API tile
Closes #2965.

hive-c0re auto-generates an OpenAPI 3 spec via utoipa
(hive-c0re/src/dashboard/mod.rs's ApiDoc), served raw at
/api/openapi.json and browsable as a Swagger UI at /api/docs, but
docs/ never mentioned either — genuinely zero hits grepping the whole
docs/ tree. Documented both in docs/web-ui/dashboard.md's Dashboard
endpoints + H0M3 page sections.

Also added the H0M3 hub's API tile mara suggested ("maybe also add
home page app that opens swagger ui") -- a plain static link to
/api/docs, no gating needed since the endpoint always exists
(unlike Forge/Matrix, which are conditionally enabled).
2026-08-02 19:13:41 +02:00
iris
da3fc9bf95 move terminal-verbosity toggle from agent overflow menu to /settings.html
Per mara: 'i wanted you to put this in .../settings.html' — the toggle
belongs with the other operator-local browser preferences, not buried
in each agent's own overflow menu.

Extracted the get/set + localStorage key into @hive/shared/prefs.js so
settings.html (writer) and every per-agent app.js (reader, via
HiveTerminal.create's expandDetails option) agree on the exact same key
without two independently-typed copies that could drift. Removed the
now-unused overflow-menu toggle + its agent.css rules from the agent
page. Docs moved from docs/web-ui/agent.md's overflow-button section to
docs/web-ui/dashboard.md's S3TT1NGS section, next to the existing
browser-notifications preference.
2026-08-02 18:34:40 +02:00
iris
3b299375e3 fix: move settings section after effort picker, matching docs
argus caught the settings block appending before the effort picker's
conditional despite the PR description and docs both saying it lands
after — DOM order is visual order here (no CSS order: override), so
actual layout was model -> settings -> effort. Moved the block after
the effort picker's if-block; layout now matches what both already
claimed.
2026-08-02 18:29:28 +02:00
iris
198db326b3 agent web UI: add terminal verbosity setting (expand tool output by default)
Adds a browser-local (localStorage only, no backend field) toggle in
the per-agent overflow menu's new settings section: whether otherwise-
collapsed <details> rows in the live terminal (long tool-results,
Write/Edit diffs, ...) default open. Message-bearing rows that already
default open (send/ask/answer/recv) are unaffected either way.

The shared terminal factory (frontend/packages/shared/src/terminal/terminal.js)
gains an optional expandDetails option (boolean or zero-arg function),
read live on every details()/detailsDiff() call rather than captured
once, so flipping the toggle mid-session applies to the next rendered
row without a reload. Unused by the dashboard's own terminal pane, so
its default-closed behaviour is unchanged.

Closes #2961.
2026-08-02 18:25:45 +02:00
iris
435ef193e1 frontend: add <hive-tab-strip>, convert logs/credentials/core/builds tabbars
Every sub-page tabbar (logs.html, credentials.html, core.html,
builds.html) hand-wrote the same <nav class="hive-tabbar"><a
class="hive-tab">...</a></nav> boilerplate and then called
createTabStrip() on it after the fact. Add <hive-tab-strip>, a
markup-owning custom element (same reuse-boundary pattern as
<hive-menu>/<hive-side-panel>) that renders that markup from a
declarative tabs list, then wires the existing createTabStrip()
behaviour over what it just rendered — no behaviour duplication.

Convert all four sites to use it: each page's JS now calls
`.configure({ tabs, defaultId, onShow })` on the tabbar element instead
of `createTabStrip(el, opts)`, and configure() returns the identical
{ show, active } shape so nothing downstream changes. builds.js's
rebuild-queue count pill (builds-tab-count-rebuild) is expressed as a
tab's `badgeId` and renders nested in the same spot.

The dashboard's own tabbar and the two no-pane stats time-range
pickers are a different markup/behaviour shape and are intentionally
left alone.
2026-08-02 13:40:24 +02:00
iris
94fb42f2b6 docs/agent.md: fix stale 3-flyout description; drop dead buildLooseEndsList
docs/web-ui/agent.md still described a "loose-ends" and "tasks" flyout
that were superseded by the todos flyout when loose-ends-v2 landed —
GET /api/loose-ends and GET /api/bash-tasks are both gone server-side.
Replaced with an accurate description of the todos flyout (including
the mark-done bulk action from #2919), and noted that the ask->operator
inline-answer binding this doc also describes is currently
non-functional (its data source was the same removed endpoint) —
tracked separately as #2922, not fixed here.

buildLooseEndsList in app.js rendered the old loose-ends flyout and had
zero call sites left; removed it. buildAnswerForm stays — reconcileAskBinds
still calls it, even though that path is currently dead per #2922.

Fixes #2920
2026-08-02 02:40:01 +02:00
iris
41c1b1a3fb agent web UI: bulk mark-done for the todos flyout
The per-agent web UI todos flyout (loose-ends v2) had no mark-done
affordance at all — dismissing a todo was only possible via the
cancel_loose_end MCP tool, one id at a time. Add a checkbox per row,
a select-all/select-none/mark-done bulk row, and a new
POST /api/todos/mark-done handler that loops the existing single-id
MarkTodoDone request over the in-agent socket (no new wire request
type needed — the todos list is small, so N same-host round-trips is
cheap).

Fixes #2917
2026-08-01 20:49:07 +02:00
iris
4c37ce9150 dashboard: consolidate NodeView.has_log into build_log_id
Per mara's review on #2896: has_log: bool was fully redundant once
build_log_id: Option<i64> existed alongside it (has_log was always
just build_log_id.is_some()). Dropped has_log, threading the single
Option<i64> field through job_queue::mod.rs, the hivectl NodeView
test-helper literal, and the one remaining frontend consumer
(findLiveBuild's live-log-panel gate, which now checks
build_log_id != null instead of the separate bool).

Also fixed a now-stale doc comment on GET /api/build-log/{node_id}
that claimed the dashboard used on-demand node-id fetches "instead
of an inline build_log_id on the wire" -- no longer true after this
PR put one there for the BUILD L0GS deep-link.

cargo build/clippy/test clean across the three touched crates; nix
fmt clean; frontend build verified (0 has_log references, 3
build_log_id references in the built builds.js bundle).
2026-08-01 11:38:23 +02:00
iris
662e303e72 dashboard: queue log link opens BUILD L0GS tab instead of downloading
Fixes hyperhive#2895. The rebuild-queue tree's per-node log icon (the
printer-glyph "open" affordance next to each node in the R3BU1LD
QU3U3 tab) linked directly to the raw-text download endpoint
(/api/build-log/<node_id>/raw, which sets Content-Disposition:
attachment server-side) -- surprising, since nothing about that icon
signals "this leaves the app", unlike the other two explicit
"download raw"/"raw" links elsewhere on the page.

Point it at the existing ?id=N#buildlogs deep-link into the BUILD
L0GS tab instead (builds.js's fetchBuild already auto-expands +
scrolls to the matching row there). That deep-link's id is the
build-log history row id -- a different id space than the queue
tree's NodeId, and wasn't exposed to the frontend before (only a
derived has_log bool was). Added NodeView.build_log_id: Option<i64>
to the wire type alongside the existing has_log (kept, since
findLiveBuild's separate live-log-panel gate still needs a plain
bool), threaded through job_queue::mod.rs, updated hivectl's NodeView
test-helper literal.

The raw download is still one click away once on that row's BUILD
L0GS detail (the two already-explicit raw-download links are
untouched). cargo build/clippy/test clean across the three touched
crates (hive-c0re, hive-host-sock, hivectl); nix fmt clean; frontend
build verified (grep for build_log_id in the built builds.js bundle).
2026-08-01 11:38:23 +02:00
iris
38aa5f77f4 frontend: guard hive-menu/hive-agent-menu connectedCallback against reconnect
Fixes hyperhive#2893: 'Element.attachShadow: Unable to re-attach to
existing ShadowDOM', crashing swarm.js's live-update render path.

A custom element's connectedCallback fires again on a same-document
*move* (insertBefore/append repositioning an already-connected node
runs the removal + insertion steps for its whole subtree), not just
on a fresh mount. swarm.js's row-fingerprint cache reuses + reorders
existing <li> subtrees on live updates -- reordering an unchanged,
cached row moves its already-initialised <hive-agent-menu>/<hive-menu>
without ever really detaching it from the document, so connectedCallback
re-runs full setup on an instance that's already set up. attachShadow()
throws unconditionally if the host already has a shadow root, and
HiveAgentMenu's unconditional child-menu creation would have appended a
second <hive-menu> on top of the first, doubling the dropdown, once the
shadow-attach crash itself was out of the way.

Both connectedCallbacks now bail early if already initialised
(shadowRoot present / _menu already built). Reproduced the crash and
duplicate-menu bug with an unguarded control copy of both files driven
via headless Chromium (simulating the exact row-reorder move), then
confirmed the guarded version throws nothing, keeps the same shadowRoot
object identity across the move, and doesn't duplicate the dropdown.
2026-08-01 01:57:06 +02:00
iris
d50bea588a frontend: move shared/terminal.js+css into one-dir terminal/
Same pure structural move as the previous commit, applied to the one
other remaining genuine component in shared/src (a self-contained
widget with its own behaviour + CSS, same class as hive-btn/hive-
dialog/hive-toast/hive-menu/side-panel/tabs) -- not the CSS-foundation
files (colors/theme/base/chrome.css) or the utility modules (forms.js,
dom.js, modal.js, shadow-css.js), which aren't components and don't
fit the one-dir-per-component convention.

External callers resolve terminal.js/terminal.css only through
@hive/shared's exports map, so again the two exports targets are the
only external-facing change. index.js's own internal re-export uses a
relative path within the package, so that needed updating too. Zero
call-site changes outside @hive/shared. Verified the built dashboard
(flow.js/common.css) and agent (app.js/agent.css) bundles still
resolve both files.
2026-08-01 01:56:41 +02:00
iris
fbc09f1b3d frontend: move shared/tabs.js+css into one-dir tabs/
Pure structural move, no API or behaviour change: tabs.js/tabs.css
move into shared/src/tabs/, matching the one-dir-per-component
layout the other shared components already use (hive-btn,
hive-dialog, hive-toast, hive-menu, side-panel).

Both files are consumed exclusively through @hive/shared's
package.json exports map (./tabs.js, ./tabs.css), never by a raw
relative path, so updating the two export targets is the only
change needed -- none of the 9 call sites (dashboard tabbar, logs,
core, builds, credentials, stats x2, agent stats) touch anything.
Verified the built dashboard/agent bundles still resolve both
files correctly.
2026-08-01 01:56:41 +02:00
iris
399a837e17 frontend: drop the Panel forwarding object and the side-panel-body compat class
Per mara's review: '2 and maybe 1, but 3 also sounds reasonable on first
glance' (against 3 options I posted). Doing 2 and 1, leaving open()/
openNamed() as-is (option 3, tentative only).

Both dashboard/common.js and agent/app.js now export/use the
<hive-side-panel> element instance directly (sidePanel) instead of a
thin Panel = { open, openNamed, refresh, close } object that existed
purely to keep the old call-site shape unchanged. All 6 real call sites
updated to call the element's own methods directly.

The .side-panel-body class each wrapper stamped onto its own instance,
purely so common.css/agent.css's pre-existing content-styling selectors
kept matching, is gone too -- those selectors now use the element's own
tag name as the root (hive-side-panel .md, hive-side-panel .agent-inbox),
which already uniquely identifies the light-DOM instance without a
compatibility class. Verified via headless Chromium/CDP that the
tag-name selectors resolve correctly with no class needed.

Drive-by: removed an unrelated dead Panel import in call.js.
2026-08-01 00:58:53 +02:00
iris
996899fcad frontend: create the side-panel instance eagerly, not lazily behind a per-call guard
Per mara's review on the side-panel PR: create the shared <hive-side-panel>
instance once at module-evaluation time instead of lazily on first call
via an ensurePanel() guard every wrapper method had to remember to call.
ES modules execute after the document is parsed (same timing as a defer
script), so document.body is already available when this code runs --
lazy init bought nothing here and left a footgun for any future method
added to either wrapper.
2026-08-01 00:58:53 +02:00