- hive-sh4re: ToolGroup::description() and Capability::description() return
short human-readable strings for each variant
- hive-c0re: ToolGroupsSnapshot and CapabilitiesSnapshot now include a
`descriptions` map (name → description); get_capabilities now iterates
Capability::ALL instead of hardcoding the list
- tabs.js: renderToolGroups + renderCapabilities use descriptions[name] as
the column header title attribute (native browser tooltip on hover)
stats.html had a <style> block with all its page-specific CSS inline.
Move to agent.css under a '--- /stats page ---' section, renaming
the generic .grid/.card/.empty-note classes to .stats-grid/.stats-card/
.stats-empty-note to avoid future collisions with other agent pages.
- rename cap-cap-col → cap-col; add CSS rules for .cap-col and .cap-save-col
- drop zero-width-space replace in capability header cells (nowrap makes it a no-op)
- add Capability::ALL to hive-sh4re; validate incoming cap strings in post_capabilities
Add a new P3RM1SS10NS tab to the dashboard that consolidates all
per-agent permission configuration:
Backend:
- GET /api/capabilities returns { caps: [...], assignments: {...} }
driven by Capability::ALL variants (manage_root_agent,
read_host_journal, query_agent_state)
- POST /api/capabilities/{agent} writes capabilities.json and queues
a rebuild so HIVE_CAPABILITIES takes effect
Frontend:
- New 'permissions' entry in TABS, placed after 'system'
- P3RM1SS10NS tab pane with two sections:
C4P4B1L1T13S — agents × capabilities checkbox matrix (.cap-*)
T00L GR0UPS — agents × tool-groups checkbox matrix (.tg-*) moved
from SYST3M tab
- activateTab('permissions') fetches both tables; neither has an SSE
channel so they re-fetch on each activation to stay fresh
- CSS for .cap-* mirrors the .tg-* layout (scrollable, Catppuccin)
- common.js fetchStateFile: 'HTTP' -> 'http' — last uppercase instance
in the frontend (tabs.js, app.js, logs.js already fixed in prior PRs)
- tabs.js: two redundant arrow wrappers in setTimeout dropped —
setTimeout(() => f(), N) -> setTimeout(f, N) where f takes no args
Same cleanup as the previous pass (tabs.js, app.js): replace
innerHTML = '' with replaceChildren() and lowercase the one
uppercase 'HTTP' error in logs.js.
- Replace innerHTML = '' with replaceChildren() throughout tabs.js and
app.js (12 + 7 sites). paintAtomic already used replaceChildren; now
the direct-clear sites are consistent with it.
- Add missing .rqe-source-startup_sweep CSS rule (startup_sweep is a
valid QueueSource variant but had no badge style, falling through to
the base muted appearance with no explicit intent).
- Lowercase the one uppercase 'HTTP ' in the fetchAndRenderToolGroups
error path to match every other fetch error in the file.
Add white-space: nowrap to .tab-label so text like '◆ Y3R C4LL ◆'
never breaks at spaces. Add flex-shrink: 0 to .tabbar .tab so tabs
keep their natural width rather than compressing — overflow detection
then correctly moves tabs that don't fit into the ⋮ dropdown.
overflowBtn/overflowDrop/overflowWrap were declared after syncTabFromHash()
was invoked. activateTab() calls updateTabbarOverflow() which closes over
these consts — hitting them in the TDZ threw ReferenceError on every page
load, also preventing fetchAndRenderToolGroups() from running (tool-groups
section stuck at loading).
Selecting any option in the M0V3 dropdown now fires confirm+POST
immediately; no separate button needed. On cancel or after the
request completes, the select resets to the placeholder. Removes
the now-redundant btn-move button and its CSS rule.
Remove the separate '⇡ M0V3 → ROOT' button from the selection bar.
Add '(no parent)' as the first real option in the existing M0V3
dropdown — selecting it submits an empty new_parent, which the backend
already treats as 'promote to root'.
The select placeholder label changes to '⇢ M0V3 →' so the combined
control reads naturally without the old standalone button. The submit
button enables as soon as any option past the placeholder is chosen
(selectedIndex > 0), which correctly covers both '(no parent)' and
named-parent selections.
Section heading, element id, CSS classes, and JS functions all renamed
from 'capabilities'/'cap-*' to 'tool-groups'/'tg-*' to accurately
describe what the UI manages (tool-group permissions, not a capabilities
system).
Backend (hive-c0re/src/dashboard.rs):
GET /api/tool-groups — returns { groups: [...], assignments: {...} };
groups list comes from ToolGroup::ALL so the UI needs no change when
a new group is added (satisfies the 'no extend ui' requirement)
POST /api/tool-groups/{agent} — accepts { groups: [...] }, calls
set_groups() then enqueues a rebuild so the new HIVE_TOOL_GROUPS
env var takes effect immediately
hive-sh4re/src/lib.rs:
Added ToolGroup::ALL const (ordered slice of every group)
Added ToolGroup::as_str() — snake_case wire name, matches serde
Frontend:
SYST3M tab: new C4P4B1L1T13S section above K3PT ST4T3 with
#capabilities-section placeholder
tabs.js: fetchAndRenderCapabilities() + renderCapabilities() —
columns are built from the groups array returned by the API;
each row has one checkbox per group and a save button that POSTs
and re-fetches after 800ms; agents without explicit assignments
show a (default) label; triggered on each SYST3M tab activation
dashboard.css: .cap-table-wrap/.cap-table/.cap-row/.cap-agent-*
styles for the scrollable matrix table
--mauve is not declared in base.css; the palette exposes the Catppuccin
Mocha mauve colour as --purple (#cba6f7). Affected rules:
.btn-move colour + border
.logs-back colour (back-link on logs page)
.logs-tab.logs-tab-active text colour (issue #1001 — tab shows no state)
border colour on a schedule input
Combined with the --surface1/2 fix from the previous commit, the active
log tab now shows a solid background (--border) with purple text (--purple).
dashboard.css used var(--surface0/1/2) and var(--text) throughout the
agent context menu, tabbar overflow dropdown, and logs toolbar — none
of which are declared in base.css (the palette only defines --bg-elev,
--border, --purple-dim, --fg etc). This caused all three dropdowns to
render with a transparent background (issue #996).
Replacements:
--surface0 → --bg-elev (dropdown background)
--surface1 → --border (hover / active state)
--surface2 → --purple-dim (border / separator)
--text → --fg (label colour)
Also addresses two argus yellows from the PR #997 review:
- Skip updateTabbarOverflow() early when the dropdown is open; avoids
DOM flicker from the 1s badge-update tick while the menu is visible.
- Fall back to 32px when overflowBtn.offsetWidth === 0 (hidden on the
first render) so the initial width measurement is not off.
Add a ⋮ overflow button at the right end of the dashboard tab strip.
SETTINGS and LOGS always live in the overflow dropdown (marked
data-overflow="default" in index.html — never rendered in the main bar).
Dynamic overflow: when the bar is too narrow to fit all remaining tabs,
rightmost tabs spill into the dropdown right-to-left. Implemented via
JS measurement + ResizeObserver; no CSS-only relayout trick needed.
The ⋮ button:
- hidden when the dropdown is empty (wide screens with only the default
tabs overflowed, which are always there anyway → button always shown)
- gets .tabbar-overflow-active when the current hash tab is inside
- dropdown items clone the tab label + count badge from the original
- closes on outside click / Escape
MutationObserver on the tabbar catches P33RS/M4TR1X hidden-attribute
changes so the overflow recalculates when those tabs are shown/hidden.
- live detection: use !h.status (null while running) not === 'running'
- SSE: replace named addEventListener('chunk'/'done') with onmessage +
frame.done check — backend sends generic message events, no named type
- SSE payload: use frame.stdout_append/stderr_append not frame.text
- timestamps: started_at/finished_at are unix seconds, multiply by 1000
was already done implicitly — actually use unix arithmetic directly
to avoid new Date() confusion entirely
Also move the misplaced .flow-main-slim rule from the logs section to
the flow-shell section of dashboard.css.
Add /logs.html as a standalone page (same back-link pattern as flow.html):
- BUILD tab: all-agents build log history via new GET /api/build-logs endpoint
- AGENT tab: per-container journald viewer with agent selector + unit filter
- SYSTEM tab: host-side hive-c0re.service logs via new GET /api/journal-host endpoint
Remove inline log drill-ins from SW4RM container rows (buildJournalTrigger
and buildBuildLogsTrigger) — log viewing now lives on the dedicated page.
flow.html: strip the full dashboard tabbar, replace with a simple back link
matching the new logs page chrome.
index.html: add L0GS tab link to /logs.html in the tab strip.
Backend additions:
- build_logs::list_recent_all — cross-agent query (newest first, cap 100)
- GET /api/build-logs — all-agents variant backed by list_recent_all
- GET /api/journal-host — host journald (no -M container flag), restricted
to allow-listed units (hive-c0re.service)
Per mara feedback: don't show disabled actions at all.
- restart/stop only rendered when agent is running
- start only rendered when agent is stopped
- removed .agent-menu-item:disabled CSS rule (no longer needed)
Also switch Escape handler to stopImmediatePropagation so the
selection-clear keydown listener doesn't co-fire when closing a menu.
Each agent card in the SW4RM tab now has a ⋮ button that reveals a
dropdown with single-agent lifecycle actions: R3ST4RT, ST0P, ST4RT,
R3BU1LD, DESTR0Y, PURG3. Items are state-aware (disabled when action
doesn't apply). The button is CSS-invisible until the row is hovered
or the menu is open — quiet rows stay clean. DESTR0Y and PURG3 are
hidden for the manager container.
Click-outside and Escape close the menu. A tree-rebuild (on any SSE
state update) also closes it to avoid stale DOM references.
Three post-rename artifacts:
1. journalUnit: was 'hive-m1nd.service' for the manager branch.
Both roles use 'hive-ag3nt.service' since the harness unification —
always use 'hive-ag3nt.service'.
2. buildTargetChips: hardcoded ['operator', 'manager'] candidates +
filter by n !== 'manager'. After the rename the manager's c.name
is 'root', so it was excluded from the filter but not from the
hardcoded initial list — the manager could appear twice or be listed
as 'manager' when its real broker name is 'root'. Fix: derive
the manager's name from c.is_manager on containersState; filter
sub-agents by !c.is_manager rather than by hardcoded string.
3. schedulesTableAgentSet: same hardcoded 'manager' + filter pattern
as buildTargetChips. Same fix applied.
Part of the docs-migration chore (issue #708). Remove GitHub issue
numbers from inline comments, option descriptions, and rustdoc —
these are contextless noise for anyone reading the code without
access to the original discussions. Replace with prose that captures
the same rationale directly.
No functional change. Build still clean (cargo check passes).
Container link URLs (/screen.html, /stats.html) were root-relative,
which works when the agent is accessed directly at :port/ but resolves
to the gateway root when accessed via /agent/<name>/.
Fix at the source: harness emits relative URLs (screen.html, stats.html)
and the dashboard joins containerBase + '/' + url. The agent page uses
the URL directly and relative resolution is now correct in both paths.
Adds the P33RS tab to the dashboard. The tab is hidden by default and
only shown when at least one peer hive is configured in
`services.hyperhive.peers`. When visible, it renders a card per peer
with a link to that hive's dashboard.
Backend (HYPERHIVE_PEERS env var, PeerHiveView in StateSnapshot) is
wired by a separate PR. This commit is frontend only:
- index.html: P33RS tab strip entry + tab-pane-peers section
- tabs.js: renderPeerHives(), gate logic, 'peers' added to TABS array
Dashboard links to the forge were hardcoded as http://<hostname>:3000,
which breaks when the operator accesses the dashboard through
hive-gateway (forge is served at forge.<domain> with no port).
- nix/modules/hive-c0re.nix: inject HIVE_FORGE_PUBLIC_URL when
forge.behindGateway = true (e.g. http://forge.pr1ma.darkest.space)
- hive-c0re/src/dashboard.rs: expose forge_url: Option<String> in
StateSnapshot, reading from HIVE_FORGE_PUBLIC_URL
- frontend/packages/dashboard/src/tabs.js: use state.forge_url when
present; fall back to http://<hostname>:3000 for gateway-off /
local-dev deploys
Distinguish RecvError::Lagged from Closed in the build-log stream
loop: Lagged continues (next recv delivers the full accumulated delta
via saved cursors), Closed returns (shutdown path).
Clear pre.textContent and reset cursor lengths in es.onerror when
readyState is CONNECTING so auto-reconnect doesn't double-append the
replay from cursor=0.
Backend (hive-c0re):
- build_logs.rs: add tokio::sync::broadcast::Sender<i64> to BuildLogs;
append() and finish() notify subscribers after each sqlite write.
Add BuildLogProgress struct and get_progress(id, stdout_cursor,
stderr_cursor) for incremental delta reads.
- dashboard.rs: two new endpoints —
GET /api/build-logs/id/{id}/stream SSE; streams BuildLogFrame
{stdout_append, stderr_append, status?, done} deltas until the
build finishes or the browser disconnects. Backed by an mpsc
channel task that watches the per-build broadcast notifications.
GET /api/build-logs/id/{id}/raw text/plain download with
Content-Disposition: attachment; filename build-log-{agent}-{id}.txt
Frontend (dashboard):
- tabs.js: running builds (status === null) connect an EventSource to
/stream and append lines live; "live" badge pulses amber while active,
flips to ok/fail on done. Finished builds still use the JSON fetch path.
Collapsing a running panel closes the EventSource; re-expanding
reconnects. Adds a "⬇ raw" download link to every expanded row.
- dashboard.css: .build-logs-dl inline download link; .build-logs-live
live pulse @keyframes animation.
Docs: web-ui.md updated for all three new endpoints + behaviour.
Adds a `↳ build logs · {agent}` drill-in to every agent card that
opens the side panel and fetches the last 10 build-log headers from
GET /api/build-logs/{agent}. Each row shows a status chip (ok/fail/
running), kind, age, and truncated cmdline. Clicking a row lazily
fetches the full stdout+stderr from GET /api/build-logs/id/{id} and
expands it inline as a scrollable pre.
CSS additions: .build-logs-{panel,toolbar,list,item,row-btn,...} plus
.badge-ok/.badge-fail/.badge-running status chips shared with future
uses. docs/web-ui.md updated with the new drill-in and the new badge
classes.
The ::after spinner ring used border-radius: 11px on a ~90px element,
making it a rounded rectangle. The amber arc swept around a rectangular
path, reading as a spinning border side. border-radius: 50% makes it a
true circle and the arc sweeps smoothly like a classic loading spinner.
Damocles's backend (c41bf1b) landed HYPERHIVE_HIVE_NAME / _SWARM_NAME
env vars and identity.rs accessors. This commit wires them into the two
web surfaces:
hive-c0re/dashboard.rs:
StateSnapshot gains `hive_name` + `swarm_name` (Option<String>),
populated from the env vars the c0re NixOS module injects.
hive-ag3nt/web_ui.rs:
Per-agent StateSnapshot gains the same two fields, populated from
crate::identity::hive_name() / swarm_name().
Dashboard frontend (index.html + tabs.js):
A `<p class="banner-thin" id="hive-identity">` sits above the tab
strip in the chrome. refreshState() populates it with "swarm / hive"
(or just "hive") when the fields are non-null, and updates
document.title to "<swarm>/<hive> // h1ve-c0re". No change to
chrome when both fields are null (backward compat).
Per-agent frontend (index.html + app.js + agent.css):
A hidden `.agent-hive-label` span under the title row shows the
swarm/hive label once setHeader() receives non-null names.
document.title is updated to "<label> // <hive_name>" when hive_name
is set, enabling browser-tab disambiguation when multiple hives are
open in parallel. Styled as a small uppercase purple-dim sub-label.
PR #807 fix (faint amber full ring + brighter top arc) still reads
as 'a border around the icon, with a brighter top' rather than as
a loading spinner — mara reports it's still broken.
This iteration drops the full ring entirely. The ::after now has
transparent borders on three sides plus a bright amber top arc, so
only the rotating arc itself is visible — no static ring frame
around the icon. Same shape every desktop OS uses for circular
progress indicators (Material, Apple's spinner, etc.).
Other tweaks:
- 3px stroke (was 2px) so the arc carries enough visual weight
- inset: -5px + border-radius: 11px to keep the arc hugging the
icon's 6px-radius square at the new stroke width
- 0.9s rotation period (was 1s) — slightly snappier read
Same DOM, same .pending-running class trigger; pure CSS swap.
Adds a fifth tab (◆ S3TT1NGS ◆) at the end of the dashboard tab
strip; moves the browser-notification controls out of the chrome
header into the new pane.
### Why
The chrome was carrying the notification controls as 'always-on
ergonomics' but the surface they belong on is operator preferences,
not navigation. With #589 / #701 likely adding more per-browser
preferences later (theme / density / etc.), having a dedicated
S3TT1NGS pane gives them a home so the chrome strip doesn't keep
accreting controls.
### Shape
- index.html: new `#tab-pane-settings` section with H2 + intro +
H3 'browser notifications' group; same notif-row DOM (same IDs)
migrated from the chrome
- flow.html: drops the duplicated notif-row from its chrome (FL0W
reused the dashboard's strip); adds a `◆ S3TT1NGS ◆ →`
cross-page link to `/#settings`. Notifications still fire on
FL0W when enabled — NOTIF.show() reads
Notification.permission + the hyperhive.notify.muted localStorage
key directly, no buttons needed in the page DOM.
- tabs.js: adds 'settings' to TABS array so hash routing activates
the pane
- dashboard.css: drops the chrome-anchoring `.tabbar #notif-row`
rule and the flow-page reuse comment, both obsolete; the general
`.notif-row` styling stays
- docs/web-ui.md: new `### S3TT1NGS tab` section; Chrome-header
bullet drops 'Notification controls' + adds S3TT1NGS; Browser
notifications section pointers at the new pane
### No JS or backend changes
Same IDs (notif-row, notif-enable, notif-mute, notif-unmute,
notif-status) so `NOTIF.bind()` in common.js wires the new pane
unchanged. NOTIF.show() dispatch is DOM-independent.
### Validation
`npm run --workspace @hive/dashboard build` clean (tabs.js 167.8kb,
flow.js 37.4kb, dashboard.css 50.2kb).
When `services.hyperhive.gateway.enable` is on (default), the c0re
NixOS module now sets `HIVE_GATEWAY_ENABLED=1` on the service env.
`/api/state` exposes the flag as `gateway_enabled`; the SW4RM tab's
container-row renderer flips three link sites (primary agent-name
link, favicon fetch, `container`-kind nav-strip links) from the
legacy `http://<host>:<port>/` direct TCP shape to same-origin
`/agent/<name>/` URLs — the gateway proxies them to the per-agent
harness via `agent-ports.json` or `agent-sockets.json` (#784 / #815).
Gateway-off deploys keep the direct TCP fallback so local-dev /
operator opt-out keeps working.
`forge`-kind nav-strip links still resolve against `:3000` (separate
sub-domain transition, tracked by `forge.behindGateway`);
`external`-kind links are already absolute.
Mirrors the `HIVE_MATRIX_GUI_ENABLED` env→snapshot-flag pattern.
Docs updated: `docs/web-ui.md::Container row` + new
`docs/gateway.md::Dashboard link shape` section.
Mara reported the rebuild spinner rendered as a rotating L-corner
('_|' / '⌐') instead of a smooth orbiting arc. Two issues:
1. The previous version coloured TWO adjacent border sides amber
(border-top + border-right). With the rest of the border
transparent, this paints a sharp L-shape at the icon corner —
which when rotated reads as a spinning border-corner, not a
loading spinner.
2. @keyframes had only `to` defined. Safer to include explicit
`from { transform: rotate(0deg) }` so any browser that doesn't
default cleanly still picks up the rotation.
Fix is the classic CSS spinner shape: faint amber ring around the
full icon perimeter (`border: 2px solid rgba(250, 179, 135, 0.2)`)
with one brighter top arc (`border-top-color: var(--amber)`) that
rotates. Reads unambiguously as a loading indicator.
Closes#804.
Final pass on tabs.js. All 6 remaining cookies were either
attribution refs to closed issues / past reviews or pure-history
mile-markers:
- #259 disabled meta-update state → drop attribution
- dashboard.rs#2170 backend handler line ref → drop the line
number (handler still lives in dashboard.rs, line drifts)
- mara on #695 M0V3 picker any-selection → drop attribution
(substance already captured in docs/web-ui.md::Selection bar)
- #541 journald scroll surface → drop the migration history
framing, keep present-state behaviour
- argus on #471 textContent vs innerHTML review → drop the review
attribution, keep the safer-pattern rationale inline
- #369 tab routing section header → drop the cookie, keep the
section divider
tabs.js: 6 → 0 issue-ref cookies (100% reduction). Across
batches 1-14 (#712): 73 → 0 issue-ref cookies, with substantive
prose (Topology tree CSS geometry, Container row icon layout,
Pending-state derivation, Selection bar, R3BU1LD QU3U3 cancel +
step annotations, Approval card requested-at, SSE multiplexing
worker-death self-heal, FL0W page MESS4GE FL0W body layout)
migrated to docs/web-ui.md across the batches.
dashboard SPA files all at 0 cookies now (PR #795 covers the
other dashboard files; this PR covers the tabs.js residual).
3 #NNN cookies scrubbed from inline HTML comments. The header
two-row layout (#394) and the overflow-extra-click rationale
already live in docs/web-ui.md::Per-agent page (after PRs #780
and #781 batches 1-2). Each comment shrinks to a doc-pointer.
index.html: 3 → 0 #NNN cookies (100% reduction).
Expanded the /screen endpoint description in docs/web-ui.md to
cover the substantive design rationale that lived in screen.html
comments. screen.html shrinks correspondingly.
Moved to docs/web-ui.md::Per-agent endpoints (GET /screen):
- Deliberate thinness — minimal RFB renderer; noVNC vendoring
path called out for production-grade replacement.
- Fit-mode flex-item min-width:auto clamp: a flex item's automatic
minimum size resolves to the canvas's intrinsic framebuffer
resolution and clamps CSS max-* back up, making fit mode a
silent no-op. The fix (flex: none + min-width: 0 + min-height: 0
+ explicit px sizing via relayoutCanvas()) is now documented.
- localStorage persistence for fit-mode (key screen-fit; default
on).
- Pointer rescale in sendPointer keeps clicks accurate.
- ExtendedDesktopSize pseudo-encoding (-308 rect) gates the
match-size button.
Collapsed in screen.html: 8 #NNN cookies scrubbed across all
inline comments. #133 (canvas-sizing fit bug — closed) ×5,
#52 (noVNC vendoring — closed) ×1, #14 (relative URL — closed
my piece) ×1, plus one CSS-block #133. Each comment shrinks to
a brief pointer.
screen.html: 8 → 0 #NNN cookies (100% reduction). Net ~26 lines
of substantive prose moved into docs/web-ui.md.