A stopped or mid-transient (restarting / rebuilding) container's
web server isn't answering, so its <url>/icon background-image
just failed to an empty box on the card.
When the container isn't reachable (not running, or a transient
is in flight) the icon now falls back to the dimmed hyperhive
mark — /favicon.svg, served by the dashboard itself so it's
always loadable — greyscaled + lowered opacity via the
.icon-unreachable class.
closes#195
Follow-up to #188. Two additions to the side-panel file preview:
- Markdown files get a rendered/plain tabbed view (was: always
rendered, no way to see source) — same tab pattern as SVG.
- Raster images (png/jpg/gif/webp/bmp/ico/avif) render as an
<img>. /api/state-file previously from_utf8_lossy-stringified
every file and served text/plain, which corrupts binary; it
now serves image files as raw bytes with their real
content-type (over-cap images are rejected, not truncated —
a clipped binary is corrupt).
buildSvgPanel generalised to buildTabbedPreview, shared by SVG +
markdown. .svg-host/.svg-render renamed .preview-host/.img-preview
since they now back images + md too.
closes#192
SVG files in the side-panel file preview showed only raw source.
Add a rendered/source tabbed view: 'rendered' (default) shows the
image, 'source' shows the markup.
The image loads via an <img> data: URI — <img>-loaded SVG runs in
the browser's secure static mode (scripts + external fetches
disabled), so an untrusted SVG from an agent's state dir can't
execute code in the dashboard origin. Tabs reuse the existing
diff-base-tab styling; a checkerboard backs the image so
transparent regions read clearly.
closes#188
Per review: build the full forge profile URL in the harness instead
of the client. /api/state now returns forge_url: Option<String>
(assembled from the request Host header — resolves against whatever
host the operator reached the page on), replacing the forge_present
bool. The JS just links forge_url when present — no client-side URL
construction.
Add a '⬡ forge ↗' link to the per-agent page's meta row, next to
the stats + screen links. It opens the agent's Forgejo profile
(http://<host>:3000/<label> — the per-agent forge user is named
after the agent) in a new tab.
- web_ui.rs: StateSnapshot gains forge_present, true when the
agent's forge-token file exists in the state dir (same signal
that tells the agent it has a forge account).
- index.html / app.js: hidden link, shown + href-filled when
forge_present, mirroring the existing gui_enabled/screen-link
pattern. Host comes from window.location so it works off
whatever host the page is served from.
closes#185
PR #171 made relayoutCanvas() set canvas.style.width/height
explicitly, but the canvas is a flex item: flex items default to
min-width/min-height: auto, which resolves to the canvas's intrinsic
framebuffer resolution and clamps the JS-set display size right back
up to native — so fit mode still did nothing (#133, "still same
behavior").
Add min-width/min-height: 0 (+ flex: none) on the canvas in fit mode
so the explicit downscaled size actually sticks. Scoped to
#canvas-wrap.fit so non-fit mode keeps native size + scroll.
re #133
The VNC desktop faded to black after weston's default 300s idle
timeout, and on wake desktop-shell showed its click-to-unlock lock
screen (a green circle) — pointless for an agent desktop viewed
over /screen, and confusing for the operator (issue #180).
Add [core] idle-time=0 to the generated weston.ini. Verified against
weston 14.0.1: idle-time parses to compositor->idle_time, and
weston_compositor_wake arms the idle timer with idle_time*1000 ms;
0 ms makes wl_event_source_timer_update disarm the timer, so the
compositor never transitions to IDLE and desktop-shell never locks.
closes#180
The agent icon was a 26px <img> inline in the card head, hidden via
onerror when a stopped container's web server didn't answer — which
collapsed the slot and shifted the row.
Restructure the live container card as icon-left / body-right:
- the icon is a background-image div with aspect-ratio 1 and
align-self stretch — full card height, square, and (being a
background) it has no intrinsic size, so loading or failing it
can never reflow the row;
- a failed load (stopped container) falls through to a placeholder
fill instead of collapsing;
- the three content lines move into a .card-body column.
Tombstone rows keep the plain stacked layout (:not(.tombstone)).
closes#177
The main dashboard had no favicon — PR #145 added them to the
per-agent pages but missed hive-c0re's index. Serve branding/
hyperhive.svg at /favicon.svg and declare it in the index head.
The dashboard represents the whole hive, so it uses the project
mark (per-agent pages keep their own configurable /icon).
closes#173
The fit toggle relied on canvas.fit { max-width/max-height: 100% }.
The canvas is a flex item, and a flex item's automatic minimum size
(min-width/min-height: auto → the replaced element's intrinsic size)
overrides max-*, so an oversized desktop never shrank — it just got
centred and clipped, looking like the toggle did nothing.
Replace the CSS approach with explicit JS sizing: relayoutCanvas()
computes a scale factor (min of width/height ratios, capped at 1 so
it never upscales) and sets canvas.style.width/height in px,
preserving aspect ratio. Recomputed on fit toggle, window resize,
and framebuffer-size (ServerInit). Pointer mapping is unaffected —
sendPointer already derives scale from getBoundingClientRect().
The dashboard cold-loaded its derived stores (approvals, questions,
containers, …) from /api/state once, then relied solely on live SSE
events. Events that fired during a disconnect window (reconnect,
hive-c0re restart) are never replayed, so the dashboard drifted stale
until a manual reload.
- terminal.js: add onStreamOpen, fired on every EventSource open
(initial + reconnect); the dashboard wires it to refreshState() so
every connection epoch re-syncs the authoritative snapshot.
- terminal.js: seq-dedupe only event kinds that actually appeared in
the history replay. Mutation events are never in /dashboard/history,
so deduping them against the broker-history seq wrongly dropped ones
that fired between the /api/state snapshot and the history fetch.
- app.js: make applyApprovalResolved / applyQuestionResolved
idempotent (guard the history unshift by id) so a re-sync
overlapping a live event can't double a history row.
closes#163
row_to_approval matched only apply_commit + spawn, so any approvals
row with kind=init_config (added by 80dd5bb's two-step spawn) failed
to deserialize. pending() / recent_resolved() collect all-or-nothing
via collect::<Result<Vec>>(), so one bad row errored the whole query;
api_state's log_default then swallowed the error and returned an empty
list — every pending approval vanished from the dashboard (issue #160).
- add the missing init_config arm to row_to_approval
- collect_lenient(): skip + log unparseable rows so a single bad row
can never blank the whole approvals list again
- dashboard: label init_config approvals 'init' (was mislabeled
'spawn' by the apply-vs-other fallthrough)
closes#160
recv tool-use rows rendered as a bare recv() regardless of args,
hiding whether a turn is parked on a long-poll (wait_seconds) or
draining a burst (max). fmtToolUse now surfaces both. Bash rows
gain a [bg] flag when run_in_background is set.
closes#158
Consumes the GET /icon endpoint from #139:
- Dashboard: each container card shows the agent's icon next to its
name (26px). Loaded from <agent-url>/icon; onerror hides it for a
stopped container whose web server isn't answering.
- Per-agent web UI: the agent's icon next to the page title (40px),
and /icon as the favicon on the index, stats, and screen pages.
/icon always returns an image (configured SVG or the default
hyperhive logo), so no presence check is needed.
Closes#140
Foundation for the per-agent icon feature (#137).
- harness-base.nix: new hyperhive.icon option (nullable path to an
SVG). An agent commits an SVG into its config repo and references
it as ./icon.svg; when set it lands at /etc/hyperhive/icon.svg.
- web_ui.rs: GET /icon serves the configured SVG, falling back to the
bundled hyperhive logo when none is set — so it always returns an
image and consumers can hit it unconditionally.
Closes#139
Adds the hyperhive logo (amber-on-dark hexagonal hive) as repo assets
under branding/ — hyperhive.svg (primary, scalable, 5 KB) and
hyperhive.png (raster fallback). Displays the SVG centered at the top
of the README.
Closes#136
Adds a 'fit' toolbar toggle on the /screen VNC viewer. When on (the
default), the canvas is CSS-scaled down to fit the browser viewport
preserving aspect ratio — no more scrolling a desktop larger than the
window. When off, the canvas renders at native resolution with scroll.
The choice persists in localStorage.
The canvas's intrinsic resolution is never touched — only its display
size. sendPointer now rescales client coordinates by the canvas
display-vs-intrinsic ratio so clicks stay accurate in fit mode (this
also fixes a latent off-by-scale bug). Toolbar buttons share a .tbtn
class for consistent styling.
Issue #133 (fit toggle now; dynamic desktop resize tracked separately).
CountPendingReminders and ReminderRollup were hardcoded to
MANAGER_AGENT. Both now take agent: Option<String> — None keeps the
current behavior (manager's own), Some(name) returns that agent's
reminder stats. The broker functions already take an agent name, so
this is a thin wire-protocol change. Callers (web UI stats page,
post-turn counts) pass None.
Closes#122
PR #119 changed get_loose_ends to default to the manager's own items
and accept an optional agent arg ("*" = hive-wide, "<name>" = one
agent), but the manager system prompt still described it as
"hive-wide ... no args". docs/turn-loop.md was already updated;
only manager.md was missed.
Two RFB protocol bugs in the /screen client:
1. ServerInit parsed framebuffer-height from byte offset 1 instead of
2. RFB 3.8 ServerInit is width@0-1, height@2-3; u16be(b,1) mixes
the low byte of width with the high byte of height. The wrong
height went into the initial non-incremental FramebufferUpdate-
Request; neatvnc feeds the client's width/height straight into
pixman_region_union_rect un-clipped (src/server.c), so an
out-of-range height corrupts the server damage region and pixman
reports 'Invalid rectangle passed'.
2. FramebufferUpdate handler had a stray drainTo(1) after the 3-byte
padding+nRects header, consuming the first byte of the first
rectangle and desyncing every update.
Closes#128
Two bugs found via the weston journal (issue #92):
1. MD5 rotation index used (j>>2 & 3) for the round number, which
cycles every 4 steps instead of every 16. Verified against RFC 1321
test vectors: md5("") was 7a1dce5b... instead of d41d8cd9... — the
derived AES key was wrong, so the server decrypted the credentials
to garbage. Fixed to j>>4.
2. weston's vnc_handle_auth calls getpwnam(username) and requires
pw_uid == weston's own uid before PAM is consulted. We sent an empty
username, which fails outright ("VNC: wrong user"). weston runs as
root, so send username "root"; the empty password still passes via
pam_permit.so on the weston-remote-access service.
Fixes#92
GetLooseEnds now takes agent: Option<String>:
- None = manager's own loose ends (default; the bug fix)
- Some("*") = hive-wide view (every approval/question/reminder)
- Some("name") = that agent's loose ends
The get_loose_ends MCP tool exposes this as an optional agent arg, so
the manager can still scan the whole swarm on demand. The web UI and
post-turn counts pass None (manager's own).
hive_wide returns ALL agents' reminders and questions, causing other
agents' reminders (e.g. triage) to appear on the manager's web page
and in the get_loose_ends MCP tool. The MCP tool spec says it shows
your own pending items — switch to for_agent(MANAGER_AGENT) which
includes all approvals (manager is sole submitter), questions where
the manager is asker/target, and only the manager's own reminders.
Fixes#118