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.
Add a per-job timeout so a wedged build fails in minutes instead of
hanging until the runner's 3h cap. 30 min on the nix flake check job
(well above a cold-cache rebuild, well under the hard cap) and 5 min
on the seconds-long tracker-tag lint. Complements the runner-level
self-heal (a watchdog that restarts a deadlocked runner): the job
timeout catches a hung job, the watchdog catches a wedged runner.
Per operator guidance: the required-status-checks config gates merge
on the nix flake check only, not this lint. So drop the warn|deny
mode — the lint just fails (exit 1, error annotations) when it finds
tracker tags, and runs as its own CI job so that failure shows red on
the PR without failing the required nix flake check job or blocking
merge. Once the legacy backlog is cleaned up, promoting this job to a
required check flips it to a hard gate — no code change.
The hive convention is prose, not tracker tags, in code, but it was
enforced only at review time — three PRs this session needed
request-changes purely for stray tags in new comments.
Add scripts/check-issue-refs.sh: scans tracked source (rust, nix, js,
ts, css, html; markdown exempt) for a hash followed by an issue number
and emits a CI warning annotation per hit. The pattern is a hash, 2-5
digits, then a non-hex char or end-of-line, so it skips CSS hex
colours (letter-bearing or six/eight-digit) while catching tags; a
pure-numeric short hex is the only residual false positive (dodge with
the six-digit form).
Wire it into the CI workflow as a fast pre-check before nix flake
check. It runs in warn mode (exit 0) so it does not block while the
legacy backlog is cleaned up; the script takes a warn|deny arg so the
later flip to a hard gate is a one-word change, not a rewrite — the
same rollout shape as tightening a clippy lint.
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.
Part of the tracker-tag cleanup: the hive convention is prose, not
issue-tracker tags, in code. Reword the 21 tags in the nix tree
(flake.nix + the hive-c0re/ci/gateway/network modules) to describe
the thing they pointed at, preserving the context without the tag.
Comment-only — no eval or logic change. Validated with nix fmt
(no reformatting) and nix flake check --no-build (all checks
evaluate clean); the full build check was skipped locally because
the shared remote builder is degraded, so CI will exercise the
build derivations once the runner recovers.
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.
The gateway container starts alongside every hyperhive deployment, so
gating it behind a separate enable flag was a footgun: an operator who
set it false lost the only thing exposed to the outside while the
agent containers kept running. Re-gate the gateway config on the
top-level services.hyperhive.enable instead.
- hive-gateway.nix: drop the gateway.enable mkOption; gate the config
block on config.services.hyperhive.enable.
- hive-forge.nix: behindGateway now defaults to services.hyperhive.enable;
remove the behindGateway-requires-gateway assertion (now vacuous).
- hive-network.nix: remove both gateway.enable assertions (vacuous).
- hive-c0re.nix: drop the firewall.allowedTCPPortRanges 8100-8999
fallback that opened agent ports when the gateway was off (the
gateway is now the sole entry point); HIVE_GATEWAY_ENABLED is always
set since the gateway always runs.
- nix/docs/default.nix: remove the gateway.enable = mkForce false stub
(would be an eval error against the removed option; the gateway is
already re-gated on hyperhive.enable, which docs force false).
- hive-matrix.nix, dashboard.rs: comment/prose updates only.
BREAKING: operators relying on services.hyperhive.gateway.enable = false
to suppress the gateway must instead point their own reverse proxy at
the gateway's port. NixOS errors clearly on the now-unknown option.
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.
Builds on the merged dashboard theming: the themed-frontend overlay now
swaps colors.css in both the dashboard/ and agent/ dist subtrees, and the
agent frontend dir (HIVE_AGENT_FRONTEND_DIR, which the gateway serves agent
static files from) points at the themed tree too. So when stylix is enabled
on the host, both the dashboard and the gateway-served agent UIs re-theme
from the host palette — still zero-op auto-detect, still a no-op (both dirs
on the unthemed dist) when stylix isn't imported.
Not covered: an agent reached directly on its own harness web server (no
gateway) serves from its per-agent mergedDist, built in the agent's own
nixosSystem with no host stylix access — that path needs the base16 palette
forwarded host->agent and is tracked 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.
The system prompt enumerated every hive-forge verb inline — which drifted
(it named pr-reviews but not pr-status), so agents skimming it for how to
check CI didn't see pr-status and reached for curl against the forge,
breaking the no-curl rule (hit multiple agents). The enumeration also
auto-loads into every turn for every agent, costing context for a list
that's better discovered live.
Replace it with a short description + 'discover verbs/signatures via
hive-forge --help / hive-forge <verb> --help', keeping only the few
conventions --help won't surface: never curl the forge (check CI with
pr-status, not curl); --body-file - HEREDOC bodies; pr-create --push; and
forge notifications arriving via the message daemon. docs/tools/forge.md
remains the full reference.
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.
When the operator's host config has stylix enabled, generate a base16
colors.css from its palette and overlay it onto the bundled frontend dist,
then serve that themed tree as the dashboard's HIVE_STATIC_DIR. Zero-op
auto-detect — no flake input, no operator action, no npm/esbuild rebuild
(a pure file-copy over the prebuilt dist). colors.css is the whole swap
contract; theme.css derives the semantic vars from the 16 base16 slots.
Guarded access (config.lib.stylix / config.stylix.enable via 'or' fallbacks)
makes it a clean no-op when stylix isn't imported — HIVE_STATIC_DIR stays
on the unthemed dist, which is what every non-stylix deployment + CI gets.
Dashboard surface only for now; the agent surface needs the base16 palette
forwarded host->agent (the per-agent harness builds its own dist) and is a
separate follow-up.
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.
The systemd ExecStart carried every host-level setting as its own flag —
nine of them, including two escaped JSON blobs (the context-window map and
the model-price table). Collapse them into a single `--config <file>` JSON.
- Reuse the existing HiveEnv as the container-injected config shape (add
Deserialize + Default), and add a ServeConfig wrapper = flattened HiveEnv
plus the hive-c0re-local model_prices table (kept out of HiveEnv since it
is never injected into containers). serde(default) lets any field be
omitted and fall back to its canonical default.
- clap: add --config; the per-setting flags become optional overrides
(config file is the base, explicit flags win — preserves hivectl/debug
ergonomics and bare `hive-c0re serve`).
- Coordinator::open and cmd_serve now take the bundled HiveEnv, which drops
their too_many_arguments clippy allows. cmd_serve keeps a single
too_many_lines allow (inherent daemon-boot orchestration, not arg-driven).
- nix: write the config as JSON to the store + pass --config, so ExecStart
is one short line.
- Add a round-trip test proving the flatten + per-field defaults work.
Closes the ExecStart-length issue.
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.