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
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).
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).
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.
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.
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.
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.
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.
The dashboard's Panel singleton and the per-agent UI's own inline Panel
IIFE each had their own near-identical implementation of the right-side
slide-in drawer used for file previews, diffs, logs, and inbox/todo
lists. Both are now thin wrappers around a new <hive-side-panel>
shadow-DOM custom element in @hive/shared, following the same house
pattern as <hive-menu>: the element owns and builds all its structural
chrome itself (backdrop, drawer, resize handle, header, title, close
button) in connectedCallback, and only the caller's opaque content node
is projected in via a default <slot> so each package's own
content-type-specific CSS keeps reaching it.
Public API is the union of both originals: open(title, content),
openNamed(name, title, content), refresh(name, title, content),
close(), and currentOwner(). Drag-to-resize + localStorage width
persistence (ported verbatim from the dashboard's original
implementation, the only one of the two that had it) is now available
to both consumers by default — a deliberate behavior widening for the
agent UI, which didn't have resize before. Along the way, fixed a
latent bug in the ported CSS: the resize handle was setting a
--side-panel-w custom property that no width rule ever consumed, so
dragging never actually resized the drawer even though it looked wired
up; the new shared stylesheet's width rule reads it properly.
Each package's own global stylesheet keeps its content-specific rules
(common.css's .side-panel-body .md, agent.css's .side-panel-body
.agent-inbox) exactly where they were — those can never be reached from
the shared element's shadow tree, same architectural floor as
<hive-menu>'s item-row styling. Each wrapper applies a plain
'side-panel-body' compatibility class to its own <hive-side-panel>
instance so those existing selectors keep matching by ordinary
light-DOM descendant matching, with the shared element itself having no
knowledge of what that class name means.
Panel.bind() is gone from both packages' public API — the shared
element wires its own listeners in connectedCallback, so there's no
bind step left to call. tabs.js's one call site (the only bind() caller
in either package) was updated to drop it.
The two original chrome CSS blocks disagreed on several purely visual
details beyond the resize-handle rules (z-index, backdrop color, drawer
border/box-shadow, title typography) — the dashboard's values (the more
feature-complete of the two) were kept as canonical, which is a small
visible style change for the agent UI's panel chrome (thinner border,
no box-shadow, no bold purple title). Flagged for visibility since nothing
in the original two implementations called this out explicitly.
Verified with a real headless-Chromium/CDP harness (bundled the actual
component + built page CSS, served statically, drove via raw CDP) for
both usage shapes: open/close, backdrop-click dismiss, Escape dismiss,
refresh() owner-matching (no-op on wrong owner, applies on matching
owner), and drag-to-resize (drawer width updates live during drag and
persists to localStorage on release).
Per mara's review on #2881: the dropdown box chrome (background/border/
radius/shadow/min-width/white-space) and the trigger button's base
icon-button treatment are both reachable from hive-menu.css now --
the box chrome lives on hive-menu's own shadow-owned .menu-dropdown
wrapper (no slotting constraint at all), and the trigger button is
styled via ::slotted([slot='trigger']) since it's the top-level slotted
node for that slot. Item-row styling stays in the caller's stylesheet
-- ::slotted() only reaches directly-slotted elements, not their
descendants, so individual dropdown items are architecturally
unreachable from hive-menu's shadow tree. Verified interactively via
headless Chromium/CDP: trigger opacity/hover/border-radius and the
dropdown wrapper's background/border all resolve correctly, hover and
click-to-open still work.
<hive-agent-menu> bundled two concerns: the agent-specific trigger/item
list, and generic "click a trigger, get a positioned dropdown" mechanics
(shadow attach, open/close, singleton close-on-open coordination,
outside-click/Escape handling). Pulled the latter out into a new
@hive/shared/hive-menu.js (<hive-menu>), following the established
per-component-directory + ._opts-before-append shadow-DOM pattern
(<hive-dialog>). <hive-agent-menu> now just builds the "⋮" trigger and
the action list and hands them to an internal <hive-menu> instance.
<hive-menu> takes ownership of every <hive-menu> instance in the app for
singleton coordination (closeAllMenus, renamed from closeAllAgentMenus)
— a deliberate widening from the old per-agent-menu-only tracking, since
the mechanism was never agent-specific to begin with.
The one subtlety worth spelling out: <hive-menu> projects the caller's
opaque trigger/content nodes via named <slot>s rather than moving them
into its own shadow root. That's load-bearing, not cosmetic — if it
re-parented them into its own shadow tree instead, <hive-agent-menu>'s
own classes (.agent-menu-btn, .agent-menu-item, ...) would stop applying,
since a <style> only styles elements within the same shadow tree/document
it's part of, and only slotting (not re-parenting) keeps the caller's
nodes in the caller's own tree for styling purposes. That in turn made
<hive-agent-menu>'s own shadow root redundant once it wasn't the thing
positioning or owning open/close state anymore, so it's dropped in favor
of a plain light-DOM element styled by dashboard.css (already the one
page it renders on) — hive-agent-menu.css is gone, its rules folded into
dashboard.css's per-agent-menu section, minus the positioning rules that
moved into hive-menu.css as the new generic `.menu-dropdown` wrapper.
Verified with a standalone esbuild bundle + a cached nix chromium driven
over raw CDP (no puppeteer/playwright/python3 available): hover-reveal
opacity, dropdown open/close/positioning, outside-click/Escape dismissal,
and cross-instance singleton coordination all behave identically to
before the split.
Moves buildAgentMenu's DOM-building body, the menuItem/menuSep/menuLink
helpers, agentMenuPost, and the open-dropdown coordination logic out of
swarm.js and into a new <hive-agent-menu> autonomous custom element
(dashboard/src/agent-menu/), following the same shadow-DOM + one-dir-per-
component shape as hive-dialog. swarm.js's buildAgentMenu is now a thin
wrapper that constructs the element and sets ._opts before appending it,
same convention hive-dialog uses since a custom element created via
document.createElement can't take constructor args.
The module-level "one dropdown open at a time" singleton (previously a
single mutable variable in swarm.js) becomes a tracked Set of open
instances inside the component module; each instance closes itself via
its own close() method rather than another instance reaching into its
shadow internals. The document-level outside-click and Escape listeners
move into the component module too, keyed off e.composedPath() instead of
e.target.closest() -- shadow-DOM event retargeting means a plain
e.target check no longer reliably reaches into a specific instance's
shadow tree. closeAllAgentMenus() is exported for swarm.js's
buildAgentTree, which still needs to close any open menu before it
replaces the container tree DOM.
The hover-reveal opacity rule crosses the shadow boundary via a
--menu-btn-opacity custom property (custom properties inherit through
shadow boundaries): dashboard.css sets it on hover of the light-DOM
hive-agent-menu element, and the component sets it directly from JS while
its own dropdown is open, since that's component-internal state a CSS
selector out in the light DOM can't see. The host element itself takes on
the structural role (flex:none, position:relative, ...) the old
light-DOM .agent-menu wrapper div played, since its shadow tree's
absolute-positioned dropdown needs a positioned ancestor to anchor off of.
Verified end to end with a standalone esbuild-bundled test harness run
under headless chromium: row layout/flex sizing, hover-reveal opacity,
and dropdown positioning all render correctly, and a scripted interaction
pass (singleton exclusivity, outside-click close, Escape close, toggle
behavior, menu-item click close, and the exported closeAllAgentMenus())
all pass.
Pulls the attachShadow-plus-plain-<style>-tag boilerplate that hive-dialog
and hive-toast already build per component into one shared helper, so the
next shadow-DOM custom element (the agent context menu, next commit) can
reuse it instead of re-deriving the same few lines. A plain <style>
element rather than a constructed CSSStyleSheet with adoptedStyleSheets --
each component instance builds its own fresh stylesheet per connect with
no sharing across instances, so adoptedStyleSheets buys nothing here over
the plain, universally-understood <style> tag.
#2875 (merged) fixed this on main's flat modal.js before #2793's
component-dir split landed. Porting the same one-line fix here now
instead of leaving it as a rebase landmine for whichever PR merges
second.
mara: 'i dont like js css attacher. is there a cleaner way?' — yes: each
component instance was already building its own fresh CSSStyleSheet()
per connect, no sharing across instances, so adoptedStyleSheets bought
nothing here over a plain <style> tag. Same raw-text CSS import, just a
simpler attach step.
Splits the shadow-DOM custom elements out of the flat shared/src layout
into per-component directories:
hive-btn/hive-btn.{js,css}
hive-dialog/hive-dialog.{js,css}
hive-toast/hive-toast.{js,css}
hive-dialog and hive-toast were previously defined inline inside
modal.js alongside the openDialog/themedConfirm/themedPrompt/themedToast
orchestration helpers; modal.js is now a slim entry point that imports
the two component modules for their customElements.define side effect
and keeps only the orchestration functions, which aren't components
themselves. hive-dialog.js now imports hive-btn.js directly (it's the
actual consumer that creates <hive-btn> elements), instead of modal.js
importing it on hive-dialog's behalf.
Pulled the identical shadow-root-plus-adopted-stylesheet boilerplate
(previously duplicated between modal.js's local attachShadow() and
hive-btn.js's inline version) into a shared shadow-css.js helper,
attachShadowCss(host, cssText, shadowInit), used by all three
components. Behaviorally identical — same attachShadow() options per
component, just deduplicated.
No external import paths changed: every consumer only ever imported
the package-level @hive/shared/modal.js entry point, never the
component internals directly, so this is fully internal to the shared
package. Verified with a full frontend build (dashboard + agent
bundles).
hive-dialog's dismiss-on-backdrop-click handler checked e.target === this
(the host). Shadow DOM event retargeting sets e.target to the host for
ANY click that originated inside the shadow tree once it reaches a
listener attached on the host itself, not just clicks that actually hit
the host's own rendering — so the check was true for every click inside
.box that no other element's listener consumed first (title, message, a
bare checkbox row with no button to intercept it), immediately closing
the whole dialog. Reported by mara: clicking a checkbox in a
confirmation dialog (e.g. the restart dialog) dismissed the dialog
instead of toggling the box.
Switched to e.composedPath()[0] === this, the true original target
unaffected by retargeting — true only for a genuine backdrop click.
current_flake_rev canonicalizes to /nix/store/<hash>-<pname>; the
hash right after /nix/store/ is what varies between builds, the
trailing -<pname> is constant. slice(-12) was taking the tail, so
two different builds would very likely render the same truncated
string. slice the hash prefix out instead, with a plain head-slice
fallback for a non-store-path rev (e.g. a bare local dir in dev).
damocles caught this in review on PR #2869.
Adds hyperhive_rev to the dashboard's /api/state StateSnapshot,
resolved via the same current_flake_rev helper get_agent_meta's
per-agent hyperhive_rev already uses. home.js renders it next to the
existing hive-identity line, truncated to the last 12 chars with the
full value in title=, hidden when the flake ref isn't a local path pin.
Requested by annika (infra.run) via dmatrix, hyperhive#2854.
Per review: don't use is=, it reads as a hack (and it is one — is=-upgraded
built-ins can never host a shadow root, which is what caused the crash this PR
fixes in the first place). <hive-btn> is now a normal autonomous custom element
wrapping a real <button> inside its own shadow root, so it gets its shadow
encapsulation back (matching hive-dialog/hive-toast) instead of the document-
level stylesheet workaround from the previous commit.
delegatesFocus: true on the shadow root means .focus() on the host (what
modal.js calls for autofocus) reaches the inner button directly. The inner
button's native click is a composed event, so host-level click listeners
(what modal.js/themedPrompt already use) keep working unchanged.
modal.js: el('button', { is: 'hive-btn', ... }) -> el('hive-btn', { ... }) at
the one call site. dom.js: removed the is= special case from el() entirely —
it existed only to support this one now-gone usage. Build clean.
Element.attachShadow() throws NotSupportedError unconditionally for a
customized built-in (<button is="hive-btn">): the spec only allows
autonomous custom elements or a fixed list of native tags to host a
shadow root, and explicitly excludes any is=-upgraded built-in
regardless of which tag it upgrades. button isn't on that list either
way. This made every themed dialog (any confirm/prompt, since openDialog
always renders at least one button) throw and fail to render in a real
browser, though it passed CI since nothing there exercises actual
browser DOM.
hive-dialog and hive-toast are unaffected — both are genuine autonomous
custom elements (extends HTMLElement, no is= upgrade), which are valid
shadow hosts.
Fix: hive-btn no longer calls attachShadow. Styles adopt onto document
once (module-level guard) instead of per-instance shadow root, scoped
via the [is="hive-btn"] attribute selector instead of :host — same
light-DOM approach the rest of the app's .btn consumers already use.
Native button behaviour is untouched, only the styling mechanism
changed. Build clean.
mara: 'wait the common styles is literally just the button stuff? pls
make a button component now as part of this pr and replace the usage
in the modal. replacing all usages and finally removing the btn styles
from common css is a follow up then.'
<hive-btn> (hive-btn.js) is a customized built-in <button is="hive-btn">
with its own shadow root -- extending HTMLButtonElement keeps every
native button behaviour (click/keyboard activation, :disabled, form
participation) instead of re-implementing it on a generic wrapper.
Shadow root holds only an adopted stylesheet + a <slot>, so the
button's light-DOM content (label, or asyncBtn's swapped-in spinner
span) renders through unchanged -- slotted content stays styled by the
light-DOM cascade, so the global .spinner class still applies.
Variants (cancel/confirm/danger) are a 'variant' attribute, not a CSS
class, since they're a semantic prop of the component.
Customized built-ins aren't supported in Safari/WebKit -- fine here,
the project targets recent Firefox only (same reasoning as the
original custom-elements pilot).
Wired into modal.js: HiveDialog's buttons now render as
<button is="hive-btn" variant="...">, replacing the old
component-common.css .btn copy -- deleted that file + component-
styles.js entirely (their sole purpose was giving dialog buttons a
.btn look, which hive-btn now owns properly). dom.js's el() gained
support so it can create customized built-ins the same way it
creates everything else. hive-dialog.css dropped the now-dead
.cancel/.confirm/.confirm.danger rules.
Per mara's scoping: NOT touching the other .btn consumers across the
app (dashboard/agent submit buttons, form() helper, etc.) or removing
.btn from dashboard/common.css / agent/agent.css in this PR -- that
migration + cleanup is an explicit follow-up.
Verified with a full frontend build (grepped bundled JS for hive-btn/
variant to confirm it inlines); nix fmt clean.
mara: 'i dont like sharing css via js, thats not how it should be done.'
Replaced the DIALOG_CSS/TOAST_CSS template-string constants in modal.js
and the inline CSS text in component-styles.js with three real .css
files (hive-dialog.css, hive-toast.css, component-common.css),
imported as raw text via esbuild's 'text' loader and turned into
CSSStyleSheet objects at runtime (same replaceSync() call as before --
only where the CSS text comes from changed). Both packages' build.mjs
gained a '.css': 'text' loader entry on their JS-bundling step; this
doesn't collide with the separate page-stylesheet bundling ('css'
loader), which is a different esbuild invocation over different entry
points.
No behavior change -- same adoptedStyleSheets wiring, same rules,
same output. Verified with a full frontend build (grepped the bundled
JS to confirm the CSS text inlines correctly); nix fmt clean.
Follow-up to the light-DOM custom-elements pilot (mara: 'that one landed
and works. i dont really like the css still being shared - id like that
to be split by component, with common stuff via @include').
<hive-dialog> and <hive-toast> now attach a shadow root and adopt a
component-scoped CSSStyleSheet built from a template-string constant in
modal.js (DIALOG_CSS / TOAST_CSS), plus a new shared/src/component-
styles.js sheet (currently just .btn) adopted alongside it via
adoptedStyleSheets -- the native equivalent of a Sass @include, no
preprocessor added. Theme vars keep resolving through the shadow
boundary since CSS custom properties inherit across it; only plain
class rules needed the explicit move.
Deleted the global shared/src/modal.css entirely and dropped its
@import from both dashboard/common.css and agent/agent.css -- nothing
outside modal.js renders the old .tc-* classes any more. The <hive-
dialog> element is now the backdrop itself (:host carries the fixed-
position/centering rules that used to be .tc-backdrop on a light-DOM
div); box/title/message/content/actions all render inside its shadow
root. <hive-toast> similarly styles :host directly instead of a light-
DOM div, with the message text placed straight into the shadow root
(no <slot> needed since there's no external light-DOM content to
project).
Public API (openDialog/themedConfirm/themedPrompt/themedToast)
unchanged -- no call-site changes needed anywhere in dashboard/agent.
Verified with a full frontend build; nix fmt clean.
`hive_host_sock::jobs::State` was a hand-maintained copy of
`hive_jobq::State` — five variants spelled the same in both, kept in sync
by whoever remembered. Adding `Skipped` last week meant adding it twice.
The wire crate now re-exports the scheduler's enum and `to_wire_state` is
gone.
Two states that were hidden now reach clients. `to_wire_state` renamed
`Pending` to `Queued` and folded `Finishing` into `Running`, so the
dashboard could not distinguish a node waiting on its dependencies from
one whose own work is done while its sub-nodes still run. Both are now
visible, and consumers say which they mean.
Every consumer had to move with it, and only the Rust ones said so: the
exhaustive matches in `hivectl` and `DagView::rollup_state` failed to
compile, while the dashboard's fourteen string comparisons would have
gone quietly wrong — a `finishing` node no longer counting as running,
a `pending` node no longer as queued.
The frontend also builds CSS class names out of the state string
(`rqe-` + state, `rqe-node-` + state) and keys its glyph map on it, all
lowercase. Those go through a `stateSlug` helper now; comparisons use the
wire spelling, presentation lowercases. Without that split every queue
entry and node chip would have silently lost its styling.
Dropping the `State as JobState` alias in hive-c0re falls out of this:
the alias only existed to tell two `State` types apart, and there is one
now.
Closes hyperhive#2788.
State::Skipped now rides the wire (per hive-host-sock's dag_view, no
longer filtered) — this was the last missing piece: the frontend had
no glyph for it, so a skipped node's per-node chip fell through to
QUEUE_STATE_GLYPH's '?' fallback.
Added a 'skipped' entry (·, same quiet glyph hivectl already uses for
the same state — no contract between them, just consistent taste) and
a .rqe-node-skipped CSS rule (dimmed only, no strikethrough —
deliberately distinct from .rqe-node-cancelled: a skipped node wasn't
dropped mid-flight, it was never going to run, so it should read as
expected/quiet rather than alarming). rollupState's defensive arm
(every(n => skipped || done) => done) already landed in #2799 and
needed no changes here.
buildNodeTree has no state-based filtering, so skipped nodes render
in the tree exactly like any other node kind — no other changes
needed. Verified with a full frontend build; nix fmt clean.
A node ruled out by its own dependency edges settles `Skipped` host-side,
but the wire folded it into `Cancelled` and `dag_view` filtered it out
entirely, so a client never saw which branch a run didn't take. Post-#2785
that is not a rare shape: every approval DAG has two not-taken tails and
every rebuild has one, on the happy path as much as on failure.
`State` gains `Skipped`, and it counts as terminal — the wait loops in
hivectl's progress display and the daemon's dag-settled check decide
"finished" with `all(is_terminal)`, so omitting it would hang them on
essentially every DAG.
`dag_view` now emits skipped nodes but no longer lets them keep a DAG
alive. Serialization and completion were the same expression: a DAG left
the snapshot because its nodes had all been filtered away. Keeping skipped
nodes on the wire under that rule would pin every finished deploy in the
queue view forever, so the completion test is now its own flag.
`rollupState` in the dashboard gains the matching arm. It has no `done`
case — `done` is inferred by falling off the wire — so its trailing
`return 'queued'` catches anything it doesn't recognise, and a green
deploy would have read as permanently queued the moment the backend
started emitting the new state. The Rust and JS roll-ups have silently
disagreed before; they are edited together here and say so.
Pilot for the components-split proposal (mara wants a look at using
custom elements now that we're recent-Firefox-only). Picked the
themed dialog system as the first candidate: most self-contained of
our existing de-facto reusable components (transient, imperative call
sites, no external render-tree coupling), and shared between the
dashboard and per-agent UI already.
<hive-dialog> replaces the manually-built tc-backdrop/tc-box tree in
openDialog — connectedCallback renders, the keydown listener and
click-outside-to-dismiss are owned by the element instead of a
closure, and the outcome is reported via a hive-dialog-close
CustomEvent rather than a hand-rolled resolve callback threaded
through the DOM tree.
<hive-toast> replaces the toast div themedToast built inline —
connectedCallback starts the auto-dismiss timer,
disconnectedCallback clears it (previously a closure-captured
setTimeout handle with no explicit cleanup on early removal).
Both are light DOM (no shadow root) — styling stays exactly where it
already lived, in modal.css's .tc-* classes, imported globally by
both packages' base stylesheets. This was the deliberate call for a
first pilot: shadow DOM would need every shared stylesheet
re-imported per instance (CSS custom properties pierce shadow
boundaries for theming, but plain class rules like .btn don't), which
is real migration cost. Light DOM validates the pattern (lifecycle
encapsulation, less manual event bookkeeping) without paying that
cost; shadow DOM is a drop-in upgrade to these same two classes if a
later pilot wants real style encapsulation.
Public API unchanged (openDialog/themedConfirm/themedPrompt/
themedToast) — every existing call site across dashboard + agent
keeps working with no changes. Verified with a full frontend build.
The dashboard has a themed modal/dialog system (modal.js: themedToast/
themedConfirm/themedPrompt) and a data-async form submit interceptor
(bindAsyncForms) that every dashboard action routes through. The
per-agent UI never adopted either — it had its own more primitive
data-async handler using native window.confirm()/alert() (8 call
sites) and a duplicated el() DOM helper.
- Moved el() out of dashboard/common.js into shared/src/dom.js.
- Moved modal.js + modal.css from dashboard/src/ to shared/src/,
updating its internal el import.
- Moved bindAsyncForms from dashboard/common.js into shared/forms.js,
alongside the asyncBtn primitive it's built on.
- Updated every dashboard file's imports to the new shared locations
(no re-export shims).
- agent.css now @imports shared/modal.css so the dialogs render
themed there too.
- agent/app.js: dropped its local el()/data-async duplicate, wired
bindAsyncForms(), and replaced all 8 window.confirm() sites with
themedConfirm (async, wrapped in a fire-and-forget IIFE where the
call site needs a synchronous boolean return, e.g. the slash-command
dispatcher).
Closes hyperhive#2791. Verified with a full frontend build
(npm run build) — both dashboard and agent bundles compile clean and
agent.css picks up the .tc-* dialog styles it previously lacked.
asyncBtn() (shared/src/forms.js) is used by both the dashboard and the
per-agent UI, but its .spinner class + @keyframes spin animation only
lived in dashboard.css. The agent UI's loading spinner rendered as a
static unstyled glyph instead of the animated amber spinner the
dashboard gets. Moved the rule to shared/base.css, which both
common.css (dashboard) and agent.css already @import.
Closes#2720 (partial — kept state + infra card layout).
core.html doesn't load dashboard.css so the .container-row styles from
the operator SPA weren't available. Add equivalent card rules directly in
core.css: .containers flex column, .container-row with bg-elev background
+ border + border-radius, .tombstone dashed variant, .head flex row with
badge + meta, .actions flex row for the action buttons. Matches the visual
weight of agent cards on the main dashboard.
Closes#2720 (partial — schedules + call history fixes).
schedules:
- Agent-name column headers: switch from -45° CSS transform (which clipped
names mid-glyph) to writing-mode:vertical-rl + rotate(180deg). Names now
read bottom-to-top without truncation in their 28px column.
- Shrink 'next' column from 8em → 5.5em and 'every' from 7em → 5em;
these only hold short duration strings so the wider widths wasted space.
call history:
- Approval history <li> items now get a lightweight card treatment
(bg-elev background, 1px border, 3px left accent) matching the visual
weight of the pending .approval-card items above them.
- Left border colour reflects outcome via :has(.glyph-*): green for
approved, red for denied, amber for failed.
Closes#2720 (partial — stats tab placement + contrast).
The time-window nav was in <main> below the page title, so it controlled
hash routing but wasn't visually part of the page chrome. Move it into
<header> (same row as ← home / ST4TS), fill the remaining header space,
and right-align the buttons — consistent with the /logs.html tabbar pattern.
Also set explicit color/border on inactive buttons so they remain readable
on lower-contrast operator colour schemes (var(--subtext1) fallback to
var(--muted)).
Closes#2720 (partial — logs scroll fix).
Make body.logs-shell a full-viewport flex column so the visible .logs-pane
fills the remaining height and .journal-output (already flex:1 overflow:auto)
scrolls its content. Also handle the AUDIT tab's <div> output the same way.
The tab bar and toolbar stay anchored at the top; only the log text scrolls.
Adds CPU/memory cap columns and an inline edit form to the container-load
table in /core.html, backed by a new POST /api/resource-limits/{name}
dashboard endpoint.
## Backend (hive-c0re)
lifecycle_ops.rs — new post_resource_limits handler:
- Parses ResourceLimitsForm { cpu_quota, memory_max } (both optional; empty
string = clear override, fall back to hive-wide default).
- Validates each non-empty value via resource_limits::validate_cpu_quota /
validate_memory_max — returns 422 UNPROCESSABLE_ENTITY with a human-
readable message on invalid input so the dashboard can surface it inline.
- Calls meta::commit_resource_limits (staged git write under META_LOCK, same
as hivectl set-limits).
- Re-applies the drop-in immediately via lifecycle::write_dropins so the new
ceilings take effect on the next container start without waiting for a
rebuild.
- Triggers rescan_containers_and_emit so ContainerView.cpu_quota/memory_max
update via SSE without waiting for the next periodic sweep.
dashboard/mod.rs — registers the route:
POST /api/resource-limits/{name}
## Frontend (core.js + system-sections.css)
core.js:
- containersState derived from /api/state snapshot alongside tombstonesState
— supplies configured cpu_quota/memory_max to the LOAD table.
- lastLoadRows stash lets SSE-triggered re-renders call renderContainerLoad
without waiting for the next 5s poll.
- renderContainerLoad: adds cpu cap / mem cap columns (muted; tooltip
'configured ceiling — takes effect on next start') sourced from
ContainerView, plus a per-row S3T toggle button that expands an inline
edit form with cpu_quota / memory_max text inputs and a S4V3 button.
The edit form shows a restart hint, surfaces validation errors inline, and
collapses on success.
- container_state_changed SSE handler: updates containersState in place and
re-renders the LOAD table so the cap columns flip immediately after a save.
system-sections.css:
- CSS for the new cap columns (.cload-cap-th, .cload-cap) and inline edit
form (.cload-edit-row, .cload-edit-form, .cload-edit-label, etc.).
- Remove dead .rqe-step rule (step sub-step label retired from the wire in
'job_queue: retire the now-off-wire step sub-step label').
logs.html/logs.js previously showed infra containers (hive-ci, hive-forge,
hive-gateway, hive-matrix) in an optgroup within the AGENT tab selector.
This was confusing because infra containers don't run the per-agent hive
daemons, making the unit filter meaningless for them.
Changes:
- Add INFRA tab (between AGENT and SYSTEM) with its own container selector
and full-machine-journal fetch (no unit filter).
- Remove the infra optgroup from the AGENT tab — it now shows agents only.
- loadContainerLists() replaces loadAgentList(): fetches /api/state once and
populates both selectors, avoiding a duplicate network request.
- Deep-link (?agent=hive-ci) now routes to the INFRA tab when the named
container is an infra container, falling back to AGENT otherwise.
- Remove syncUnitSelectForSelection() — no longer needed since the AGENT
tab no longer contains infra containers.
- Extend the 30s timestamp ticker to cover the INFRA tab fetch time.
No backend changes: /api/journal/{name} already supports infra container names.
When ContainerView.paused is true, show a clickable yellow `⏸ paused`
badge on the agent card that POSTs to the new /api/resume/{name} endpoint
to un-park the turn loop. The badge doubles as the resume button so the
state is self-documenting and one click to fix.
The agent action menu gains ⏸ P4US3 (when not paused) and ▶ R3SUM3
(when paused), orthogonal to the running/stopped start/stop actions.
On the backend, /api/pause/{name} and /api/resume/{name} POST routes
wire to Coordinator::set_paused and trigger an immediate rescan so the
badge flips via the existing SSE ContainerUpdate without polling.
Depends on the ContainerView.paused field and Coordinator::set_paused
added in the parent PR.
Replace the Unicode prefix string approach (└─ / ├─ / │ built up as text
in a single <span class="rqe-tree-indent">) with positioned DOM elements
that draw real lines:
- rqe-tree-guide: fixed-width ancestor column, optionally draws a full
vertical border-left when the ancestor has siblings below it
(.rqe-tree-guide-line).
- rqe-tree-connector: draws the L/T shape via ::before (vertical stem,
top→center for last child, full height for mid child) and ::after
(horizontal spur, center→right). .rqe-tree-connector-last vs
.rqe-tree-connector-mid controls stem length.
renderTreeNode() now takes ancestorLines: boolean[] instead of a prefix
string. Each entry is true when the ancestor at that depth was not the
last child (so a vertical guide is still needed through that column).
childAncestorLines propagates depth === 0 correctly (root nodes have no
guide columns, so their children start with an empty array).
Lines are drawn with var(--border) so they follow the theme and work at
any font size without alignment drift. Addresses the review note on PR 2686.
Add NodeView::parent to the wire (hive-sh4re + hive-c0re dag_view), then
render the recursive parent/child tree in the dashboard build queue instead
of the previous flat chain/fan-out layout.
Wire change (hive-sh4re, hive-c0re):
- NodeView gains parent: Option<NodeId> (skip_serializing_if = None)
- dag_view() projects node.parent, filtering out the Dag container id
(top-level work nodes become parent: None on the wire)
Frontend (builds.js):
- Replace nodeComponents + splitFanOut with buildNodeTree (uses parent
edges directly) + topoSort helper (orders siblings by deps)
- renderTreeNode walks the tree depth-first, rendering indented rows
with └─/├─ connectors and agent label per chip
- Flat chains and fan-out heuristics are gone; structure comes straight
from the scheduler's parent axis
Closes: none (parent issue tracked in forge)
Removes entryAgents() and the two places it rendered agent names:
- rqe-agent code element in the entry header
- rqe-node-agent-label prefix per component chain when multi-component
The DAG structure split (WCC + fan-out) already communicates subgraph
boundaries visually via the separate .rqe-nodes rows; the agent-name
labels on top of that caused layout breakage (#2666) and duplicate
information. Closes#2666.
The live build log header still labels liveNode.agent (a single specific
node, not the whole DAG) — that .rqe-agent rule is kept.
Also removes the now-unused .rqe-node-agent-label CSS rule and its
comment.
hive-c0re's broker reminder store and /api/reminders endpoint were
removed in PR #2644 (reminders migrated to in-container sqlite store).
The dashboard had a QU3U3D R3M1ND3RS section on the schedules tab backed
by that endpoint, and a per-agent badge driven by pending_reminders
(always 0 after the migration).
Remove both. Per-agent reminders now surface through get_loose_ends /
the todos pill on the agent page, consistent with the todos migration.
- frontend/packages/dashboard/src/schedules.js: drop refreshReminders,
renderReminders, applyRemindersChanged; drop appendLinkified import
(no longer used); update module comment.
- frontend/packages/dashboard/src/tabs.js: drop applyRemindersChanged
and refreshReminders imports; remove reminders-section from managed
list; remove refreshReminders call site; drop reminders_changed from
SSE dispatch; simplify countdown ticker (reminder-due gone).
- frontend/packages/dashboard/src/dashboard.html: remove QU3U3D
R3M1ND3RS section.
- frontend/packages/dashboard/src/dashboard.css: remove reminder list +
row CSS.
- frontend/packages/dashboard/src/common.css: remove .badge-reminder.
- frontend/packages/dashboard/src/swarm.js: remove pending_reminders
from fingerprint key and badge render.
Both old endpoints removed. refreshLooseEnds() call sites cleaned up;
lastLooseEnds stays as empty [] for reconcileAskBinds (no-op now that
the loose-ends source is gone).
- asyncBtn now returns fn().finally(...) so callers can await/chain it
- Move re-fetch calls inside try/catch in core.js and permissions.js so
network errors from fetchAndRenderStalePerms / fetchAndRender* are
caught instead of escaping as unhandled rejections
- clearStaleAgent returns the asyncBtn promise so the function is
properly awaitable when a button is present
- Update asyncBtn doc comment to reflect the return-value contract