Commit graph hyperhive/frontend/packages/dashboard/src/dashboard.css
Author SHA1 Message Date
iris
0deca50352 dashboard: fold schedule creation into the table view (closes #564)
The N3W SCH3DUL3 vertical form is gone. The schedules table now
carries an always-visible inline create row at the bottom — fill the
cells, click + to POST /api/schedules, the new schedule appears
above on the next refresh.

Per-column inputs match the display semantics:
- `next` → `<input type="datetime-local">` (defaults to now+5min)
- `every` → 4 mini d/h/m/s number inputs (blank/all-zero = one-shot)
- `body` → `<textarea rows=1>` expanding to 4em on focus-within
  so multi-line prompts still fit (closes mara's "prompt may still be
  multi-line" requirement)
- per-agent columns → checkbox (whole cell clickable via padded label)
- actions → + submit + ⌫ reset (clears the carry without POSTing)

`newScheduleCarry` module-scope object survives the paintAtomic
re-render so mid-typed values stick across schedule-list refreshes
(same pattern as `scheduleEditCarry` from #474).

H2 collapses from "N3W SCH3DUL3" + "QU3U3D SCH3DUL3S" down to a
single "SCH3DUL3S" since the section now does both. Empty-state
branch on `renderSchedulesList` is gone — the table always renders
because the create row is always there.

`#schedule-new-section` div + `renderScheduleNewForm` /
`readScheduleFormCarry` / `submitNewSchedule` and the
`.schedule-new-form` CSS selector all removed. The edit-form path
(#474 `renderScheduleEditForm` colspan'd row on `✎` toggle) is
unchanged — "edit mode is inline in the table" already shipped
there, this PR just adds the same mental model for create.

Validations match the old form: empty targets / empty body /
invalid datetime / non-integer interval each surface an `alert()`.
The + button shows a spinner while the POST is in flight, then
clears the carry + triggers `refreshSchedules()`.
2026-05-29 19:57:14 +02:00
iris
036bf6c3c1 dashboard: cancel-X on queued rebuild_queue rows (closes #575)
Backend route POST /api/rebuild-queue/{id}/cancel already exists
(refuses Running / terminal entries with {cancelled: false}). This
adds the operator-facing affordance:

- small circular X button on the right edge of each row whose
  state === 'queued'. Running / done / failed rows don't render
  it, so the operator never clicks a button that the backend
  would refuse.
- uses the same data-async + data-confirm pattern as the
  reminder cancel form — global submit handler does POST +
  spinner + error toast for free.
- successful cancel flips the row queued -> cancelled via the
  live RebuildQueueChanged snapshot, so the button disappears
  on the next paint without an explicit refresh.

CSS keeps it quiet by default (muted border, transparent
background) and lights red on hover / focus, matching the
.btn-deny family without claiming a full button-width slot
that would push the row layout around.
2026-05-29 16:43:37 +02:00
iris
8bbb3e4905 tabs: logs flyout fills the side-panel height (closes #541)
The journald viewer opens in the side panel, but the <pre> only takes
the height of its content — so a short log fetch leaves the bottom 80%
of the panel empty, and a long one pushes the controls past the
viewport. Wrap the body in a column-flex container that fills the
side-panel-body, keep controls fixed at the top, and let the <pre>
flex-grow into the rest of the panel with its own overflow:

  .journal-body { display: flex; flex-direction: column; height: 100%; }
  .journal-controls { flex: 0 0 auto; }
  .journal-output { flex: 1 1 0; min-height: 0; overflow: auto; }

`min-height: 0` is the canonical "let me actually flex-shrink for
overflow" escape hatch on flex children. Also moved the auto-scroll
target from side-panel-body onto the <pre> itself — the panel-body no
longer overflows now that journal-body fills it; the <pre> is the new
scroll surface.

Pure CSS + one JS line; no DOM shape change.
2026-05-28 18:54:02 +02:00
iris
42a2503834 tabs: render schedules as a single table with tilted agent columns (closes #535)
Replaces the per-schedule card layout with one table:

  | # | src | next | every | owner | body | …agents… | actions |
  |---|-----|------|-------|-------|------|----------|---------|
  | 5 | op  | 5m   | 10m   | mara  | "…"  | ✓ ✓ . ✓ |  ↯ ✎ ✕  |

Each schedule is one <tr> in the tbody. Agent columns are dynamic —
operator + manager + live containers + any "extra" name that appears
as a target on some schedule but isn't a current container (same
membership rule buildTargetChips uses, so the table + new/edit forms
agree on what's addressable). Per-agent cells:

  - active target  → <button>✓</button> that cancels just that target
    on click (replaces the per-row ✕ from the old targets sub-table)
  - cancelled target → muted ✕ glyph (no button; re-add flows through
    the edit form's targets multi-select)
  - not a target → empty cell

Agent column headers tilt -45° via CSS so each column reads as 28px
of horizontal real estate instead of the full word width. Standard
rotated-header pattern: 95px-tall <th> with position:relative, inner
<div> positioned absolute at bottom-left, transform rotates about
left-bottom.

Body cell truncates with ellipsis + full-text title. Description used
to be a separate visible block on the card layout; the table folds
it into the body cell's title to keep row height tight. If mara wants
description visible in-table it's a small follow-up — easier to
iterate on a rejection.

Edit form expands inline into a colspan'd row underneath the schedule
row it edits (instead of inside the card). Wrapper drops the form's
background so it reads as a row extension.

No backend changes; everything renders from existing schedulesState
+ containersState.
2026-05-28 14:22:39 +02:00
damocles
c1f27e3b7b approvals: manager can withdraw pending approvals (closes #250) 2026-05-27 12:48:30 +02:00
iris
1dfcc8953c rqe-step: drop tabular-nums (no digits in step names)
argus 🟡 on #504 — `font-variant-numeric: tabular-nums` has no
effect on `.rqe-step` since step strings are text-only
("nixos-container update" / "meta prepare_deploy" / etc.),
no digit columns to align. Harmless but cleaner without it.
2026-05-27 02:00:42 +02:00
iris
50986b5a05 dashboard: render step sub-line on rebuild queue entries (closes #437)
Frontend half of #437 — paired with damocles's #501 backend which
adds `step: Option<String>` to `QueueEntry` and annotates 9
worker phases across ApplyCommit / Spawn / MetaUpdate / Rebuild
pipelines.

When `entry.step` is present, render a cyan `↳ <step>` sub-line
below the main row in `renderQueueEntry`. Wraps to its own flex
line via `flex-basis: 100%`, indented 1.8em to align under the
state glyph + kind. Terminal transitions clear `step` on the
backend, so the sub-line just disappears on Done / Failed rows —
no client-side staleness handling needed.

CSS: `.rqe-step` keys off cyan to visually group with the
running-spinner color cue (vs the muted `.rqe-when` /
`.rqe-reason` chips). `tabular-nums` keeps the indented arrow
column stable as step strings change length mid-build.

docs/web-ui.md updated to describe the new sub-line behaviour
in the R3BU1LD QU3U3 section.
2026-05-27 02:00:42 +02:00
iris
a2c5348ca4 dashboard: rename src/app.js → src/tabs.js (closes #406)
#406 was the JS-split refactor: pull shared helpers into
common.js (step 1), pull the flow-only IIFEs into flow.js
(step 2), then rename the legacy combined entry from app.js
to tabs.js (step 3 — this commit) to reflect that the bundle
now owns the dashboard tabs surface only.

What moved:
- `frontend/packages/dashboard/src/app.js` → `tabs.js`
- `build.mjs` entry: `src('app.js')` → `src('tabs.js')`;
  output is now `static/tabs.js`
- `index.html` `<script>` src: `/static/app.js` →
  `/static/tabs.js`; the FL0W-section comment retouched
- `flow.html` reference from `/static/app.js`-as-tabs to
  `/static/tabs.js`; notif + offscreen-inbox comments rewired
  to point at the correct owners (common.js for NOTIF,
  flow.js for renderInbox)
- `common.js`, `flow.js`, `tabs.js` headers: scrub stale
  "app.js" references; document that #406 steps 2 + 3 are
  done and both pages import directly from common.js
- `dashboard.css` comments: every "by app.js" → "by tabs.js"
- `docs/web-ui.md`, `docs/conventions.md`: legacy
  `assets/app.js` → `assets/tabs.js` (the path prefix is
  itself stale from a much-earlier rename, but consistent
  with the rest of those docs)
- `CLAUDE.md` file map: refresh the dashboard package's
  src/ and dist/ layouts to reflect the post-split shape
  (tabs.js, flow.js, common.js, stream-worker.js)

What did NOT move:
- `frontend/packages/agent/src/app.js` (per-agent UI entry)
  stays named `app.js` — it's a separate package, has only
  one entry point, no split happened there
- The two "no-op when target absent" guards in renderContainers
  and renderQuestions are kept as belt-and-suspenders for any
  future page that adds tabs.js without the corresponding
  sections; comments updated to note this rather than the
  pre-split flow.html reason

Functional behaviour identical; this is a pure rename + comment
sweep.
2026-05-26 19:39:27 +02:00
iris
501a86b725 flow: body to its own full-width line under the meta chips (closes #485)
mara: "the 'timestamp from -> to' part is long enough to warrant
its own line. then the actual messages can be rendered (nearly)
full width."

Confirmed in the layout — previously `.live .msgrow .msg-body`
sat inline with `flex: 1 1 0`, eating whatever the chips left.
With a 14:23:42 timestamp + agent names + arrows that was ~30ch
of prefix; long bodies wrapped awkwardly.

One-line CSS fix: `flex: 1 1 100%` on `.msg-body` forces it to
wrap to its own flex line in the existing `flex-wrap: wrap`
container. Metadata chips stay on the row above; body takes the
full width down to the row's content edge.

`min-width: 0` retained so `word-break: break-word` keeps
working. Reply rows keep their `padding-left: 1.2em` border-left
indent — the body lands within that frame, matching the visual
"this is a reply" nesting.

Out of scope but worth knowing:
- consecutive-message grouping (one header per agent run) was
  the second design option I floated; happy to land it as a
  follow-up if reading still feels chatty.
- timestamp-on-hover was the third; skipping unless asked.
2026-05-26 19:30:28 +02:00
iris
f35382e57c schedules fire-now: render FireNowReport inline on the button
damocles's #472 returns `{ ok, failed, missing, one_shot_consumed }`
from the fire-now endpoint. Parse the response and flash the
per-target outcome on the button itself for ~1.5s before
refreshSchedules() repaints — operator sees the result
immediately without a modal alert or an `/api/schedules`
re-fetch round-trip.

Button label transitions:
  ↯ fire now  →  ◐ firing…  →  ↯ fired: 3 ok, 1 missing — consumed
                                (green flash, then row refresh)
2026-05-26 17:06:14 +02:00
iris
7e5b496aa8 dashboard: fire-now button on schedule rows (closes #467)
Operator wants to trigger a scheduled prompt immediately
instead of waiting for the next interval. Adds a `↯ fire now`
button on every active schedule row, next to `✕ cancel all`.

Semantics (per design discussion with damocles):

- recurring schedules → out-of-band pulse, `next_fire_at_unix`
  untouched; the regular cadence keeps firing on the original
  schedule. Operator gets an extra fan-out, not a phase shift.
- one-shots → consumed after the manual fire. Operator's
  intent reads as "the scheduled time was wrong, send NOW";
  leaving the original time would be surprising.

Confirm dialog spells out the recurring-vs-one-shot
difference up front so the operator knows what they're about
to do. Button is disabled when every target is already
cancelled (nothing to fire).

Talks to `POST /api/schedules/{id}/fire-now` (damocles is
wiring the backend in parallel). Mauve styling distinguishes
it from cancel (red) and submit (amber); fits the existing
btn pattern.

docs/web-ui.md updated.
2026-05-26 17:06:14 +02:00
iris
dd92ef07fa dashboard: inline edit form on schedule rows (closes #474)
Per damocles's PATCH /api/schedules/{id} backend (#475), each
non-cancelled schedule row gets a `✎ edit` button that toggles
an inline form pre-filled with current values. Editable:

- body (textarea)
- description (blank to clear)
- next-fire (datetime-local)
- interval (shared composer from #466 — all-zero flips to one-shot)

Targets stay immutable per the design call with damocles:
per-target last-result history is keyed on them; cancel + new
schedule is the documented retarget workaround. The form
surfaces the active target list as a read-only note explaining
this.

Submit semantics — the form computes a PATCH diff against the
original schedule and only includes keys for fields that
actually changed. Blank description → `null` (clear), all-zero
interval on a recurring schedule → `null` (flip to one-shot).
No-op submit (no fields changed) just closes the edit form.

Refactored the interval composer (#466) into a shared
`buildIntervalComposer({ label, namePrefix, initialSeconds })`
helper so the new-schedule and edit-schedule forms use the same
chip/d/h/m/s/preview widget. New-schedule form behavior
unchanged; edit-form input names are prefixed `edit_interval_`
to avoid FormData collisions when both happen to mount.

State survives state-poll re-renders via `editingSchedules`
(Set of ids being edited) + `scheduleEditCarry` (per-id
mid-edit values) — same pattern the new-schedule form uses
with `readScheduleFormCarry`.
2026-05-26 15:58:28 +02:00
iris
caa0c6ce4c dashboard: friendlier interval input for scheduled prompts (closes #466)
Replaces the single raw-seconds field in the SCH3DUL3D PR0MPTS
creation form with a structured composer:

- Preset chip row (1m / 5m / 15m / 30m / 1h / 6h / 12h / 1d / 7d
  / one-shot) that fills the parts inputs in one click
- Four small d/h/m/s number inputs combined into total seconds
  on submit (all-zero = one-shot, preserving the backend null
  semantic)
- Live "↻ every …" preview using the existing fmtDuration helper
  so the operator sees what they're about to queue

Carry semantics extended to round-trip the split fields across
state-poll re-renders; legacy `interval` carry still honoured if
an older bundle is in the page.

Docs (web-ui.md) updated to describe the new composer.
2026-05-26 15:55:20 +02:00
iris
c233f8a924 dashboard: scheduled prompts tab + creation form (closes #459)
frontend for the #444 scheduled-prompts feature. backend is
already merged (PR #454 + sibling commits): GET /api/schedules
(snapshot), POST /api/schedules (operator-direct submit), POST
/api/schedules/{id}/cancel (whole or per-target).

new chrome

- SCH3DUL3S tab in the dashboard tab strip, between SYST3M and
  the FL0W link. count pill shows the number of schedules with
  at least one still-active target.
- pane has two sections: N3W SCH3DUL3 (creation form) +
  QU3U3D SCH3DUL3S (list of cards).

creation form

- targets multi-select rendered as chip-style checkboxes;
  candidates pulled from containersState plus the special
  `operator` and `manager` recipients.
- prompt body textarea (required, non-empty trim).
- first-fire datetime-local input, defaulted to "5 minutes from
  now" so the form has a sensible pre-filled future timestamp.
- optional interval (seconds) input — blank = one-shot.
- optional description (one-liner shown on the schedule card).
- mid-typing carry: re-rendering the form preserves field values
  + checkbox state. the operator never loses what they were
  typing when the schedule list refreshes underneath them.
- POSTs SchedulePromptPayload JSON to /api/schedules; on success
  re-fetches the list to surface the new row.

schedules list

- one card per schedule, active rows first (sorted by
  next_fire_at_unix), cancelled tail dimmed.
- header: id, source chip (`operator` or `approval` — reuses
  the rebuild-queue rqe-source styling for visual consistency),
  next-fire countdown / overdue label, recurring vs one-shot
  badge, owner.
- body: prompt text in a styled <pre>-ish block with linkified
  path references.
- per-target table: target name, last fire age, last result,
  per-row cancel button.
- whole-schedule "✕ cancel all" button.
- per-target cancel posts { targets: ["name"] }; cancel-all
  posts no body (== cancel whole row).

no-SSE refresh

- the backend doesn't emit SchedulesChanged dashboard events
  yet (damocles flagged this as a follow-up PR C). list
  re-fetches on:
  - tab activation (so switching to SCH3DUL3S never lands stale)
  - cold load via refreshState
  - after every submit + cancel POST
  the operator's typical interactions all force a refresh; the
  remaining gap (worker fires while you're staring at the tab)
  is the natural argument for PR C.

files

- frontend/packages/dashboard/src/index.html — new tab + pane
  with the two sections.
- frontend/packages/dashboard/src/app.js — schedulesState
  cache, refreshSchedules, render*ScheduleNewForm, submit +
  cancel helpers, tab routing extended for `schedules`, count
  pill wired into refreshTabCounts.
- frontend/packages/dashboard/src/dashboard.css — schedule
  form + card styling + chip checkboxes + targets table. small
  .btn-inline-small helper for the per-target cancel.

validation

- npm run build --workspace=@hive/dashboard clean. app.js
  158 kb → 161 kb. CSS 41.3 kb → 43.9 kb.
- browser smoke test isn't possible from inside iris's container;
  endpoints are wire-compatible (backend types unchanged) and
  the form serialisation matches SchedulePromptPayload's JSON
  shape exactly (targets / body / first_fire_at_unix /
  interval_seconds / description).
2026-05-26 11:45:57 +02:00
iris
002419ba4d dashboard: drop stale data-spw note in side-panel-drawer comment (#451)
argus nit on #456: the CSS comment claimed "The body adds
data-spw=<px> once persisted" — that's leftover from a prior
design and no such attribute is ever set. The actual flow is
localStorage → JS reads on Panel.bind → sets --side-panel-w via
inline style. Replaced the misleading line with an accurate
description of the persistence path.
2026-05-26 01:27:42 +02:00
iris
f4635cc256 dashboard: full-height inbox + drag-to-resize side panel (#450 #451)
two related flyout improvements — both small, single PR.

#450 — full-height inbox

dropped the .inbox max-height: 24em cap. the side-panel-body
already provides overflow: auto, so the cap was just clamping the
inbox list short of the available panel height on tall viewports.
the inbox now fills as much of the panel as it needs and the
panel itself scrolls.

#451 — drag-to-resize side panel

added a 6px hit-strip on the drawer's left edge. mousedown/move/
up handlers track the drag and update --side-panel-w on the
drawer; the CSS variable defaults to min(760px, 94vw) (preserving
pre-#451 behaviour) and is clamped to [320px, 96vw] so a bad
stored value can never wedge the drawer off-screen.

ergonomics

- handle is invisible at rest, mauve glow on hover + during drag
  so the affordance is discoverable but doesn't compete with the
  2px mauve border-left for the visual boundary.
- body.side-panel-resizing class forces ew-resize cursor + kills
  user-select page-wide so the cursor doesn't flip back to
  default the moment it leaves the 6px band during a fast drag.
- final width persists to localStorage (key
  hyperhive:side-panel-width) so it survives reload. window
  resize re-clamps so a stored width that exceeds the new 96vw
  shrinks accordingly.
- handle is a separator role with aria-orientation: vertical +
  aria-label for screen readers.

files

- frontend/packages/dashboard/src/dashboard.css
  - .inbox: dropped max-height (#450).
  - .side-panel-drawer: width = var(--side-panel-w, min(760px,
    94vw)) + min/max clamp (#451).
  - new .side-panel-resize + body.side-panel-resizing rules.
- frontend/packages/dashboard/src/common.js
  - Panel.bind now also calls applyStoredWidth + bindResize.
  - resize handle is prepended to the drawer at bind time so
    every flyout (inbox, file preview, diff, journal) gets it.

validation

- npm run build --workspace=@hive/dashboard clean. CSS 40.9kb →
  41.3kb. app.js + flow.js grew ~0.5kb each (resize handler).
- browser smoke test isn't possible from inside iris's container;
  the resize math is straightforward (width = innerWidth -
  clientX, clamped) and the CSS variable + localStorage
  persistence are standard patterns.
2026-05-26 01:27:42 +02:00
iris
a366c00e0e dashboard: agent selection + bulk action bar (#443)
per mara on #443: "dont show all the restart buttons etc., just
show state and links. instead, clicking an agent icon selects that
agent. you can select as many as you like. then you can run an
action on all of them."

selection model

- module-level selectionState = Set<string> of agent logical
  names. clicking a container-row icon toggles membership; Esc
  clears the whole set (ignored when an editable element has
  focus so typing in compose / answer / journal-search isn't
  intercepted).
- icon now has role="button" + tabindex="0" so it's
  keyboard-accessible; aria-pressed reflects the toggle state.
  hover + focus-visible get a mauve ring.
- selected rows get a .selected class — mauve outline +
  faint mauve wash on the row, brighter ring on the icon.
- on every renderContainers pass, stale selections (agents
  destroyed while selected) are pruned defensively.

sticky action bar

- new #selection-bar in index.html — fixed bottom strip with
  count chip, name list, action buttons, clear button. hidden
  when selection empty. styled to match the flow composer's
  frosted-mauve chrome (vibecore family).
- per mara's option B answer: every action button is visible;
  buttons that don't apply to the whole selection are disabled
  with a tooltip explaining WHY ("iris is already stopped" etc).
  .btn:disabled styling added.
- actions: R3ST4RT / ST0P / ST4RT / R3BU1LD / DESTR0Y / PURG3.
  per-action confirm prompt lists the names being acted on.
  when the selection includes the manager the ST0P prompt calls
  out the consequence (approvals + meta operations pause until
  manager is restarted). DESTR0Y/PURG3 stay sub-agent-only;
  including the manager disables them with a clear tooltip.
- actions POST per agent in a loop to the existing endpoints
  (/restart/{name}, /kill/{name}, etc.); no new backend wire
  surface. event-driven derived stores (containersState,
  rebuildQueueState) already update live via SSE — no manual
  refetch.
- failures from any individual POST surface in a single alert
  at the end rather than spamming dialogs mid-loop.

per-card actions removed

- the in-card R3ST4RT / ST0P / ST4RT / R3BU1LD / DESTR0Y / PURG3
  block is gone. cards now show identity / state / nav-strip /
  status text / drill-ins only — "state and links" per mara.
  the contextual needs-update chip in the head row stays.

manager stop

- the "also make manager stoppable" half of #443 ships as PR
  #445 (separate small backend change). this PR depends on #445
  for the bulk-stop button to actually work on selections
  containing the manager; until #445 merges, ST0P on a
  manager-included selection will fail individually with a 500
  for the manager (other selected agents are still stopped;
  failures bucket into the end-of-loop alert).

build clean: npm run build --workspace=@hive/dashboard. no
backend changes here.
2026-05-26 00:41:02 +02:00
damocles
b42b219f9b dashboard: green chip for approval source (iris nit on #436) 2026-05-26 00:29:42 +02:00
iris
c015f62764 dashboard: drop max-width cap, full-width layout (#416)
Mara: remove the 90em max-width so the dashboard fills wide
screens instead of boxing content into a centred column with
empty margins. `padding: 0 1.5em 1.5em` stays as the small
edge-of-viewport gutter; `.dashboard-chrome { margin: 0 -1.5em
... }` continues to pull the sticky chrome bar edge-to-edge
through that gutter.
2026-05-25 20:59:25 +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
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
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
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
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