Commit graph

686 commits

Author SHA1 Message Date
iris
918dccfedd flow.js: drop dead window.HiveTerminal (#406 step 2 follow-up)
Per @argus on PR #412: after the common.js step (#410)
`appendText` uses the direct `termLinkify` import and `termCreate`
is called directly in flow.js — nothing reads `window.HiveTerminal`
anywhere. Drop the line + the stale comment alongside.
2026-05-25 02:19:19 +02:00
iris
06c23e0bdc dashboard: extract flow.js as separate /flow.html entry (#406 step 2)
Splits the single bundle into two: `app.js` (entry for /index.html —
tab renderers + tab routing + refreshState) and a new `flow.js`
(entry for /flow.html — operator inbox derived store + inbox pill
flyout + broker terminal + @-mention composer). Both bundles inline
`./common.js` (DOM helpers, Panel, NOTIF, path linkification).

## What `flow.js` owns

- Operator inbox derived store (`operatorInbox`, `INBOX_LIMIT`,
  `inboxAppendFromEvent`, `buildInboxListNode`, `renderInbox`) +
  inbox-pill click wiring
- Broker terminal init (`HiveTerminal.create({ logEl: msgflow, ... })`)
  with `renderMsg`, `pulseBanner`, `msgRowMap` reply-thread indicator,
  and the renderers map for `sent` / `delivered` broker rows
- @-mention composer (`#op-compose-input` IIFE — sticky recipient,
  autocomplete, parseAddressed, /op-send POST)
- A small local `flowContainers` cache for the composer's
  autocomplete, refreshed on cold load + on every SSE reconnect via
  `onStreamOpen`, and live-updated by `container_state_changed` /
  `container_removed` SSE events (the dashboard's `containersState`
  lives in `app.js` and isn't available here)

## What `app.js` no longer does

- Drops the inbox derived store, the bindFlowInboxPill IIFE, the
  broker-terminal IIFE, and the composer IIFE — all moved
- Drops the `renderInbox()` call in `refreshState` (dashboard has
  no #inbox-section element)
- Drops `setTabCount('flow', operatorInbox.length)` — the FL0W tab
  count lives in flow.js now (cross-page count broadcasting is a
  future follow-up; the slot currently stays hidden on /index.html)
- Drops the `window.HiveTerminal` global — the bare-import pattern
  in common.js / flow.js made it unused on the dashboard

## What changes for /flow.html

- `<script src>` switches from `/static/app.js` → `/static/flow.js`
- Mutation events on the dashboard stream (`approval_added`,
  `container_state_changed`, etc.) are silently ignored on /flow.html
  via a `_default: () => {}` renderer (the dashboard tabs aren't on
  this page; firing the legacy applyXxx handlers from here just
  mutated dead stores). #408 follow-up filters this at the SSE level

## Validation

- `npm run build` clean.
- Bundle deltas:
    - `app.js`: 154kb → 135kb (dropped ~19kb of flow code)
    - `flow.js`: NEW 29kb (was bundled into the old 154kb app.js)
    - `flow.html` page total: 154kb → 29kb (flow.js + inlined common,
      no tab renderers shipped)
- Source: `app.js` 2287 → 1907 lines (-380); `flow.js` 423 lines (new)
- No HTML / CSS changes besides the `<script src>` swap on /flow.html.

## Known limitations (out of scope; tracked separately)

- /index.html still has no live SSE subscription — the dashboard
  updates only on cold load + after async-form submits. Pre-existing
  behaviour; the SSE wiring also lived in the flow IIFE before.
  Step 3 of #406 (or its own bug fix) re-wires it.
- /flow.html's `_default: noop` drops mutation events; #408 fixes
  the duplicate-traffic by splitting the SSE endpoint server-side.
- The FL0W tab-strip count pill on /index.html stays hidden — the
  count source is now in flow.js. Broadcast via localStorage /
  BroadcastChannel is a small follow-up if both pages are open.

Browser smoke test isn't possible from inside iris's container.
Worth eyeballing post-deploy:
  - /flow.html: terminal renders broker rows; inbox pill shows count
    + opens flyout; composer autocomplete suggests known agents
    + sends successfully
  - /index.html: tab renderers all work; notification toggle still
    binds; side panel still opens for diffs / file previews / logs
2026-05-25 02:19:19 +02:00
damocles
7e12da83e2 hive-forge: update agent/manager prompts + gotchas for rust rewrite (mara@#407) 2026-05-25 02:16:53 +02:00
damocles
15195b0c47 hive-forge: #[must_use] on Client::repo (mara@#407) 2026-05-25 02:16:53 +02:00
damocles
badf21714b hive-forge: global -r/--repo flag instead of per-verb [repo] positional (mara@#407) 2026-05-25 02:16:53 +02:00
damocles
76cf2ffd36 hive-forge: assign accepts [repo] override (argus@#407) 2026-05-25 02:16:53 +02:00
damocles
595e3c040c hive-forge: rewrite bash CLI helper as a rust binary (closes #280) 2026-05-25 02:16:53 +02:00
iris
560360d2e3 dashboard: extract shared helpers into common.js (#406 step 1)
First slice of the app.js split (#406). Pure utility / infrastructure
code that both /index.html and /flow.html use lifts out of the IIFE
into a sibling ES module:

- DOM helpers: `$`, `el`, `esc`, `form`, `fmtAgeSecs`
- Side-panel singleton (`Panel.open` / `openNamed` / `refresh` /
  `close` / `bind`). The `ensure()` lazy-init makes it tolerate
  being imported before the DOM element exists — `bind()` still
  needs to be called once the host page is ready.
- Path linkification + file-preview side panel:
  `appendLinkified`, `appendText`, `makePathLink`, plus the
  internal `openFilePanel` + `fetchStateFile` + `mdNode` /
  `svgImage` / `buildTabbedPreview` it depends on.
- Browser-notification module `NOTIF` (`bind`, `show`,
  `renderControls`).

`app.js` now imports these from `./common.js` and the duplicated
definitions are gone. Each removal is replaced by a one-line
breadcrumb comment so a reader chasing a name from the bundled
output can find where it landed.

`truncate`, `fmtAgo`, `fmtElapsed`, `fmtDuration` stay in app.js for
now — each has caller-specific phrasing ("X running", "X ago") that
doesn't generalise cleanly. Lift them when a second consumer needs
the same shape.

## Next steps (separate PRs)

- Step 2: split app.js into `tabs.js` (entry for /index.html — tab
  renderers + tab routing + refreshState) and `flow.js` (entry for
  /flow.html — broker terminal + inbox derived store + compose),
  both importing from common.js. Updates `build.mjs` for multiple
  entry points and switches each HTML file's `<script src>`.
- Step 3 (#408 follow-up): backend-side stream split so /flow.html
  doesn't have to subscribe to the dashboard's mutation events at all.

## Validation

- `npm run build` clean.
- Build deltas: `app.js` 154.3kb (was 153.6kb) — bundle size bumped
  slightly due to per-module overhead; same code under the hood.
  Source: app.js 2603 → 2287 lines (-316); common.js 367 lines (new).
- No HTML / CSS changes. Both pages still load `/static/app.js` as
  before.

Browser smoke test isn't possible from inside iris's container.
Worth eyeballing post-deploy:
  - Notification toggle + send still works (NOTIF.bind, NOTIF.show)
  - Side panel still opens for diff / file preview / logs (Panel)
  - Path tokens in messages still render as clickable anchors that
    open the file in the side panel (appendLinkified → makePathLink
    → openFilePanel)
2026-05-25 02:01:49 +02:00
lexis
38920d3af1 docs: update web-ui.md for agent header redesign and in-flight rebuild badge (#394 #398) 2026-05-25 02:00:47 +02:00
iris
c08218acdc dashboard: slug to page footer, not chrome (#389 follow-up)
PR #392 moved the slug below the tab strip but kept it INSIDE the
sticky chrome — Mara's report ("still at old position everywhere
except flow tab") makes the original intent clearer: the slug
should be at the page BOTTOM, with the chrome reduced to pure
navigation.

## index.html
- Remove the banner-thin from `.dashboard-chrome` entirely.
- Move it into the existing `<footer>` element (above the divider
  + project-link line). Sits at the bottom of every tab pane after
  the main content scrolls past.

## flow.html
- Remove the banner-thin from the chrome. The flow page is a
  full-viewport terminal with `body { overflow: hidden }` and
  no normal-flow footer position — the slug simply doesn't appear
  here. The frosted chrome is purely the tab strip now.

## dashboard.css
- `--flow-header-h: 4.7em → 3.6em` — chrome is shorter without
  the banner; terminal padding-top + tail-pill offset + inbox-pill
  top all derive from this variable, so they follow automatically.
- `footer .banner-thin { margin-bottom: 0.8em }` so the slug
  doesn't crash into the divider + link line below it.

No JS changes. Build clean.
2026-05-25 01:59:12 +02:00
iris
4559a56e2e dashboard: guard renderers against missing-root on /flow.html (#399)
Mara hit:

  TypeError: can't access property "innerHTML", root is null
      renderContainers app.js:666
      applyContainerStateChanged app.js:484
      container_state_changed app.js:2306

…on /flow.html. The same bundled `app.js` runs on both /index.html
(dashboard, has the tab panes) and /flow.html (flow page, has only
the broker terminal). SSE events arrive on every page —
`container_state_changed` / `tombstones_changed` / `approval_*` /
`question_*` route through their corresponding renderers, which
then `root.innerHTML = ''` on `$('section-id')` and crash when the
section isn't in the DOM.

The convention is already "no-op when the target DOM doesn't
exist" — `renderMetaInputs`, `renderRebuildQueue`, `renderReminders`
all guard with `if (!root) return;` at the top. Bring the rest in
line:

- `renderContainers`
- `renderTombstones`
- `renderQuestions`
- `renderApprovals`

`renderInbox` already handles the absent case via its
`if (root && !root.hidden)` branch — no change.

No behaviour change on /index.html. On /flow.html the failing
events silently no-op as intended (terminal renderers still
re-render the broker tail normally).
2026-05-25 01:30:11 +02:00
iris
92becdd951 agent: drop orphaned button rules in agent.css (#394 follow-up)
After moving `↑ DASHB04RD` / `↻ R3BU1LD` / `↻ new session` into
the overflow `⋯` menu in #394, the per-button rules in agent.css
have no live consumer:

- `.btn-dashlink` (cyan back-link chip)
- `.btn-rebuild` (amber rebuild chip — note: the dashboard has
  its own `.btn-rebuild` for the per-row R3BU1LD form on the
  SW4RM tab, that stays)
- `.btn-new-session` (amber round-pill button)
- `.btn-send` (green send-button variant — orphaned since an
  earlier compose-form retirement, swept here)

Plus a comment-only `#state-row` block (layout fully provided by
the renamed `.agent-state-row` / `.agent-header-row` classes).

Each removed block is replaced with a one-line "moved to overflow"
comment so future readers don't reintroduce the old chrome by
mistake.

agent.css 23.6kb → 22.4kb.
2026-05-25 01:29:39 +02:00
iris
69312b8553 agent: redesign terminal header — full-height icon, two-row main, overflow menu (#394)
Operator brief (#394): the header had thirteen distinct visual
elements in one flex row with three different border-radius
languages, four colour treatments, three label styles. Mara's
direction:

- agent icon bigger (full header height) as the identity anchor
- title glow stays
- nav links lose their default-anchor underline
- overflow `⋯` absorbs `↻ R3BU1LD` + `↻ new session` (rare,
  destructive — both worth one extra click; rebuild is normally
  done from the dashboard)
- accent stacking in the state strip stays — that's the vibe

## Layout shape

Three flex columns in `.agent-header`:

  [icon · full height] [main column · 2 rows] [pills + ⋯]

The main column carries row 1 (`◆ AGENT ◆` title + meta-nav) on
top and row 2 (alive · state · model · ctx · cost · last-turn ·
cancel-turn) below.

## Changes

### `index.html`
- Wrap title + nav in `.agent-header-row .agent-header-title-row`;
  wrap state-row siblings in `.agent-header-row .agent-state-row`;
  both go inside a new `.agent-header-main` column.
- Right cluster `.agent-header-pills` contains the inbox + loose
  pills + a new `<button id="overflow-btn">⋯</button>` trigger.
- Drop static `#new-session-btn` from `#state-row` — moved into the
  overflow menu, populated dynamically.
- Add `<div id="overflow-menu" role="menu" hidden>` as a sibling
  of `<header>` (lives outside the header so its `position: fixed`
  popover isn't trapped by any header stacking context).

### `agent.css`
- `--agent-header-h: 4.6em → 6em` so the icon can be square + full
  height without crowding the two-row main column. Terminal
  padding-top + status overlay top + tail-pill all derive from
  this variable, so they follow automatically.
- `.agent-header { align-items: stretch }` lets the icon stretch
  to full height; `.agent-icon { height: 100%; aspect-ratio: 1 }`
  sizes it as a square off the stretched height.
- `.agent-nav-link` rule added — `text-decoration: none`, cyan +
  soft glow, hover lights brighter (mara's spec).
- `.overflow-btn` (round trigger) + `.overflow-menu` (frosted
  popover, fixed-position) + `.overflow-item` (rows with an icon
  column + label, hover ink matches per-action accent — cyan for
  dashboard, amber for rebuild/new-session).
- Remove the old `#state-row` selector (layout now provided by
  `.agent-state-row` + `.agent-header-row`).

### `app.js`
- `setHeader` no longer appends DASHB04RD / R3BU1LD chips into the
  title — title is just the identity glyph now. Both actions get
  rendered into the overflow menu by `populateOverflowMenu()`.
- `populateOverflowMenu(label, dashUrl)` builds three rows:
  `↑ dashboard` (anchor), `↻ rebuild container` (button — same
  POST-form action as before), `↻ new claude session` (button —
  same `/api/new-session` call as the legacy header button).
- Overflow toggle / outside-click / Escape dismissal — same
  pattern as the side-panel flyout (`Panel`).
- Drop the static `new-session-btn` IIFE binder; the dynamically-
  rendered menu item owns its handler now.
- Drop the per-nav-link inline `marginLeft` (layout gap comes from
  the new `.agent-nav { gap }` rule).

## Validation

- `npm run build` clean.
- Build deltas: agent.css 21.0kb → 23.6kb (overflow + nav rules
  + comments), app.js 117.4kb → 118.9kb (menu builder + toggle).
- Browser smoke test isn't possible from inside iris's container.
  Worth eyeballing post-deploy:
    - Icon fills the full header height as a square
    - Title glow + uppercase styling preserved
    - Nav links render without underline; hover lights brighter
    - `⋯` opens a frosted popover with `↑ dashboard`, `↻ rebuild
      container`, `↻ new claude session`
    - Rebuild confirm + POST works the same as the legacy chip
    - New-session confirm + POST works the same as the legacy button
    - State strip still wraps when crowded (model/ctx/cost
      multi-line on narrow viewports)
    - Cancel-turn button still appears while thinking and clears
      on turn end
    - Terminal padding-top adjusts to the new 6em header height
      (no row hidden under the chrome)
2026-05-25 01:29:39 +02:00
iris
88bc07fbbe dashboard: surface in-flight rebuild on container card (#398)
The SW4RM tab's container card was reading container state
straight from the snapshot — when a rebuild was in flight and the
container was momentarily stopped between teardown and bring-up,
the card showed "stopped" while the SYST3M tab's rebuild queue
showed the operation running. The two surfaces disagreed.

Mara: *should show as building in container page as well*.

Cross-reference: build a `inFlightOpsByAgent()` map from
`rebuildQueueState` (kinds: rebuild / meta_update / destroy;
states: queued / running — skip `spawn` since transients already
drive that case). When rendering each container row, prefer the
operator-initiated transient if set; otherwise fall back to the
in-flight queue entry as a synthetic pending kind:
`rebuilding` / `meta-updating` / `destroying` (or `… queued`
when still waiting on the worker). The existing `pending-state`
spinner badge surfaces it visually — no new CSS rule needed.

Also wire `applyRebuildQueueChanged` to re-render containers so
the badge lights up the moment a rebuild lands in the queue and
clears the moment it finishes — no manual refresh.
2026-05-25 01:16:10 +02:00
iris
7743c07380 dashboard: connect tree-prefix vertical bars across taller rows (#388)
The container-row tree-prefix used text box-drawing glyphs (├ └ │)
positioned with `top: 0.6em` — a single text-line tall. Once rows
grew past one line (5em square icon + multi-line body), the `│`
columns of consecutive siblings no longer touched, leaving visible
breaks in the tree.

Replace the text-glyph string with structured DOM: one `.tree-lane`
per depth column. Continuation lanes (`.lane-line`) paint a 1px
border-left spanning the full row height + the `.containers` gap
below, so adjacent siblings' bars visually merge into one unbroken
vertical. The row's own joint lane is `├` (branch — bar continues
below) or `└` (last — bar stops at icon midline), with a horizontal
stub at 3.1em (row padding-top + icon half-height) reaching to the
icon edge.

Joint y / stub width are derived from the 5em icon + 0.6em row
padding-top + 0.8em row padding-left so they meet the icon cleanly.
2026-05-25 01:09:03 +02:00
iris
47403595f1 terminal: anchor tail pill outside .terminal-wrap stacking context (#375)
#375 set `z-index: 35` on the agent page's tail pill so it'd float
above the fixed composer (z-index 30). The fix only landed because
nothing else in `.agent-main` created a stacking context. But the
pill is anchored inside `.terminal-wrap`, which carries
`backdrop-filter: blur(...) saturate(...)` for the frost effect —
and `backdrop-filter` CREATES A STACKING CONTEXT. The pill's
z-index 35 was trapped inside that context and never got to compete
with the composer's z-index 30 in the root stacking context, so the
operator still saw the badge clipped under the input box.

Same root cause on the flow page — `.flow-main .terminal-wrap`
inherits the same backdrop-filter rule.

Fix: anchor the pill in `.agent-main` / `.flow-main` instead of
`log.parentElement` (= `.terminal-wrap`). Both ancestors are
`position: absolute` with `overflow: hidden` but NO backdrop-filter
or other stacking-context creators, so the pill's z-index reaches
the root and properly floats above the composer.

Geometry unchanged — `.agent-main` / `.flow-main` and the
`.terminal-wrap` they contain both `inset: 0` the same area, so the
pill's `bottom: calc(--composer-h + 0.6em)` lands at the same y.

Also added `.flow-main .tail-pill { z-index: 35 }` (the flow page
was missing the per-page z-index bump that the agent page already
had).

`pillAnchor` is an existing opt in @hive/shared/terminal.js (the
default is `log.parentElement`); both consumers now set it explicitly.
2026-05-25 00:52:51 +02:00
iris
9975be9c08 terminal: snap to bottom after post-row mutations (#393)
#375 captured `wasNearBottom` BEFORE the row's initial append so
the autoscroll wouldn't be tricked by the new row's own height.
That's still right, but it leaves a second gap: renderers commonly
call `api.row(cls, text)` to build the shell, then mutate the
returned element by appending more children (badges, multi-line
turn bodies, tool-result panes). The initial scrollTop assignment
in afterAppend only sees the row's INITIAL height — once the
renderer adds the body, the row's grown past the visible bottom
and the operator's stranded scrolled to the row's TOP, breaking
stick-to-bottom for every subsequent event.

Mara's symptom: two lines of text appear, the view scrolls only
one row, the terminal isn't at the bottom anymore, and the next
event doesn't auto-scroll because `isNearBottom()` now returns
false.

Fix: add a `stickToBottom` boolean (updated synchronously from the
scroll event handler) + a MutationObserver on the log subtree
(`childList`, `subtree`, `characterData`). On any mutation, if
stickToBottom is true, re-snap to bottom. MutationObserver
batches mutations into one microtask callback per synchronous
block, so it runs once per renderer call regardless of how many
children the renderer appends after `api.row` returned.

Programmatic `scrollTop = scrollHeight` doesn't trigger MO (the
scroll isn't a DOM mutation), so no feedback loop. Operator
scrolling up still flips stickToBottom to false via the existing
scroll handler — MO becomes a no-op until they scroll back.

Same shared `@hive/shared/terminal.js` powers the dashboard's
flow page + per-agent terminal, so both pages inherit the fix.
2026-05-25 00:47:31 +02:00
lexis
e9cce17828 docs: update web-ui.md for tabbed layout, topology tree, per-agent overhaul (#366 #373 #381) 2026-05-25 00:45:56 +02:00
iris
91a72b5cbb dashboard: tab strip above banner-thin in chrome (#389)
Swap the order of `.tabbar` and `.banner` inside `.dashboard-chrome`
so the operator's navigation surface sits at the very top of the
sticky/fixed header — the "WE ARE THE WIRED" slug becomes decoration
below the tabs rather than chrome above them.

Applied to both `index.html` (sticky chrome) and `flow.html`
(fixed-overlay chrome).

No CSS changes — `.tabbar { border-bottom }` still divides tabs from
the area below (now the banner), and active-tab `margin-bottom: -1px`
still merges into that boundary cleanly.
2026-05-25 00:38:12 +02:00
damocles
d79b8a0a38 hive-forge: --body-file + piped stdin for pr-create / issue-create / issue-edit (closes #382) 2026-05-25 00:12:43 +02:00
iris
ba669d2d6c flow: keep the dashboard tab strip on the flow page (#383)
Operator wanted the tab header visible on /flow.html so switching
tabs doesn't require navigating back to / first.

The flow page now reuses the same `<header class="dashboard-chrome">`
markup the dashboard renders, with a few tweaks:

- The SW4RM / Y3R C4LL / SYST3M tabs are cross-page links
  (`href="/#swarm"` etc.) — clicking lands on the dashboard with the
  destination tab pre-active via the hash router.
- The FL0W tab is rendered `.active.tab-link` + `aria-current="page"`
  so it reads as the current view (no clickable arrow / "go here"
  affordance — you're already here).
- Banner-thin echoes the dashboard for visual continuity.
- Notif controls cohabit with the tabs (same IDs the dashboard uses,
  so app.js's NOTIF binding picks them up unchanged).

Layout glue:
- `body.flow-shell .dashboard-chrome.flow-chrome` overrides the
  dashboard's `position: sticky` with `position: fixed` so the
  chrome stays put under flow-shell's `overflow: hidden` body
  layout, keeping the terminal full-viewport behind/beneath.
- New rule for the active FL0W tab — the `.tab-link` styling on the
  dashboard otherwise reads as a passive cross-page link; here we
  need it lit-up like a regular active tab.
- `--flow-header-h` bumped from 4.2em → 4.7em to match the natural
  height of the tab strip + banner combo. Terminal padding +
  inbox-pill top offset both derive from this variable, so they
  follow automatically.

Removed:
- Legacy `.flow-title`, `.flow-hint`, `.flow-back` CSS rules (their
  HTML counterparts are gone — the tab strip carries the
  identity now).
- The `<a class="flow-back">← d4shb04rd</a>` link and the
  `<h2 class="flow-title">` from flow.html.

## Validation

`npm run build` clean.
  dashboard.css: 38kb → 37kb (legacy rules removed, new shared-
                  chrome rules are smaller)
  flow.html:    4.4kb → 4.7kb (tab strip replaces title bar)
  app.js:       unchanged (no JS changes — the tab navigation is
                pure HTML href + cross-page hash)

Closes #383.
2026-05-25 00:12:28 +02:00
iris
3abd0ba711 dashboard: drop redundant C0NTAINERS heading from SW4RM tab (#385)
The SW4RM tab label already announces the section — an inline
`<h2>◆ C0NTAINERS ◆</h2>` + divider underneath was redundant
ink. The tab is single-section so there's no other content to
disambiguate from.

`#containers-section` div stays put (app.js targets it by id);
just the heading + divider go.

`npm run build` clean.

Closes #385.
2026-05-24 14:24:56 +02:00
damocles
1294ca2755 hive-forge: stdin beats bare positional for comment / comment-edit (closes #379) 2026-05-24 14:09:12 +02:00
iris
d4bb3e4aa0 agent: loose-ends flyout body on its own row (#376)
Operator: the per-entry header (from/sep/ts) + body were on the same
grid row, with the body squeezed into a 1fr column. In the side-
panel flyout's ~640px width, after the auto header columns ate
their share, the body wrapped over many narrow lines.

Fix: `.agent-inbox li` switches from `display: grid` to
`display: block`. The body element (currently a `<div>` for
loose-ends and a `<span>` for inbox messages) gets explicit
`display: block` so it always breaks to a new line under the
header. Light `padding-left + border-left` indent on the body
gives a visual relationship to the header row above without
needing a column structure.

`inbox-ts` + `inbox-sep` pick up small left margins to preserve
the inline spacing the grid's `gap: 0.5em` used to provide.

`.answer-form` drops its no-longer-applicable `grid-column: 1 / -1`
(replaced with a regular block layout + matching padding-left so
the form aligns with the question body it answers).

## Validation

`npm run build` clean. Bundle deltas: agent.css unchanged in
bundled size (rules swapped 1:1).

Browser smoke test isn't possible from inside iris's container.
Worth eyeballing post-deploy:
  - Loose-ends entries with long question/description bodies wrap
    to the panel width rather than getting clipped into a narrow
    fourth column.
  - The header line stays compact and readable.
  - Answer-form for a question loose-end still aligns visually
    under the question body it answers.

Closes #376.
2026-05-24 13:22:41 +02:00
iris
5615da9211 terminal: tail pill above floating chrome + autoscroll fix (#375)
Two bugs on the agent terminal page after the #362 overhaul:

## 1. `↓ N new` pill clipped by the composer

The fixed-overlay composer (z-index 30, agent.css) sits in the
root stacking context. The pill — `position: absolute` inside
`.live.terminal` with no z-index — defaults to its document-order
position in the body's stacking order, which the composer covers.

Fix: bump `.agent-main .tail-pill { z-index: 35 }` so the pill
participates in the root stacking context above the composer.
Scoped to the agent-page overlay layout — the shared `.tail-pill`
rule stays untouched (the dashboard's in-page layout doesn't need
the bump).

## 2. Autoscroll-on-new-message not firing when the operator was
   already at the bottom

`afterAppend()` in terminal.js was calling `isNearBottom()` AFTER
appending the new row. The new row's own height is already in
`scrollHeight` at that point, so for any row taller than the
NEAR_BOTTOM_PX threshold (48px — easily passed by a multi-line
message body, a tool-result summary, a markdown block), the check
returns false and the pill shows + scroll stays put.

Fix: capture `wasNearBottom = isNearBottom()` BEFORE the
`log.appendChild(...)` in each of `row` / `details` /
`detailsDiff`, pass it into `afterAppend(wasNearBottom)`. Now the
auto-scroll triggers whenever the operator was visually at the
bottom an instant before the row landed, regardless of the new
row's height.

Same shared `@hive/shared/terminal.js` is used by the dashboard
+ per-agent UI + the upcoming /flow.html page, so both pages
inherit the fix.

## Validation

`npm run build` clean.

Bundle deltas: shared terminal bundle re-inlined into both consumers
unchanged in size (the wasNearBottom variable is a single bool, no
measurable delta). Agent CSS +0.1kb (z-index property).

Browser smoke test isn't possible from inside iris's container —
worth eyeballing post-deploy:
  - With the operator scrolled to bottom, a tall message lands and
    the view scrolls to keep it visible (instead of pinning the
    pill).
  - The pill appears above the composer when the operator is
    scrolled up and new messages land.

Closes #375.
2026-05-24 13:22:18 +02:00
damocles
14b79f43cf lifecycle: stop before update for boot-style apply (mara@#372) 2026-05-24 13:16:31 +02:00
damocles
bc872fffb9 lifecycle: drop defensive is_running check per mara@#372 2026-05-24 13:16:31 +02:00
damocles
0540f213f1 lifecycle: use nixos-container restart for the running case (mara@#372) 2026-05-24 13:16:31 +02:00
damocles
48420890e0 lifecycle: preserve stopped state across rebuild (closes #371) 2026-05-24 13:16:31 +02:00
iris
9666cb8c3f dashboard: tab-bar restructure + extract FL0W to /flow.html (#369)
Operator: 'option A (tabs)' (#369#issuecomment-3434) +
'yes terminal can be a separate page' (#369#issuecomment-3437).

## Tab framework

`index.html` becomes a 3-tab dashboard with a sticky chrome header:

- `◆ SW4RM ◆`    — containers list (the central thing)
- `◆ Y3R C4LL ◆` — pending approvals + operator-targeted questions
- `◆ SYST3M ◆`   — meta inputs + rebuild queue + reminders + tombstones

Hash routing: `#swarm` / `#call` / `#system` (empty → SW4RM).
F5-reloadable + back-button-aware without a router framework.

SSE stays alive across tab switches — count pills on inactive tabs
update live so the operator never loses pulse on what's happening
elsewhere:

  - SW4RM:  containers with needs_update
  - Y3R C4LL: approvals.pending + questions.pending (attn-coloured pill)
  - SYST3M: rebuild_queue entries in Queued|Running

Pills hidden when count is zero. setInterval(1s) polls the existing
state stores (cheap, no per-renderer hookup needed).

## FL0W as its own page

The all-agents chat moves to /flow.html — full-viewport vibec0re
layout mirroring the per-agent live page (#362):

- Fixed-overlay frosted-glass header at top (back link + title +
  notif controls), backdrop-filter blur shows the scrolled chat
  text behind.
- Full-viewport terminal, scroll-padded for the floating chrome so
  first/last rows stay reachable.
- Fixed-overlay frosted composer at the bottom.
- Operator inbox surfaces via a pill (📬 inbox · N) in the upper
  right — click opens the side-panel flyout with the message list.

In the dashboard tab strip, FL0W is the right-most entry but
renders as a `<a class="tab tab-link" href="/flow.html">` — clicking
navigates to the page rather than swapping a pane. Same pattern
back from flow.html via the `← d4shb04rd` link.

## Implementation notes

- New `/flow.html` page rendered by the same bundled `app.js` — the
  flow page just doesn't have the dashboard-chrome DOM, so the
  matching renderers no-op silently (each `if (!el) return`).
  Avoids splitting the bundle for v1; can extract later if size
  becomes a concern.
- `Panel` module gains `openNamed(name, …)` + `refresh(name, …)` —
  the legacy untyped `open(title, content)` calls clear the owner,
  so file-preview / diff / log drill-ins behave unchanged. `refresh`
  is no-op when a different view owns the panel, so live message
  events re-render the inbox flyout only when it's actually open.
- `renderInbox` updates BOTH the dashboard's inline `#inbox-section`
  (now living on the flow page) AND the flow page's pill count +
  side-panel refresh. The dashboard's empty FL0W tab is removed —
  inbox + message flow + compose box only exist in flow.html.
- Banner shrinks to a thin Catppuccin gradient strip at the top of
  the dashboard chrome (dropped the multi-line ASCII art —
  affectionate but pure chrome budget in a tabbed layout).
- `build.mjs` copies both `index.html` + `flow.html` into dist.

## Validation

`npm run build` clean. Dashboard bundle deltas:
  app.js  150kb → 152kb  (tab routing + count pills + named-Panel)
  dashboard.css 33kb → 38kb (tab chrome + flow page layout)
  + dist/flow.html  4.4kb

Browser smoke test isn't possible from inside iris's container
(no JS engine) — drafting as a PR for operator visual review on
next deploy. Worth eyeballing:
  - Tab switching feels right; counts update live across SSE events
  - FL0W page reads like the agent live page (frosted header + composer)
  - Inbox pill opens flyout; live message arrivals refresh it
  - Back link from flow → dashboard returns to last tab via the
    URL hash (browser remembers the hash across page nav)

Closes #369.
2026-05-24 13:03:53 +02:00
iris
8c7bc850f3 dashboard: render agent topology as a tree in the container list
Closes #363 (frontend half of milestone #361). Consumes the
`ContainerView.parent: Option<String>` field landing in damocles'
backend slice — when present, the container list renders depth-first
with sibling-position tree glyphs (├─ / └─ joints + │ continuation
columns). When absent (pre-#361 state — every container's `parent` is
None) the tree collapses to a flat list with no glyphs and no indent
— bit-identical to the legacy render.

## Tree shape

- Roots: containers whose `parent` is None OR whose parent name isn't
  in the current container set (orphan tolerance).
- Sibling ordering: alphabetical by name within each level (matches
  damocles' wire spec at #363#issuecomment-3356).
- Cycle safety: any container not reached via the root-walk gets
  emitted as a root at the end — no agent ever silently disappears
  from the list when the topology is malformed.
- Tree glyphs: ancestor at depth d contributes a │ continuation column
  when that ancestor has more siblings below; otherwise a 3-space gap.
  The joint is ├─ for non-last siblings, └─ for the last child.
- Depth-0 ancestor column is suppressed: roots already separate
  visually as top-level rows, no need for a column 0 vertical line.

## DOM / CSS

- New `buildAgentTree(containers)` + `treePrefix(node)` helpers in
  app.js. The render loop walks the tree-ordered list instead of the
  legacy alphabetical containers array.
- Each container row gets `data-depth=N` (only when N > 0) and a
  `<span class="tree-prefix">…</span>` prepended (absolute-positioned
  into the row's left margin so the existing flex icon/body layout
  isn't disrupted).
- CSS: per-depth `margin-left` step rules for depths 1-6 (hardcoded
  rather than typed-attr() because CSS Values 5 is Chromium-only as
  of 2026). 6 depths cover any reasonable hive topology with
  headroom; deeper agents render at depth 6 indent without further
  step — visually clamps gracefully.
- `.tree-prefix` rendered with `var(--purple-dim)` so the structural
  lines read as supporting chrome, not as content.

## Validation

- `npm run build` clean. Bundle deltas: dashboard app.js
  +1.8kb (tree builder + treePrefix + render-loop tweak),
  dashboard.css +0.4kb (tree-prefix + per-depth indent rules).
- The render is a no-op until `ContainerView.parent` is populated —
  validation in production deferred to once damocles' meta-topology
  field lands. The pre-#361 path (every parent=None) is exercised
  by every existing dashboard load.
- Forward-compatible with damocles' design pivot at #364 (topology
  source moved from agent.nix to meta/topology.json). The wire shape
  on ContainerView is unchanged from the frontend's perspective — the
  field is just sourced from a different backend store.
2026-05-24 12:04:19 +02:00
damocles
3fa12bf363 rebuild_queue: dedup MetaUpdate on (kind, agent, inputs) (closes #365) 2026-05-24 11:43:47 +02:00
damocles
0b03d5bcfb topology: meta-repo agent hierarchy + ContainerView.parent (#361) 2026-05-24 04:47:55 +02:00
iris
e931c08739 frontend: vibec0re terminal overhaul (#360)
Per operator spec at #360#issuecomment-3333:
- full-screen terminal
- frosted-glass header overlaid on top
- inbox + loose-ends → flyout
- no a/b flag, just ship it

## Layout

`frontend/packages/agent/src/index.html` restructured to a three-zone
fixed-overlay shape:

- `<header.agent-header>` — fixed top, frosted glass via
  `backdrop-filter: blur(12px) saturate(140%)`. Holds icon + title +
  nav links + state-row (badges/buttons) + two new pill buttons that
  surface inbox / loose-ends counts (and open the side panel on click).
- `<main.agent-main>` — fills the viewport. Terminal positioned absolute
  inset:0 with padding-top/-bottom + scroll-padding equal to the
  floating header/composer heights so first/last rows stay reachable
  and `↓ N new` pill anchors land in the visible scroll zone.
- `<footer.agent-composer>` — fixed bottom, mirror-frosted. Owns
  `#term-input`; dropped the in-frame dashed separator (border-top
  + box-shadow on the bar already separate it from the terminal).
- `<div.side-panel>` — singleton drawer (copy of the dashboard
  pattern, candidate for extraction into @hive/shared). Inbox +
  loose-ends details render here instead of expanding inline.

Dropped from the page: the pre-banner ASCII shimmer (`<pre.banner>`)
and the in-page `<details>` collapsibles for inbox + loose-ends. The
banner JS path (`setBannerActive`) is now a no-op (early-returns on
missing element); kept as dead code rather than ripped out to keep
the diff focused.

## JS

`frontend/packages/agent/src/app.js`:

- New `Panel` singleton with `open(name, title, content)` +
  `close()` + `refresh(name, title, content)` (no-op if a different
  view owns the panel — lets live updates re-render an open view
  without grabbing focus from a closed one). Mirror of the
  dashboard's Panel module; the duplication is intentional for now.
- `renderInbox` + `renderLooseEnds` refactored: update the header
  pill counts, hide/show the pills, and `Panel.refresh` if the
  matching view is open. The list-building DOM logic moved into
  `buildInboxList` + `buildLooseEndsList` so the pill click handler
  can call them on the latest snapshot kept in `lastInbox` /
  `lastLooseEnds` module state.
- Pill click handlers `Panel.open(...)` with the freshly built list.
- Auto-expand behavior on first appearance dropped (the pill +
  count badge is the discoverable signal; auto-popping the flyout
  would interrupt whatever the operator is doing).
- `setHeader` no longer touches `#banner` (element removed); title +
  dashboard back-link + rebuild button still get appended to `#title`.

## CSS

`frontend/packages/agent/src/agent.css` major additions, scoped
`body.agent-shell` so the sibling `stats.html` (which doesn't apply
the shell class) keeps its normal-document scroll + `.banner` ASCII
header via a `body:not(.agent-shell)` block.

New CSS custom properties on :root: `--agent-header-h`,
`--agent-composer-h`, `--agent-frost-bg`, `--agent-frost-blur`. The
terminal's padding + scroll-padding derive from these so a single
height tweak ripples consistently.

Added `.header-pill` (inbox/loose-ends triggers) +
`.agent-status-overlay` (centred login card when status != online).
Side-panel rules copied from `dashboard.css` with one delta: width
caps at 640px (vs dashboard's 760px) since per-agent inbox / loose-
ends rows are narrower than approval diffs / file previews.

## Validation

- `npm run build` — succeeds both workspaces.
  - agent: `dist/static/{app.js (115kb), stats.js (435kb), agent.css (21kb)}`
  - dashboard unchanged (no shared sources touched).
- Browser smoke test isn't possible from inside iris's container
  (no JS engine) — op-side check on next deploy.

Closes #360.
2026-05-24 03:49:26 +02:00
damocles
4d7c767eb0 meta: emit nixpkgs follows for agents that declare it (closes #355) 2026-05-24 02:45:11 +02:00
damocles
8e5112aa27 hive-forge: fix assign endpoint + surface API errors via --fail-with-body (#353) 2026-05-23 15:53:06 +02:00
lexis
b3e94760e1 docs: add hyperhive.frontend.* options to README (follow-up to #350) 2026-05-23 15:03:51 +02:00
damocles
09c51c87aa meta: collapse hyperhive's nixpkgs into meta's via follows (#317) 2026-05-23 14:51:40 +02:00
iris
0e2319d206 frontend: populate real npmDepsHash from prefetch-npm-deps
Manager approval 1b1bcca added `pkgs.prefetch-npm-deps` to my
container. Ran `prefetch-npm-deps frontend/package-lock.json` →
`sha256-MHXxkZpe/5LAhpQ76ZK94znG2noTobthjUi6iNY8/K4=`. Replaced
the `lib.fakeHash` placeholder in `nix/frontend.nix` with the real
value; updated the comment to point at the recompute command instead
of the let-it-fail workflow.

This unblocks PR #350 for merge — `nix build .#frontend` will now
succeed without the operator having to compute and patch the hash.

Refs #273.
2026-05-23 14:51:01 +02:00
iris
65532e8387 frontend: tighten extraFiles target type to strMatching regex
damocles suggested using lib.types.strMatching for the target option
itself rather than relying solely on the post-hoc assertion. Pattern:
`^[A-Za-z0-9_][A-Za-z0-9_./-]*$` — first char alphanumeric/_, then
alphanumerics + _ + . + / + - allowed (so nested layouts like
"games/bitburner" still work).

This rejects at type-check time:
- leading `/` (absolute paths)
- leading `.` (so `..` as a full string blocked, also `./foo`)
- leading `-` (would parse as flag by some tools)
- spaces, control chars, weird unicode

The existing assertion stays — it catches mid-path `..` segments
(`foo/../bar`) that the regex can't reject without lookahead. POSIX
regex (which nix uses) doesn't support lookahead, so the
type-and-assertion split is the cleanest expression.

Refs #273.
2026-05-23 14:51:01 +02:00
iris
2951da32e7 frontend: tighten extraFiles target validation per damocles review
Follow-up to PR #350 review:

1. New assertion: hyperhive.frontend.extraFiles[*].target must be a
   relative path inside the static dir — leading '/' and '..'
   segments rejected at config eval time. Belt-and-braces against
   string-concat-into-paths escapes (the boundary doc flags this
   pattern even though agent.nix goes through operator review).

2. Documented overwrite semantics in the option doc: collision with
   a default-dist path or with a prior entry's target is a hard-fail
   (`refusing to overwrite existing path …`). To override a default
   file, fork `hyperhive.frontend.dist` instead — extraFiles is
   pure additions.

The collision-hard-fail behaviour was already implemented in
`mergedDist` (in commit a19e156); this commit just makes the
contract explicit in the docstring.

Refs #273, addresses damocles' notes on PR #350.
2026-05-23 14:51:01 +02:00
iris
229c4292e9 frontend: cut over Rust binaries to ServeDir; delete legacy assets
Phase 4 of #273 — the actual switch. Both axum routers now serve
their static surface via `tower_http::services::ServeDir` mounted
as a fallback service, reading the dist path from `HIVE_STATIC_DIR`
(set by Phase 3's NixOS module wiring).

Deletes:
- `hive-c0re/assets/{index.html, app.js, dashboard.css}`
- `hive-ag3nt/assets/{index.html, app.js, agent.css, stats.html,
   stats.js, screen.html}`
- The whole `hive-fr0nt/` crate (workspace member dropped, both
  hive-c0re and hive-ag3nt drop their `hive-fr0nt.workspace = true`
  dep). Its contents now live as `@hive/shared` under
  `frontend/packages/shared/`.

Rust changes:
- `hive-c0re/src/dashboard.rs`: remove `serve_index`, `serve_css`,
  `serve_app_js`, `serve_shared_js`, `serve_marked_js`,
  `serve_favicon` (all six `include_str!` handlers); replace their
  routes with a single `.fallback_service(ServeDir::new(static_dir))`
  on the router. Fail closed (anyhow::bail) if `HIVE_STATIC_DIR` is
  unset or not a directory at startup.
- `hive-ag3nt/src/web_ui.rs`: remove `serve_index`, `serve_css`,
  `serve_app_js`, `serve_shared_js`, `serve_marked_js`,
  `serve_stats`, `serve_stats_js`, `serve_screen`; same
  `fallback_service` pattern. `serve_icon` stays (consumes
  `/etc/hyperhive/icon.svg` + `branding/hyperhive.svg` fallback,
  neither of which lives under the frontend dist).
- `AgentLink` URLs for stats/screen switched from `/stats` / `/screen`
  to `/stats.html` / `/screen.html` since ServeDir doesn't auto-
  append the extension and the on-disk filename is the natural URL
  post-cutover.
- `Cargo.toml` (workspace): drop `hive-fr0nt` member + workspace
  dep, add `tower-http = { version = "0.6", features = ["fs"] }`.
- `hive-c0re/Cargo.toml` + `hive-ag3nt/Cargo.toml`: drop the
  `hive-fr0nt.workspace = true` dep, add `tower-http.workspace =
  true`.

Docs updated:
- `CLAUDE.md`: file map reflects `frontend/` (was `hive-fr0nt/` +
  `assets/`) and the ServeDir/HIVE_STATIC_DIR shape.
- `docs/web-ui.md` 'Shape (shared by both)' section: describes the
  ServeDir fallback + bundled-by-esbuild surface, no more
  `include_str!` references.
- `docs/terminal-rendering.md`: src paths point at
  `frontend/packages/{agent,shared}/src/`; marked is the npm dep,
  not vendored UMD.

Validation:
- `cargo check --workspace` — clean (5 warnings, all pre-existing
  in `rebuild_queue.rs`, none on changed files).
- `cargo clippy --workspace --all-targets` — clean (11 warnings,
  same pre-existing source).
- `cd frontend && npm run build` from the prior commit's lockfile
  produces the dist directories the new routers consume:
    dashboard: `dist/{index.html, static/{app.js, dashboard.css}}`
    agent:     `dist/{index.html, stats.html, screen.html,
                       static/{app.js, stats.js, agent.css}}`
  (favicon.svg lands in dashboard/ during the nix build —
  `nix/frontend.nix` install phase copies `branding/hyperhive.svg`
  there, since it's outside the npm tree.)

Refs #273.
2026-05-23 14:51:01 +02:00
iris
2ecf15bb6f frontend: nest asset output under dist/static/
The src/index.html / src/stats.html files reference assets at URLs
like /static/app.js, /static/dashboard.css. The initial Phase 1 build
flattened everything to dist/{app.js, dashboard.css, ...} which would
have forced the Phase 4 Rust ServeDir mount to do URL rewriting just
to make the existing HTML references resolve.

Rework: bundles now write to dist/static/, HTML stays at dist/ top
level. Layout matches the URLs the HTML uses, so the Phase 4 mount
is the simplest possible `fallback_service(ServeDir::new(dist))`.

No source-file changes — just the esbuild outfile/outdir paths.
Rebuilt; verified asset filenames + sizes unchanged.

Refs #273.
2026-05-23 14:51:01 +02:00
iris
892e034908 frontend: wire static-dir env var + per-agent extraFiles option
Phase 3 of #273. Container plumbing for the bundled frontend dist:

- flake.nix overlay: `pkgs.hyperhive-frontend` exposed for the
  agent / manager containers (mirrors the existing `pkgs.hyperhive`
  pattern); module argument `hyperhiveFrontend = system: self
  .packages.${system}.frontend` threads the package into the host
  hive-c0re module without forcing operators to apply the overlay
  on their host pkgs.

- `services.hive-c0re.frontend` option: pinned to the flake's
  frontend package by default, overridable for custom dashboard
  SPAs. The hive-c0re systemd service gets `HIVE_STATIC_DIR =
  ${cfg.frontend}/dashboard` — the Rust binary will pick it up
  in Phase 4.

- `hyperhive.frontend.dist` option: per-container, defaults to
  `pkgs.hyperhive-frontend`. Override to ship a fully custom
  agent SPA (advanced; the default + extraFiles flow handles the
  common 'add files' case).

- `hyperhive.frontend.extraFiles` option: attrsOf submodule
  (mirroring the `hyperhive.extraMcpServers` shape per damocles'
  request so existing #322-style assertions keep their grip).
  Each entry has `source` (path relative to agent.nix) and
  `target` (URL/disk prefix within the merged static tree,
  defaulting to the attribute name). Operator-named example:
  the bitburner agent drops `bitburner-dist` into
  `/bitburner/` alongside the default agent UI at `/`.

- `hyperhive.frontend.mergedDist` (readOnly): the runCommand
  derivation that composes `agent/` from the default dist plus
  every `extraFiles` entry. Aborts on overwrite so a filename
  collision becomes a build error rather than a silent dist swap.
  agent-base.nix + manager.nix set their respective systemd
  service `HIVE_STATIC_DIR` to this merged path.

Until Phase 4 lands, the env var is set but unused — the Rust
binaries still serve assets via `include_str!`. The cutover
happens in the next commit on this branch.

Refs #273.
2026-05-23 14:51:01 +02:00
iris
c8af7bc70c frontend: add hermetic nix derivation in nix/frontend.nix
Phase 2 of #273. Adds `packages.${system}.frontend` to the flake —
a `buildNpmPackage` derivation that consumes the lockfile committed
in the previous step and produces two static dist trees under $out:

  $out/dashboard/   the hive-c0re dashboard SPA assets
                     (index.html, app.js, dashboard.css, favicon.svg)
  $out/agent/       the per-agent default UI assets
                     (index.html, app.js, stats.html, stats.js,
                      agent.css, screen.html)

The dashboard favicon lives outside the frontend src tree
(branding/hyperhive.svg at the repo root). It's passed in as a
callPackage argument so the hermetic build can grab it.

`npmDepsHash` is set to `lib.fakeHash` — the build will fail on
first attempt with the actual sha256 printed; copy that in. Use
`nix run nixpkgs#prefetch-npm-deps -- frontend/package-lock.json`
to recompute locally without a build round-trip (works from
operator's host; iris's container can't recompute it without
prefetch-npm-deps in PATH).

The Rust crates and NixOS modules continue to use the legacy
include_str! routes; cutover happens in Phase 4.

Refs #273.
2026-05-23 14:51:01 +02:00
iris
9c7d4df08c frontend: lock npm dependencies via package-lock.json
Follow-up to 9e558c3. Runs `npm install` with the new nodejs_22 + npm
toolchain that just landed in iris's container (approval dfae406),
which generates the lockfile + node_modules tree. Only the lockfile
is checked in; node_modules/ stays in .gitignore.

Pinned versions (resolved by npm from the package.json constraints):
- chart.js 4.4.4   (replaces the jsDelivr CDN script on stats.html)
- marked 4.3.0     (replaces hive-fr0nt/assets/marked.umd.js)
- esbuild 0.25.5   (bumped from 0.24.0 to clear an audit warning
                    about the dev-server CSRF advisory; bundling
                    behaviour is unaffected)

Validated locally:
  npm install        — 0 vulnerabilities reported
  npm run build      — both workspace builds succeed
    dashboard: dist/{app.js (149kb), dashboard.css (33kb), index.html}
    agent:     dist/{app.js (114kb), stats.js (435kb), agent.css (16kb),
                     index.html, stats.html, screen.html}
  Stripped-comment diff of dist/dashboard.css vs the runtime concat
  (BASE_CSS + TERMINAL_CSS + assets/dashboard.css) shows only
  whitespace + comment-strip differences — selectors/properties match.

Hermetic-build wiring (the Nix `buildNpmPackage` derivation that
consumes this lockfile) lands in Phase 2 on a follow-up commit.

Refs #273.
2026-05-23 14:51:01 +02:00
iris
8bebd78895 frontend: add npm workspace scaffold under frontend/
Phase 1 of the backend/frontend code split (#273). Additive — no
existing code is touched; the legacy hive-c0re/assets, hive-ag3nt/
assets and hive-fr0nt/assets trees stay in place until the Rust
cutover later in this branch.

Layout:
  frontend/package.json                       npm workspaces root
  frontend/packages/shared/                   @hive/shared
    src/{base,terminal}.css + terminal.js     (ES module)
    src/index.js                              re-exports terminal.js
  frontend/packages/dashboard/                @hive/dashboard
    src/{index.html, app.js, dashboard.css}   ported from hive-c0re/assets
    build.mjs                                 esbuild config → dist/
  frontend/packages/agent/                    @hive/agent
    src/{index,stats,screen}.html + agent.css
        + {app,stats}.js                      ported from hive-ag3nt/assets
    build.mjs                                 esbuild config → dist/

Changes vs the existing assets:
- terminal.js is an ES module exporting { create, linkify } instead
  of assigning to window.HiveTerminal. The dashboard / agent app.js
  files re-expose them on window so the IIFE bodies keep working
  unchanged through Phase 1; the global aliases can be dropped in a
  follow-up once the IIFEs are unwrapped.
- marked is imported from the marked@4.3.0 npm package (replacing
  the vendored hive-fr0nt/assets/marked.umd.js bundle).
- chart.js is imported from chart.js@4.4.4 (replacing the jsDelivr
  CDN script tag on the per-agent stats page — page now works
  offline / on operator machines without internet egress).
- dashboard.css and agent.css both gain @import lines at the top
  that pull base.css + terminal.css from @hive/shared, replacing
  the runtime string concatenation in serve_css.
- index.html / stats.html collapse from three / two script tags to
  one type="module" tag pointing at the bundled output.

package-lock.json is intentionally omitted from this commit — npm
isn't available in the iris container yet (approval pending) and the
lockfile will land in the next commit on this branch once the
toolchain is in place. The PR will not be opened until it's there.

Phase 2 (nix derivations), Phase 3 (container plumbing + the
hyperhive.frontend.extraFiles option for per-agent layering), and
Phase 4 (Rust cutover to tower_http::ServeDir, delete hive-fr0nt
+ legacy assets dirs) land as follow-up commits on this same
branch.

Refs #273.
2026-05-23 14:51:01 +02:00
damocles
d81b430136 rebuild_queue: pre-enqueue meta-update cascade at submit time (closes #347) 2026-05-23 12:36:06 +02:00
lexis
4c425ede69 docs: document rebuild queue panel + module (follow-up to #340) 2026-05-23 12:27:03 +02:00
iris
a2ed35cd19 dashboard: pin agent icon at a fixed size
`.container-icon` had `align-self: stretch` + `aspect-ratio: 1`, so
the square's width tracked the body's height. As soon as state pills
(rate-limited / needs-login / needs-update / ctx) wrapped the head
row, the body grew taller and the icon grew with it — two cards
with different state ended up with visibly different-sized icons
(issue #344).

Pin the icon at 5em; height follows from aspect-ratio. Card-level
`align-items` drops to flex-start so a row taller than the icon
doesn't stretch the icon back out. The card body still flows
however many lines it needs.

Closes #344.
2026-05-23 12:12:49 +02:00