Instead of requiring a click on the load-more pill, trigger loadMore()
automatically when the user scrolls within LOAD_MORE_SCROLL_PX (80px)
of the top of the log. The pill stays as a visual indicator that more
history is available.
Without a viewport meta tag, some browsers (notably Firefox with
Fingerprinting Protection active) internally read screen.availWidth /
screen.availHeight to compute the default viewport size, producing the
console warning 'Fingerprinting Protection is altering screen.availWidth
and screen.availHeight'.
Fix:
- Add <meta name="viewport" content="width=device-width, initial-scale=1">
to all five pages that were missing it (dashboard index/flow/logs, agent
index/stats). screen.html already had it.
- Replace window.innerWidth with document.documentElement.clientWidth in
the side-panel drag-resize code in common.js. clientWidth returns the
actual CSS layout viewport width and is not rounded by Firefox's
Fingerprinting Protection, making the drag calculation correct even
with privacy.resistFingerprinting enabled.
Remove the deployed sha plain-text badge from the card head.
Add a forge-linked menu item to the three-dot menu instead:
'deployed:abc123def012 ↗' opens agent-configs/<name>/commit/<sha>
on the hive forge in a new tab.
Only shown when both c.deployed_sha and forgeBase are present.
buildAgentMenu now takes forgeBase as second arg; call site passes
the same forgeBase computed for nav-strip links.
CSS: add text-decoration: none + box-sizing to .agent-menu-item
so <a> link items render identically to the <button> items.
- Wrap table in .schedules-table-wrap (overflow-x: auto) so it
never exceeds the page width on narrow viewports
- Add .schedules-table-next-col (width: 8em) and
.schedules-table-every-col (width: 7em) with white-space: nowrap
to stop those columns from stealing space from the body column
- Align visual style with the perm page tables: unified border on
all cells (was: border-top row separator only), bg-elev thead,
font-size 0.82em (was 0.9em), min-width: 100% on the table itself
Drop the 'h-sock :8421' meta span from the agent card head.
ContainerView.container is now #[serde(skip)] since no frontend
code reads it; the field stays in Rust for internal use.
c.port remains serialized — used for fallback agent UI URLs.
- 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.