The per-agent terminal marks turn boundaries (◆ TURN ← / ✓ turn) but
shows no time. Append a wall-clock HH:MM:SS to both the turn-start and
turn-end rows, plus the elapsed duration on turn-end, rendered as dim
metadata so the boundary glyph stays the focus.
This is the frontend half of the feature; it reads a per-event `ts`
(unix seconds) off the turn_start / turn_end events. The read is guarded
on a numeric `ts`, so until the harness surfaces per-event timestamps
the rows render exactly as before (inert-until-capture). Once the
backend attaches `ts` to the history rows + live SSE frame, the times
light up automatically for both live and scrollback, with no further
frontend change.
H0M3 had tiles for every operator surface except the forge. Add a
Forge tile that links straight to the hive-forge web UI. It mirrors
the Matrix tile's gating: hidden by default, revealed by home.js only
when state.forge_present is true, with the href filled from
state.forge_public_url (the gateway-served vhost) or the direct :3000
port fallback — the same precedence the dashboard uses for forge links.
Operators without a forge never see a dead link.
Adds a reasoning-effort quick-picker to the agent page's overflow menu,
a direct sibling of the existing model quick-picker, wired to the backend
seam from #1597/#1600: /api/state carries effort + available_efforts, the
picker POSTs { effort } to /api/effort (operator-only, mirroring
/api/model), and live updates arrive via the effort_changed SSE event.
The available levels are the backend's to declare — the frontend holds no
hard-coded list; availableEfforts is seeded from state.available_efforts
on cold-load and the picker section is omitted until the backend supplies
the set. Mirrors the model picker otherwise: postEffort(),
renderEffortChip() (updates the new amber effort-chip + button active
states), the cold-load seed, and the effort_changed handler. Build green.
Applies on the next claude session (operator uses the existing
clear-session), per the backend contract.
Per mara's review: instead of measuring the banner height in JS and
offsetting the chrome's sticky top, put the warning banner and the page
chrome in the same sticky div so they stack naturally.
common.js builds the wrapper: ensureStickyTop() wraps the page's chrome
(.dashboard-chrome / .page-header) in a single .sticky-top container and
injects the warning banner as its first child. The banner and the chrome
are no longer individually sticky — .sticky-top owns the stickiness, so
they pin together in one context instead of two top:0 stickies colliding
(the banner used to overlay the tab bar). Pages without a chrome (the
H0M3 hub) get a banner-only sticky region. No per-page markup needed; no
JS height measurement. Build green.
The schedules table builds a column per target, including agents that
have since been destroyed (their past schedules linger). Those columns
showed identically to live agents, so the operator couldn't tell a
schedule still targets something that no longer exists.
Flag them: renderSchedulesTableHead now checks each agent column against
the live roster (containersState) and, for any that isn't there (and
isn't 'operator', which is always valid), greys + strikes through the
column label and titles it 'no longer exists (gone)'. The rows stay
visible + cancellable — the operator can see the stale targets and clear
them rather than be surprised by phantom columns. Build green.
Largest step of the tabs.js split: the SCH3DUL3S tab — reminders +
scheduled-prompts (the lists, inline create row, per-row edit form, and
fire-now / cancel actions) — moves into a new schedules.js, ~1060 lines
out of tabs.js. It owns its own module state (schedules list +
edit-in-progress tracking) and reads the shared agent roster from
state.js for the target-chip pickers; render + format helpers come from
util.js, so the keystones (state.js, util.js) paid off here.
Exports the four entry-wired functions — applySchedulesChanged /
applyRemindersChanged (mutation dispatch), refreshSchedules /
refreshReminders (tab activation + cold-load) — plus activeScheduleCount
(the count pill). The 19 other functions stay module-private. Also drops
the now-unused paintAtomic import from tabs.js (its only callers were in
this block).
Behaviour-preserving code motion; esbuild inlines schedules.js into the
tabs.js bundle. Build green; tabs.js drops ~1060 lines (4323 → 2969).
Each home-hub tile now leads with an icon next to its label: 🖥 Dashboard,
📡 Flow, 📜 Logs, 📊 Stats, ⚙ Settings, 💬 Matrix. Matches the existing
emoji nav-icon convention (the per-agent link strip uses 📊/🖥/⬡/↳).
Markup: the label is wrapped with the icon in a .home-tile-head flex row
(icon + label on one line, description below). The icon is aria-hidden —
decorative, the label carries the accessible name. CSS-only layout
addition; no behaviour change. Build green.
The tabs.js split's render-heavy domains (schedules, system, containers,
questions/approvals) all share a handful of pure helpers that lived in
the tabs.js IIFE: paintAtomic (atomic-swap render) and the fmtAgo /
fmtElapsed / fmtDuration / truncate formatters (used 10/15/4/6/6 times
across the file). Pull them into a new dashboard-internal util.js so the
upcoming per-tab modules can import them instead of depending on the
entry's closure — the helper analogue of the state.js roster keystone.
They stay out of the cross-page common.js (their phrasing is
dashboard-specific) but are now a shared dashboard module. All five are
pure, so this is behaviour-preserving code motion; esbuild inlines util.js
into the tabs.js bundle. Build green.
The tracker-tag lint's trailing class `[^0-9a-fA-F]` treated a non-hex
letter as a valid terminator, so it flagged hash-route fragments like
#24h / #1h (the stats-page window-selector routes) as tracker tags — four
false positives in the frontend. A real tracker tag is never immediately
followed by a letter, so widen the trailing class to [^0-9a-zA-Z]: still
catches every real tag (followed by space / punctuation / EOL) while
excluding any digit-run glued to a letter. Letter-bearing and 6-8-digit
hex stay excluded as before.
Also write theme.css's --crust fallback in six-digit form (#000 -> #000000)
to dodge the one remaining pure-numeric-short-hex residual the lint
documents. Pixel-identical colour. With both, the frontend tree
contributes zero lint hits; only the legacy backlog (Rust) remains.
Pattern + cases reviewed and approved by atlas on the issue.
.dashboard-chrome was a plain block, so the inline-block home back-link
and the block-level .tabbar stacked on two separate rows. Make the chrome
a flex row (align-items: center) so the '← home' link and the tab strip
sit side by side. The chrome now owns the horizontal gutter and the
single full-width bottom divider; .dash-home-back and .tabbar drop their
own side padding / border accordingly. CSS-only — the markup already had
both as siblings of the chrome header.
Second module of the tabs.js split (after the roster keystone). The
P3RM1SS10NS tab — the per-agent capabilities + tool-groups matrices — is
the cleanest leaf: six contiguous functions with no module-scoped state
(each render builds fresh from the fetched payload) and no cross-domain
references except the agent roster, which it now imports from state.js.
Moves applyCapabilitiesChanged / applyToolGroupsChanged (the live-update
handlers wired into the entry's mutation dispatch table) and
fetchAndRenderCapabilities / fetchAndRenderToolGroups (called on tab
activation) into a new permissions.js, exporting those four; the two
renderers stay module-private. tabs.js imports the four — the dispatch
table and tab-activation call-sites resolve unchanged.
Behaviour-preserving: pure code motion. esbuild inlines permissions.js
into the tabs.js bundle, so the static output is unchanged. Build green;
tabs.js drops 258 lines.
The server-warning rows composited their level tint (amber/red) over
`transparent`, so only 16-18% of the colour was opaque. Because the bar
is `position: sticky; top: 0`, page content scrolled up behind it showed
through the see-through background. Composite the tint over the elevated
surface grey (--bg-elev) instead, so the bar reads as a solid grey strip
with the level tint on top — no bleed-through. Affects every page that
shows the banner (the flow page is where it was spotted).
First step of splitting the 4.3k-line tabs.js monolith into per-tab
modules. The live agent roster (containersState) is the one piece of
genuinely cross-domain state — read by the container tree, the
capabilities + tool-group matrices, the schedule target-chips, the
container-load poll, the selection bar and the operator inbox. Move it
(and its snapshot-sync) into a new dashboard/state.js that each domain
module imports, so the upcoming per-tab extractions have a single source
of truth to depend on instead of a shared IIFE closure.
Minimal by design — only the roster moves; the container row cache and
the apply-handlers (which also re-render) stay with the container domain.
Behaviour-preserving: the imported Map is mutated in place exactly as
before. esbuild inlines state.js into the existing tabs.js bundle, so the
static output is unchanged. Build green.
The dashboard tabbar was the styling outlier: a folder-tab metaphor
(top-rounded corners, transparent side borders overlapping the strip
border, and a box-shadow lift on the active tab) while every standalone
page's sub-tabs use the flat shared .hive-tab pill. Restyle .tabbar .tab
to the same pill family — drop the lift shadow, folder border and
top-only radius; active tab now uses the filled var(--border) background
like .hive-tab--active.
Kept a touch more weight than the bare sub-tab (larger 0.92em font, bold
labels, count pills) since this is the dashboard's primary nav, per the
operator's 'less fancy but not too minimal' steer. CSS-only, no markup
or behaviour change.
The standalone pages (/flow, /logs, /stats, /settings) all share the same
sticky back-link header, but its CSS lived in the dashboard's common.css
under the misnomer `.logs-*` (the comment even noted it was used by more
than logs). Promote it to a first-class shared component.
- new @hive/shared/chrome.css with neutral `.page-header` / `.page-back` /
`.page-title` (cohering with the `.page-content` gutter wrapper);
exported from the shared package and @imported into common.css next to
base/terminal/tabs.css.
- rename the four pages' header markup `.logs-*` → `.page-*`.
- drop the old `.logs-*` chrome block from common.css; refresh the stale
doc comments in settings.css / stats.css / dashboard.css.
CSS-only, behaviour/visual-neutral (same rules, new names + home). The
dashboard keeps its own richer sticky `.dashboard-chrome`; unifying the
per-agent UI's `.stats-nav` chrome is a larger follow-up, deferred.
Part of #1464 step 3.
Follow-up from #1532. Previously `body.dashboard-shell` / `body.home-shell`
carried the 1.5em horizontal gutter, and full-width chrome broke out of it
with negative margins (`.dashboard-chrome { margin: 0 -1.5em }`, the
`#server-warnings { margin: 0 -1.5em }` override).
Invert it: `<body>` is now full-bleed, and the padded page content lives in
a single inner `.page-content` wrapper that carries the gutter. The
server-warnings banner, the sticky tab strip, and the footer then span the
full width for free — so the `-1.5em` breakout hacks are gone.
- `.page-content { padding: 0 1.5em }` is a shared primitive in common.css
(the dashboard + H0M3 both opt in by wrapping their content; FL0W / L0GS /
ST4TS / S3TT1NGS stay full-bleed with their own `.<page>-main` padding).
- dashboard.html / index.html wrap their content in `.page-content`.
- dashboard.css / home.css: drop the body horizontal gutter (keep
`padding-bottom` for foot breathing room); `.dashboard-chrome` margin
`0 -1.5em 1em` → `0 0 1em`.
- common.css: drop the `#server-warnings` breakout override.
Behaviour/visual-neutral by intent (content gutter unchanged; chrome +
banner already rendered full-width via the old breakout). Worth a gui
screenshot-diff to confirm — esp. the now-full-width footer divider.
Last fold-in of #1464 step 2.
Extracts the hive-wide turn-stats rollup out of the dashboard tab strip
into a standalone /stats.html page, reached from the H0M3 hub — same
minimal-chrome pattern as /flow.html and /logs.html. The dashboard tab
strip is now purely operational.
- new stats.{html,css,js}; stats.js holds the moved render JS and
fetches /api/stats-hive on load + window change.
- migrate the window selector (#hive-stats-windows) from a bespoke
data-w/.active toggle to the shared createTabStrip — now hash-routed
(#1h / #24h / …) and deep-linkable, matching the per-agent /stats page.
- drop the ST4TS tab + pane from dashboard.html and the hive-stats render
block + the stats->refreshHiveStats lazy-load from tabs.js.
- move the shared .hive-stats-table to common.css (the dashboard SYST3M >
C0NT41N3R L04D table still uses it); the ST4TS-only window/chip/bar
styles go to stats.css.
- add a Stats tile to the H0M3 hub (and drop the now-stale "stats" from
the Dashboard tile desc); wire build.mjs + the nix/frontend.nix manifest.
Second slice of #1464 step 2; follows the /settings.html extraction.
Extracts the operator-local preferences (browser-notification toggle)
out of the dashboard tab strip into a standalone /settings.html page,
reached from the H0M3 hub — same minimal-chrome pattern as /flow.html
and /logs.html. The dashboard tab strip is now purely operational.
- new settings.{html,css,js}; settings.js binds NOTIF + renders the
shared server-warnings banner (matching the other standalone pages).
- drop the S3TT1NGS tab + pane from dashboard.html; the NOTIF.bind()
toggle wiring moves to settings.js. The dashboard keeps NOTIF.show()
(approvals / questions) — it reads the same browser permission +
localStorage mute flag the settings page sets, so firing still works.
- move .notif-row / .btn-notif from dashboard.css to settings.css.
- add a Settings tile to the H0M3 hub; wire the new entries into
build.mjs and update the nix/frontend.nix asset manifest comment.
First slice of #1464 step 2; the ST4TS page extraction follows separately.
The dashboard's tab strip now runs on the shared createTabStrip
(@hive/shared/tabs.js) — the third and final consumer of #1464 step 1
(after logs + the agent window selector).
- activateTab is reduced to the per-tab side-effects only
(body.dataset.activeTab, the selection-bar gate, the lazy-loads);
createTabStrip owns the active tab/pane toggle + aria-selected + hash
routing, calling activateTab via onShow. Panes switch via the `hidden`
attribute (was a `.tab-pane-active` class) — the markup carries `hidden`
so there's no flash before the script runs.
- the responsive overflow menu is DROPPED per the design call (move panes
to their own pages rather than hide them in a dropdown). Removes ~150
lines: updateTabbarOverflow, closeOverflowMenu, the click/keydown
handlers, the ResizeObserver + MutationObserver, and syncTabFromHash.
S3TT1NGS is a regular strip tab now (no data-overflow).
- CSS: .tab.active becomes .tab.hive-tab--active; pane visibility is now
.tab-pane[hidden] { display: none }; the overflow dropdown styles are
gone. Count pills (setTabCount/refreshTabCounts) are unchanged.
Behaviour-preserving except the deliberate overflow removal; aria-selected
is now standardised on the tabs.
Replaces the agent stats page's bespoke window-tab click handler with the
shared createTabStrip (@hive/shared/tabs.js). The selector is now
hash-routed — #1h / #24h / … become deep-linkable and survive
back/forward navigation.
- window buttons use data-tab (was data-w); the strip resolves them by
that convention and toggles .hive-tab--active + aria-selected.
- onShow(w) sets currentWindow + reloads stats; the strip's initial show
fires it once, so the explicit loadStats() in DOMContentLoaded is
dropped (avoids a double fetch).
- the selector keeps its distinct pill look — agent.css now targets
.window-tabs button.hive-tab--active (was .active), not the shared
.hive-tab text-tab base. It's a pure data-selector with no panes;
createTabStrip skips the absent [data-tab-pane] gracefully.
Behaviour-preserving; aria-selected is now standardised on the buttons.
Removes the standalone P33RS tab + pane. Peer hives now render as a
headline section at the bottom of the SW4RM pane; the swarm/hive name
moves off the cramped strip above the tabbar into a headline at the top
of SW4RM. Both are quiet when nothing's configured:
- swarm/hive identity → `#swarm-identity` h2 in the SW4RM pane (hidden
until hive_name/swarm_name are set), replacing the `#hive-identity`
banner-thin line above the tabbar.
- peer hives → `#peers-block` headline + `#peers-section` cards in the
SW4RM pane. When no peers are federated the headline block is hidden
entirely and a grey "no peer hives configured" note shows in its place.
- drops `peers` from the tab list + the per-refresh peers-tab gating;
renderPeerHives now toggles its own headline.
No backend change — still reads `state.peer_hives` + `hive_name` /
`swarm_name`. Build green.
Adds @hive/shared/tabs.js — createTabStrip(tabbar, {defaultId, onShow}):
a hash-routed tab strip that resolves controls by `[data-tab="<id>"]`
inside the passed container and panels by `[data-tab-pane="<id>"]`,
toggling `.hive-tab--active` + aria-selected + the panel's `hidden`, and
firing onShow(id) for per-tab side-effects. Always hash-routed (deep-
linkable + back/forward), no element-resolver callbacks (convention over
config), per the #1464 design review.
Wires it up: @hive/shared exports `./tabs.js` + `./tabs.css`, and
common.css @imports tabs.css (component structure, not a swap target, so
inlining is fine — unlike theme.css).
Migrates the logs sub-tabs as the first consumer: markup uses
`.hive-tab`/`data-tab`/`data-tab-pane`, logs.css drops the duplicated
base styles (keeps only its `flex:1` layout delta), and logs.js swaps its
activeTab/showTab/hashchange for createTabStrip (onShow lazy-loads the
SYSTEM tab). Behaviour-preserving. aria-selected is now standardised.
First piece of the generic tabs component: @hive/shared/tabs.css carries
the base .hive-tabbar / .hive-tab / .hive-tab--active styles (lifted from
the logs sub-tab pattern, the most generic of the three tab UIs). Colours
use the semantic theme vars so it inherits theme-swap safety.
Not yet consumed — the createTabStrip JS helper + the per-call-site
migrations (logs, agent window-tabs, dashboard tabbar) follow. Count
pills + the dashboard's responsive overflow menu will layer on top in
the per-page CSS rather than the shared base.
The banner is prepended as a direct child of <body>, so on pages that
pad the body gutter (the dashboard + H0M3 use a 1.5em horizontal gutter)
it inherited 1.5em gaps left and right. Break out of that padding with
negative margins matching the gutter — the same trick .dashboard-chrome
uses — scoped to the dashboard + home shells. FL0W + L0GS are full-bleed
already (no body padding), so they're unchanged.
Split the palette into two standalone stylesheets:
- colors.css — the 16 base16 slots (--base00..--base0F). This is the
entire theme swap contract; a generator (e.g. a stylix base16 scheme,
which is natively base00-base0F) replaces only this file.
- theme.css — the semantic layer (--bg, --purple, …) derived from the
base16 slots via var()/color-mix. Never changes on a swap.
Every page links colors.css then theme.css; theme.css does NOT @import
colors.css (that would re-bake the slots into it) — they're separate
dist outputs so a swap touches just colors.css. Pixel-identical refactor:
base16 defaults are Catppuccin Mocha and the three off-slot vars
(--crust, --muted, --subtext0) derive via color-mix reproducing their
exact prior hexes.
Wires colors.css through both build.mjs CSS entry lists, the
@hive/shared exports map, and all 7 page templates. css-vars.md + the
frontend.nix output-list comment updated.
Replace ~92 hardcoded Catppuccin rgba literals across the dashboard,
agent, and shared terminal stylesheets with color-mix() over the theme
:root vars, so a stylix-generated theme.css recolours the whole UI, not
just the elements that already referenced the vars. Pixel-identical
under the default palette (same RGB + alpha; color-mix(in srgb, C N%,
transparent) == rgba(C, N/100)).
Also fixes four var(--mauve, #cba6f7) usages: --mauve is undefined (the
var is --purple), so they were silently falling back to the hardcoded
hex and would not have recoloured on a theme swap. The bare crust hex in
terminal.css and the checkerboard-gradient hexes in common.css are
var-derived too.
Pure black/white drop-shadow/frost scrims are left as literals (not
theme colours). Two off-palette surface tints (dim-purple schedule rows,
warm-amber inline-button hover) are also left, with a comment, pending a
dedicated named var. Correct-var fallbacks like var(--red, #f38ba8) are
left as-is since they already recolour via the var.
Per operator request: instead of a disk-specific alert, surface a generic
server-warnings banner at the very top of every page, so new system
warnings can be added backend-side with no frontend change.
- hive-c0re `host_stats`: `server_warnings() -> Vec<ServerWarning>`
(`{ kind, level, message }`). The threshold logic lives server-side; the
host disk-pressure check (a `statvfs` probe of `/nix`: ≥85% used → warn,
≥95% → crit) is the first and only producer today. No new deps (libc).
- `/api/state` carries `server_warnings` (replaces the disk-specific
field). Empty when all clear.
- frontend: `renderServerWarnings` / `initServerWarnings` in `common.js`
inject a sticky top-of-<body> banner and render the list, coloured by
`level`. Wired on every page — dashboard (live, via refreshState),
FL0W, L0GS, H0M3. No per-warning frontend code; adding a warning kind
is a pure backend change.
cargo check/clippy/fmt + npm run build green. Closes#1518.
Swarm-level companion to the per-agent favorite-tools doughnut (#1433).
Aggregates each agent's bash_commands(ts, head) table (written by
hive-bash-mcp) across the whole hive and surfaces the top-10 most-run
command heads on the dashboard ST4TS tab, alongside the existing model
mix.
- hive_stats.rs: AgentAgg gains a `bash` head→count map, filled by a new
guarded `read_bash_heads()` that reuses read_agent's read-only
connection. A missing `bash_commands` table (capture hasn't run for
that agent) or any read error yields an empty map — isolated from
read_agent's error path so it never drops an agent from the rollup.
HiveStats gains `bash_mix: Vec<KeyCount>` (busiest-first, top 10).
Unit tests cover the per-head tally + window cutoff and the
missing-table degrade-to-empty path (in-memory sqlite).
- dashboard: a "favorite tools (bash commands across the swarm)" CSS-bar
list on the ST4TS pane, mirroring the model-mix bars. Header + list
stay hidden until bash_mix has data, so a fresh hive shows no empty
block. (Dashboard ships no chart lib — bars, not a doughnut.)
- docs: dashboard.md ST4TS section documents the new rollup.
Closes#1449. Inert until the hive-bash-mcp capture (#1448, merged) has
recorded data across agents.
Two small follow-ups after the modelPrices PR (#1442):
- nix/modules/hive-c0re.nix: the `modelPrices` option `example` still
showed the old 5-minute sonnet `cache_write = 3.75`; the default is now
the 1-hour TTL `6.0`. Bump the example to match so it doesn't mislead.
- frontend/packages/agent/agent.css: remove the dead `.stats-empty-note`
selector (no element uses it — the stats empty state paints on canvas).
Follow-up to the route swap (#1501): now that every separate page is
reachable from the H0M3 hub, the dashboard tab strip should hold only
real in-page tabs, not the "→" links out to other pages (mara: "remove
links to the pages from dashboard - only actual tabs stay here").
- dashboard.html: remove the `tab-matrix`, `tab-flow`, and `tab-logs`
`.tab-link` entries (and the stale `tab-count-flow` pill slot — the
operator inbox moved to Y3R C4LL). The dashboard strip is now SW4RM /
Y3R C4LL / SYST3M / P3RM1SS10NS / SCH3DUL3S / P33RS / ST4TS / S3TT1NGS.
- tabs.js: drop the now-dead `tab-matrix` matrix_gui_enabled gate (the
H0M3 Matrix tile is gated by home.js instead) and the obsolete
`#tab-count-flow` comment. Kept the contextual deep-links into the log
viewer (an agent's logs, a build entry's log) — those are functional
content links, not navigation chrome.
- flow.css: remove the dead `.dashboard-chrome.flow-chrome` +
`.tabbar .tab.active.tab-link` rules — the flow page uses a slim
back-link header, not the dashboard tabbar, and `.tab-link` no longer
exists anywhere.
npm run build green (flow.css 1.8kb). No residual references.
Step 2 of the nav restructure: make the H0M3 menu hub the landing page
at / and move the dashboard SPA to /dashboard.html, with every surface
linking back to the hub.
Mechanism (frontend-only, no host-side change — confirmed against the
ServeDir route table): the dashboard's ServeDir fallback serves
index.html at / via append_index_html_on_directories, and there is no
SPA path-routing catch-all to break. The dashboard SPA is served as the
plain file dashboard.html so it never shadows the exact-match
/dashboard/stream + /dashboard/history SSE routes registered before the
fallback.
- Swap the HTML entry files: the H0M3 page becomes index.html (loads
home.js → served at /), and the dashboard SPA becomes dashboard.html
(loads tabs.js → served at /dashboard.html). build.mjs copies the new
set; JS bundle names are unchanged (referenced by absolute /static/
paths, independent of the HTML filename).
- H0M3 Dashboard tile now points at /dashboard.html.
- The dashboard gains a "← home" back-link in its chrome; flow.html and
logs.html relabel their back-link from "← dashboard" to "← home"
(href stays / — which is the hub now). Pages link to the hub, not to
each other.
- Agent page (app.js + stats.js): the "↑ dashboard" link now targets
/dashboard.html. The API base (rebuild / answer-question /
mark-all-read POSTs) stays the origin root, unchanged.
- Comment-only: tabs.js / flow.js / common.js references to the
dashboard's old index.html filename updated to dashboard.html.
Note for review: git renders the file swap as a deleted home.html + an
added dashboard.html + a heavily-modified index.html, because index.html
exists on both sides with swapped content. It's a content swap, not a
rewrite — the built dist/ is verified (index.html→home.js, dashboard.html
→tabs.js).
Deferred to a follow-up: removing the FL0W / L0GS / M4TR1X "→" page-links
from the dashboard tab strip (touches tabs.js gating/overflow), and the
shared reusable chrome component (a later step).
The operator inbox moved to the dashboard's Y3R C4LL tab in #1469
(◆ 1NB0X ◆ section, with per-message + mark-all read). FL0W goes back
to being the pure event firehose, so remove its now-redundant inbox UI:
- flow.js: drop the operatorInbox store, inboxAppendFromEvent,
buildInboxListNode, renderInbox, the inbox-pill click wiring, and the
onAnyEvent hook that fed them. The side panel was only used for the
inbox flyout on this page, so drop Panel.bind() + the Panel import too.
- flow.html: remove the inbox pill, the offscreen inbox-section div, and
the side-panel markup.
- flow.css: remove the .flow-pill* and .flow-inbox-headless rules.
- home.html: Flow tile desc → "live all-agents message firehose".
- docs/web-ui/dashboard.md: drop the FL0W 0PER4T0R 1NB0X section, point
at Y3R C4LL, and fix the count-pill + /op-send descriptions.
The agent filter, sent→delivered collapse, compose box, and OS
notifications on operator-bound traffic are unaffected.
Agents that `send(to: "operator")` were easy to miss — they only
surfaced on the FL0W firehose with no read-state (#1469). Surface them
on the Y3R C4LL ("things waiting on you") tab as a proper inbox.
Backend:
- broker: `unread_for_recipient(recipient, limit)` — unacked messages
for a recipient, newest-first. Mirrors `mark_all_read`'s filter
EXACTLY (`recipient = ?1 AND acked_at IS NULL`, no `delivered_at`
condition) so everything listed is exactly what mark-read clears —
operator rows never get `delivered_at` set (no agent-socket recv).
- dashboard: `GET /api/operator-inbox` → `{ messages: [...] }` (id,
from, body, at, in_reply_to, validated file_refs). Mark-read reuses
the existing `POST /api/agent/operator/mark-all-read` (the route
format-validates the name; "operator" passes; `mark_all_read`
already acks `to="operator"` rows).
Frontend (Y3R C4LL):
- New ◆ 1NB0X ◆ section listing unread messages (sender · time · body,
path-linkified) + a "✓ mark all read" button.
- Cold-loaded on page load + on tab activation; appended live from the
broker `sent` stream (deduped on row id); cleared on mark-all-read.
- Unread count folds into the Y3R C4LL tab pill + the browser-title
`(N)` prefix, so messages are visible from any tab.
Removing the now-redundant FL0W operator-inbox UI is a clean follow-up
(deferred to avoid a flow.js conflict with the in-flight #1473).
Backend (broker + route) is host-side — @damocles to review per plan.
Closes#1469.
Adds an agent filter to the FL0W header (#1473): a select listing the
live agents narrows the timeline to messages involving the chosen agent
(matched on `from` OR `to`). Each message row now carries
`data-from`/`data-to`; non-matching rows get `.flow-hidden`. New rows
pick up the active filter at render time; changing the filter re-scans
existing rows. The selection persists in localStorage so a reload or
tab-switch keeps the view. The dropdown is populated from the live
container list (and stays current on container add/remove); a saved
selection survives even if that agent isn't currently listed.
Pure frontend; composes with the sent+delivered collapse (the surviving
collapsed row keeps its `data-from`/`data-to`). Closes#1473.
When a message directly wakes its recipient, the broker emits `sent`
then `delivered` for the same row id ~instantly, rendering two
near-identical lines on the FL0W timeline. Track each recent `sent`
row and, when its `delivered` lands within COLLAPSE_SECS (3s), upgrade
that row in place (arrow → green ✓, title "sent + delivered") instead
of adding a second line. A delivery that arrives later (recipient was
busy) still renders as its own row, so genuine delivery latency stays
visible.
`Sent` and `Delivered` share the same broker row id
(dashboard_events.rs), so matching is exact. The recentSent map is
bounded to 256 entries. Reply threading is unaffected (msgRowMap still
resolves the surviving row).
Closes#1472.
Per mara on #1464: keep l33t only in page headlines, not link labels
(we were overdoing it). H0M3's tiles now read Dashboard / Flow / Logs /
Matrix; the page banner headline stays H0M3.
First step of the dashboard-tab consolidation (#1464): a static menu
page linking to every top-level surface, so the tab strip can later
shed its `→` page-links and the dashboard can graduate Stats/Settings
to their own pages.
- dashboard/src/home.{html,css,js}: a responsive grid of link tiles
(D4SHB04RD, FL0W, L0GS, M4TR1X). Pure portal — no tabbar/SSE. Colours
from the shared theme.css, base typography from common.css. The
Matrix tile is hidden until home.js confirms `matrix_gui_enabled`
(same gating as the dashboard's M4TR1X tab); home.js also fills the
swarm/hive identity line.
- build.mjs: emit home.{html,css,js}.
Served at `/home.html` for now (additive — reachable via the existing
ServeDir, links to surfaces at their current routes). Promoting it to
`/` (and relocating the dashboard to `/dashboard` + wiring `← home`
back-links) is the next step — a route swap that touches hive-c0re's
static router, coordinating with damocles. Deliberately decoupled so
this page ships standalone without conflicting with the in-flight
tabs.js change (#1449/#1451).
Part of #1464.
Phase 1 of stylix integration (#1457): extract the Catppuccin palette
into a dedicated, separately-linked stylesheet so a theme swap can
replace just that file without rebuilding the rest of the frontend.
- shared/src/theme.css (new): the `:root` palette, moved out of
base.css (which now holds only the body typography it references).
- shared/package.json: export `./theme.css`.
- dashboard/src/theme.css + agent/src/theme.css (new): one-line
re-exports of @hive/shared/theme.css so each package's esbuild emits
its own standalone `dist/static/theme.css` (palette is NOT inlined
into the page bundles).
- both build.mjs: add theme.css to the CSS build list.
- every page (dashboard index/flow/logs, agent index/stats/screen):
link `theme.css` first, ahead of the page CSS, so the `:root` vars
resolve for everything.
- docs/web-ui/css-vars.md: document the split + the no-rebuild rationale.
Behaviour-neutral — same colours, just relocated. Verified both
`npm run build` outputs: theme.css emits standalone (383b) with the
palette; no `--*` palette defs duplicated into common.css/agent.css.
Phase 2 (nix derivation that swaps theme.css from stylix colours) is a
follow-up; touches nix/frontend.nix, coordinating with damocles.
Part of #1457.
Four selectors defined in dashboard.css but referenced nowhere in the
repo (verified against all html/js/rs, including dynamically-built class
names):
- .role-m1nd / .role-ag3nt — old per-role colouring; only `.role-pending`
is still applied (`'role role-pending'`), and there's no `'role-' +`
builder, so the m1nd/ag3nt variants are unreachable.
- .journal-body — the journald viewer is now a link to the dedicated
logs.html page (`menuLink('journal logs →', …)`), not an inline panel.
- .talkform (+ its select/input/placeholder/focus rules) — old operator
T4LK form styling, no longer emitted.
Part of #1450. Pure dead-code removal, no behaviour change; dashboard
bundle still builds (dashboard.css 36.9kb → 36.1kb).
The surfacing half of the per-agent favorite-tools stat. Reads a
`bash_commands(ts INTEGER NOT NULL, head TEXT NOT NULL)` table from the
per-agent turn-stats.sqlite — one row per bash task, written by
hive-bash-mcp (the capture half, landing separately) — and rolls up the
top-10 command heads for a doughnut, mirroring the existing
tool_breakdown.
- stats.rs: new `Snapshot.bash_breakdown` + `read_bash_breakdown()`.
The read is guarded: a missing `bash_commands` table (capture hasn't
shipped / agent hasn't run a bash task) maps to an empty list, never
an error — the snapshot degrades gracefully. Unit tests cover both
the absent-table and populated cases (incl. window cutoff + ordering).
- frontend: a "favorite tools (bash)" doughnut card on the agent /stats
page, kept hidden until bash_breakdown has data so it never shows a
permanently-empty doughnut while capture is pending.
Part of #1433 (does not close it — pairs with the hive-bash-mcp capture
half). Inert until the capture lands; merge order with it is irrelevant.
C0NT41N3R L04D on the SYST3M tab + GET /api/container-resources.
Backend (hive-c0re/src/container_stats.rs): reads cgroup v2 cpu.stat +
memory.{current,peak,max} for each running agent machine
(machine-h\x2d<name>.scope under machine.slice), read-only/world-
readable so no hive-priv. CPU is a two-sample (~200ms) host-normalised
percentage; one shared sleep covers all agents. Skips agents whose
scope dir is absent (= not running). Network omitted: agents share the
host netns, no per-container counter.
Frontend: a polled C0NT41N3R L04D table on SYST3M (agent / cpu / mem /
peak / limit with meter bars), reusing the ST4TS table style. Polls
/api/container-resources every 5s only while the tab is active.
Backend reviewed-in-principle by damocles (path escaping + cpu delta
math); ping for the on-host sign-off.
Builds the dashboard UI on the /api/stats-hive endpoint: a new ST4TS
tab showing swarm totals (active agents, turns, tokens, labelled est
cost), a busiest-first per-agent table, and a model-mix bar list.
Plain tables/CSS bars — no chart lib in the dashboard bundle; per-agent
trend charts stay on each agent's own /stats page. Fetched on tab
activation + window change (pull-only, no SSE).
Also adds conn.busy_timeout(500ms) in hive_stats read_agent per review:
turn_stats is rollback-journal, so a read landing mid-INSERT would hit
SQLITE_BUSY and silently drop that active agent — wait the write out.
First slice of #1424 (per-agent /stats enrichments):
- Backend: add per-bucket result_counts to the stats Snapshot (mirrors
model_counts), so result outcomes can be charted over time, not just
as a window total.
- Frontend: two new summary chips — cache hit-rate % (cached input vs
all input-side tokens) and avg tokens/turn — both derived from the
existing per-bucket token sums. Plus a stacked result-trend chart so
error / rate-limit / compaction spikes are visible across the window.
Hive-wide aggregate, cost estimate, and container resource load land in
follow-up PRs.
Reminder due-at labels ("in 3m 45s") and schedule next-fire cells
were computed once at render time and never updated. For short-horizon
reminders the stale count was visually misleading.
Both renderers now stamp data-due-at=<unix> on their respective
elements (.reminder-due / .sched-due). A shared 1s setInterval
ticker walks all live elements and rewrites textContent using
fmtDuration / fmtAgo — same zero-re-render pattern as the
existing question TTL chip ticker.
web_ui.rs: add available_models() helper that reads HIVE_AVAILABLE_MODELS
(comma-separated, injected by services.hyperhive.availableModels nix option).
Falls back to ["haiku", "sonnet", "opus"] when absent or empty. Field added
to StateSnapshot so the frontend receives the list on cold-load.
app.js: replace hardcoded MODEL_ALIASES array with availableModels module var.
Seeded from state.available_models before setHeader/populateOverflowMenu on
first /api/state load. Well-known aliases (haiku/sonnet/opus) still get their
descriptive labels; operator-declared custom model names show the name itself.
Implements the frontend + web_ui.rs side of issue #1359 (nix option shipped
separately in PR #1360 by atlas).
renderMetaInputs snapshots the set of checked data-meta-input values before
calling root.replaceChildren(), then restores them after rebuilding the list.
Without this, a MetaInputsChanged SSE event (e.g. fired when a concurrent
meta-update completes) would silently wipe any checkboxes the operator had
ticked but not yet submitted — forcing them to re-select their inputs.
Approval cards render their age label ("requested N ago") at
render time from a cold /api/state or an approval_added /
approval_resolved SSE event. An approval sitting pending for
an hour would show a stale "0s ago" unless one of those events
fired in the meantime.
Fix: stamp data-requested-at=<unix> on each .approval-ts span.
A 1s setInterval ticker reads all live chips, recomputes the
relative time via fmtAgo, and toggles .stale (amber highlight)
at exactly the 1-hour threshold — no longer dependent on
the next full re-render cycle to apply the amber colour.
Previously only the 'all' chip showed a count (all · N). The operator
had to click each chip to see how many questions were in each category.
Now every chip shows its own count:
all · 5 @operator · 2 @peer · 3 @agentname · 1
This lets the operator see the distribution at a glance and navigate
directly to whichever category has pending items without guessing.
Stamp data-rqe-enqueued / data-rqe-finished / data-rqe-state on queued
and terminal rqe-when spans. A 30s ticker updates them in place so
'queued 2m ago' and 'done 5m ago' labels advance as time passes.
Complement to the keyed rebuild-queue row cache: rows now persist
across snapshots, so without a ticker these static fmtAgo labels
would become arbitrarily stale. The running-entry elapsed ticker
(data-rqe-elapsed, 1s interval) already handled that state;
this fills the gap for queued and terminal states.