Closes#3423.
Modern browsers validate an <input pattern> attribute's regex in
Unicode-set ('v') mode, which is stricter about hyphen placement than
classic mode: `[a-z0-9-]` throws "Invalid character class" under 'v'
mode even though a trailing hyphen is unambiguous (and valid) in classic
regex. Reproduced directly: `new RegExp('[a-z0-9-]', 'v')` throws,
`new RegExp('[a-z0-9\\-]', 'v')` doesn't. Escaping the hyphen fixes it
without changing what the pattern matches.
atlas, on the issue: swarm-controller's problem+json conversion merged
(58588a68, closing hyperhive#3412 which this comment referenced as an
in-flight fix). Every first-party API is RFC 9457 now, so the non-JSON
fallback path is not backend-transition compatibility anymore — it's for
bodies this repo didn't author (a caller pointed elsewhere, or an
nginx-synthesised 502/504 that never reaches our handlers). Comment
updated to say that instead of pointing at a transition that's already
finished.
mara, on review: "i want each component to import its own css file
itself[;] if you need a bunch of extra css externally, its not a proper
component ... you may need to migrate other components that you would
want to use first".
WarnBanner is the Preact-component successor to the shadow-DOM
<hive-warn> custom element (same three-tier info/warning/error visual
language, colours copied faithfully from hive-warn.css). ApiErrorPanel
now composes it instead of owning a copy of the border/colour/pulse
rules itself — its own CSS is back down to just the layout that's
actually specific to it (heading, copy button, detail text).
Re-verified with a fresh mock-server screenshot: same rendered output as
before, now via composition instead of a duplicated banner shape.
Closes#3410.
`ApiErrorPanel` renders a ProblemDetails (RFC 9457) error nicely, with a
copy button so the full text can be pasted straight into a bug report.
No truncation of `detail` — on the #3363 incident that motivated this
issue, that string was the entire diagnosis.
`readApiError`/`problemMessage`/`ProblemDetails` are promoted out of
credentials.js's original `readErrorBody` into `@hive/shared/api-error.js`
(comment rewritten: the RFC 9457 rework has already landed everywhere
except swarm-controller's status route, #3412 in flight, so the raw-text
fallback is a compat shim for that one gap, not a general transition).
credentials.js's 4 call sites switch to the shared reader (kept as
one-line messages there, its result slots are single-line aria-live
regions, not a panel context).
Wired ApiErrorPanel into OverviewPage.tsx (the issue's own worked
example) and CreateAgentPage.tsx (second real call site).
Not built on <hive-warn> despite matching its visual language — that
custom element's CSS-as-text import only works under a build with
loader: 'text' for .css (dashboard's), and silently renders unstyled
under swarm-ui's default css loader (filed separately as #3415).
ApiErrorPanel is a self-contained light-DOM component instead, per
mara's own suggestion to keep it independent of the old UI's shapes.
Relative hrefs (static/main.js) only resolve correctly for a route
one path segment deep - the browser resolves them against the
current URL's directory, so a route two-plus segments deep would
404 the JS/CSS silently once this is wired to a real server. Made
absolute (/static/main.js), matching what build.mjs's own comment
already documents these paths as being served at.
mara: the route should reflect creating an agent, and stay separate
from a future agent list page. Renamed AgentsPage -> CreateAgentPage
(file, component, css classes) and moved the route from /agents to
/create-agent - flat, not /agents/new, since index.html's relative
asset links only resolve correctly one path segment deep (filed
separately as a real bug, not fixed here). Leaves the bare /agents
path free for a future roster page.
Adds /agents: a name field that POSTs to swarm-controller's
POST /api/agents (from the CreateIdentity work), shows the queued
job node id, and links to /jobs to watch it settle. Scope matches
the issue exactly - no forge/deploy options, those aren't wired
server-side yet.
mara, follow-up on PR#3365: "follow up with the same for jobq". Drops
mountJobqGraph the same way mountJobqRollup was dropped - builds.js
now calls render(h(JobqGraph, {...}), root) directly, split into
mountRebuildQueue() (the one-time replaceChildren() + first render,
called once at page init) and renderRebuildQueue() (the render-only
path the rebuild_queue_changed handler and the cancel flow reuse,
bumping a module-level jobqGraphToken instead of holding a mount
handle). The split matters here specifically: repeating
replaceChildren() on every refresh would wipe Preact's own tracked
children out from under its diffing instead of letting it update them
minimally - JobqRollup's simpler version didn't need this since it
only ever renders into its own dedicated section once per app
lifetime's worth of state, but the rebuild queue refreshes on every
`rebuild_queue_changed` tick.
Re-verified: npm run build (whole workspace) + swarm-ui typecheck
clean (JobqGraph is also used via JSX on swarm-ui's /jobs page,
untouched by this), comment-block + issue-ref lints clean, headless-
chromium screenshot of builds.html's R3BU1LD QU3U3 tab against a
mocked /api/jobq/graph payload - tree, filter checkboxes, and cancel
buttons all render identically to before.
mara, on review: expected the plain Preact pattern (render(h(Widget,
props), container), call again to update) rather than a custom
mountX() returning {refresh(), update()}. Preact's own render is
already the re-render/diff entry point, so the wrapper was indirection
this component didn't need - swarm.js (plain .js, no JSX pragma
required for h()/render() either) now calls render(h(JobqRollup,
{...refreshToken}), root) directly, bumping a module-level token to
force a refetch instead of holding a mount handle.
JobqGraph/mountJobqGraph (a separate, already-merged component) is
untouched - out of scope for this PR, flagged as a possible follow-up
if she wants the same simplification there.
Re-verified: npm run build (whole workspace) + swarm-ui typecheck
clean, comment-block + issue-ref lints clean, re-screenshotted the
dashboard SW4RM tab against the same mocked payload - identical
render, spinner now visibly mid-rotation in the frame (confirms the
animation is live, not just present in markup).
The extraction dropped the old banner's `spinner` class on the glyph
span (`el('span', { class: 'glyph spinner' }, ...)`) - JobqRollup.tsx
rendered a static `.jqr-glyph` with no animation rule anywhere.
`.spinner` (shared/base.css) is already imported by both consumers, so
just reuse it on the glyph rather than duplicating the keyframes.
argus caught this on review - a single-frame screenshot can't tell a
frozen spinner from a missing one, which is exactly why it slipped
past the PR's visual verification.
New @hive/shared/jobq-rollup.js (JobqRollup.tsx + jobq-rollup.css),
mirroring JobqGraph's shape exactly: JSX use plus an imperative
mountJobqRollup(container, props) for a plain-.js call site. Fetches
Vec<hive_jobq_wire::StateCount> off `endpoint`, sums Running+Finishing
roots as "running" and Pending roots as "queued", renders nothing when
both are zero. Optional `queueHref` adds a "view queue -> " link.
Swapped dashboard's hand-rolled queue-summary banner (swarm.js) over to
this component instead of keeping two parallel implementations - same
"one shared component" pattern JobqGraph already set for the rebuild
queue tree view. Mounted once into a new #jobq-rollup-section, kept as
a sibling of (not inside) #containers-section since that section gets
replaceChildren()-wiped on every container-state render, which would
tear down and remount a Preact tree on every tick. Refreshed via the
mount handle's .refresh() on rebuild_queue_changed, same as builds.js's
JobqGraph handle.
Also mounted in swarm-ui's /jobs page, above JobqGraph, with no
queueHref (a link back to the page you're already on is noise) - the
literal ask on hyperhive#3364.
Verified: npm run build (whole workspace) and swarm-ui typecheck both
clean, comment-block + issue-ref lints run manually, headless-chromium
screenshots of both the dashboard SW4RM tab and swarm-ui's /jobs page
against mocked /api/jobq/rollup payloads - banner renders identically
in both, with and without the queue link as expected.
Answers mara's question on PR#3342 (lightweight dep for fmtAgo?): no
dep needed, Intl.RelativeTimeFormat is built into the runtime and its
narrow style produces the same '5m ago' shape, verified with a real
call rather than assumed from the spec.
Per mara's review on PR#3342 — the hive-status view shouldn't live
inline in App.tsx. Moves it to pages/OverviewPage.tsx, mirroring
JobsPage's shape: App.tsx stays routing-only, each page owns its own
fetch + render.
App.tsx now fetches GET /api/hives/status (the swarm-controller
aggregate: one row per roster hive, freshness derived at read time
from the status bucket) instead of GET /api/hives + a static
'configured' chip. Renders fresh/stale/never_reported/unknown as
StatusChip tones with a relative age, per the placeholder comment that
was already waiting on this endpoint to exist.
Adds a small local fmtAgo helper (src/util.ts) mirroring the
dashboard package's near-identical formatter — not worth sharing
across a vanilla-JS and a Preact/TS call site.
Both introduced by this PR's own JSX->TSX conversion, caught by argus:
the top-of-file comment grew to 42 lines converting it, and JobsPage.css
still pointed at 'JobqGraph.jsx's own comment' after the rename.
New /jobs route in swarm-ui, reusing the shared JobqGraph component
against swarm-controller's own GET /api/jobq/graph (same wire shape
hive-c0re's dashboard already consumes, different endpoint, no fork).
Converted JobqGraph.jsx to JobqGraph.tsx with real prop/wire types
(mirrors hive_jobq_wire's GraphNode/GraphDep/State by hand) instead of
a hand-maintained ambient .d.ts at the swarm-ui consumer side — the
.d.ts would duplicate the prop list and drift from the source the
moment the component's signature changes without the declaration being
touched. Both dashboard (untyped consumer, esbuild strips types) and
swarm-ui (tsc --noEmit) build/typecheck clean off the one file.
Same pattern frontend.nix already uses for the dashboard: the SVG
lives outside the npm tree at branding/hyperhive.svg, copied into
$out during the nix install phase (not build.mjs, so plain npm build
consumers still work, same split frontend.nix uses). Added the
matching <link rel="icon"> to index.html.
The hash was hardcoded independently in nix/packages/frontend.nix,
nix/packages/swarm-ui.nix, and nix/checks.nix's inline
swarm-ui-typecheck derivation, all three building from the one
frontend/package-lock.json. Nothing enforced the three copies staying
in sync, and on a recent PR only some of them got updated when the
lockfile changed.
Moved the hash into a new file, frontend/npm-deps-hash (plain text, no
trailing newline, co-located with package-lock.json so it reads as
the lockfile's other half), and all three derivations now
builtins.readFile it instead of hardcoding their own copy. A lockfile
change now only needs prefetch-npm-deps + one file overwrite; the
other two derivations pick it up automatically.
Verified: nix eval against all three derivations' npmDepsHash
attribute (eval-only, not a build) confirms all three resolve to the
same value read from the one file.
Mara on PR#3315: "shouldnt the pattern be that the old dashboard has a
dep on preact and has a preact instance running for the jobq view?
then we could get rid of a lot of extra plumbing" - right: the plain
h() authoring existed only to dodge adding JSX support to the
dashboard's esbuild config, and that dodge is exactly the plumbing to
remove now that the dashboard already depends on preact.
- JobqGraph.js -> JobqGraph.jsx, rewritten in real JSX.
- dashboard/build.mjs: added jsx: 'automatic', jsxImportSource: 'preact'
to the JS-bundle esbuild call (esbuild already picks the jsx loader
for .jsx by extension; this just sets the transform mode, matching
swarm-ui's config). No other entry in that bundle uses JSX today.
- shared/package.json: export target updated to the .jsx file.
The CSS-as-page-level-@import structure is unchanged and stays that
way regardless of JSX: dashboard bundles this component transitively
through one esbuild call whose .css loader is 'text' (for the
shadow-DOM components that need their CSS as a literal string), and
esbuild's loader map is global per call, not per-module - importing
CSS from this component would silently pick up that loader too.
Explained in the file's own top comment.
Verified: npm run build (whole workspace) and npm run typecheck
(swarm-ui) clean. Re-ran the same headless-chromium screenshot against
a mock GET /api/jobq/graph payload as the previous verification -
pixel-identical to the h()-based version, confirming this is a pure
authoring-style refactor with no behavior change.
Ports the shadow-DOM <hive-jobq-graph> custom element
(frontend/packages/shared/src/jobq-graph/) to a Preact component
(JobqGraph.js) shared by the dashboard and swarm-ui, per hyperhive#3310.
- JobqGraph.js: written with plain h() calls (no JSX) so the same file
compiles unmodified under both the dashboard's text-loader CSS config
and swarm-ui's JSX config. Exports `JobqGraph` for JSX use and
`mountJobqGraph(container, props)` for the dashboard's non-JSX
imperative mount, returning a `{refresh(), update()}` handle matching
the old custom element's public surface. Same rendering contract as
before: indented state tree, payload.label verbatim, payload.data as
a generic key/value list, "waits on: <label>" text for Node-kind deps,
per-state filter checkboxes, optional cancel button.
- jobq-graph.css: light-DOM adaptation of the old shadow-scoped
stylesheet (:host -> .jg-root, otherwise unchanged).
- dashboard/src/builds.js: local mountJobqGraph() renamed to
mountRebuildQueue() to avoid colliding with the newly-imported shared
mountJobqGraph; cancel handling is now a plain onCancel callback
instead of a DOM CustomEvent listener (no shadow boundary to cross
anymore).
- dashboard + shared package.json: added preact as a dependency (matches
swarm-ui's existing pin, 10.29.8) - the dashboard was a vanilla-JS MPA
with no Preact/JSX pipeline before this.
- Removed the old hive-jobq-graph.js/.css entirely (confirmed via grep
it had exactly one consumer, dashboard/src/builds.js, so this is a
clean swap, not parallel maintenance of two implementations).
- Updated stale doc-comment references to the old element name in
builds.html, tabs.js, swarm.js, docs/web-ui/dashboard.md, and
hive-c0re/src/job_queue/mod.rs.
Verified: npm run build (whole frontend workspace) and npm run
typecheck (swarm-ui) both clean; cargo build/clippy/test -p hive-c0re
all clean (331 tests, 0 failures); headless-chromium screenshot of
/builds.html against a mock GET /api/jobq/graph payload confirms full
visual/behavioral parity with the old custom element (tree, filter
checkboxes, cancel buttons, error text, waits-on line, data list, live
build log panel).
This covers the dashboard-replacement half of hyperhive#3310 only. The
swarm-ui half (rendering the CreateAgent DAG on the agent-creation page)
is downstream of hyperhive#3306/#3124 landing - no swarm-ui page exists
yet to mount it in.
Removes the SW4RM tab's "P33R H1V3S" headline section
(#peers-block/#peers-section in dashboard.html, renderPeerHives in
swarm.js, its call site + import in tabs.js). Superseded by swarm-ui's
own hive roster page.
Verified: npm run build clean (would have failed to resolve
renderPeerHives if the import/export pair were left mismatched).
Single 🔗 button in the Shell header (direction 1 of 3 proposed on the
issue) — one fixed element regardless of link count, so it can't
clutter as the list grows, and visible on every route since Shell
wraps all of them. Opens a popover listing whatever GET /api/links
returns (icon + label, opens in a new tab); hidden entirely when the
list is empty, same "don't render a dead affordance" rule the old
dashboard's H0M3 tiles follow for Forge/Matrix.
Docs: docs/swarm/ui.md gets a short section on the feature + the
contribute-your-own-entry idiom.
Verified: npm run build + npm run typecheck clean, headless-chromium
screenshots of both the closed and (temporarily forced open for the
screenshot only) open states, pixel-sampled to confirm the popover is
actually using the dark theme vars and not just looking that way in a
downscaled preview.
New /components route showing every ui primitive (Panel, StatusChip,
Table) in each mode it supports: Panel with/without a title,
StatusChip's four tones, Table populated and empty. Sample data only,
no API calls, so it renders identically regardless of backend state.
Linked from the Shell nav next to "overview".
Verified with npm run build + npm run typecheck, and a headless
chromium screenshot of the built dist.
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.
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.
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.
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
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.
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
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.
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.
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.
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.
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.
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.
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.
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).
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).
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).
<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.
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.
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.
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).
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."
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.
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).
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.