Watch
0
0
Fork
You've already forked hyperhive
0

Compare commits

...
Author SHA1 Message Date
iris
565a9ea18f shared: drop the removed core page from the tab-strip comment 2026-10-16 20:02:28 +02:00
iris
92206ba63e dashboard: fix stale core.js comment refs and a doubled word in the gateway comment 2026-10-16 20:02:28 +02:00
iris
0622736b30 dashboard: serve the dashboard at /, fold kept state in as a tab, add a pages menu
Drop the H0M3 tile hub and the core page. The dashboard is now the index;
kept state is its fifth tab. A pages menu in the chrome links flow, stats,
API docs and the swarm UI. The swarm apex URL comes from
services.hyperhive.swarm.domain via HIVE_SWARM_PUBLIC_URL and the new
swarm_public_url field of the state snapshot.
2026-10-16 20:02:28 +02:00
37 changed files with 664 additions and 989 deletions

View file

@ -10,24 +10,25 @@ what you actually do here.
## Where things are
Everything starts at the **H0M3 hub**, served at `/` — a grid of tiles
linking to every surface (Dashboard, Flow, Logs, Stats,
Settings, Core). Every page links back to H0M3, so you're
never more than one step from the hub.
Everything starts at the **dashboard**, served at `/`. A pages menu
(the `☰` button at the left of the tab strip) links to the other
surfaces — Flow, Stats, the API docs and the swarm UI — and each of
those pages links back to the dashboard.
The **dashboard** itself (`/dashboard.html`) is where you'll spend most
of your time. It's a single page with four tabs:
The dashboard is where you'll spend most of your time. It's a single
page with five tabs:
- **4G3NTS** — every agent, live. This is the default tab and the one
you'll check most.
- **R3BU1LD QU3U3**, **M3T4 1NPUTS** and **BUILD L0GS** — the build
lifecycle: the rebuild queue with a live log of whatever is building,
the meta flake inputs you can bump, and the build log history.
- **K3PT ST4T3** — kept state from agents with no container
(tombstones), with a purge button per entry.
Everything else lives on its own page instead, all reachable from the
H0M3 hub: **Flow** (`/flow.html`, the raw live message stream across
the whole swarm), **Stats** (`/stats.html`, swarm-wide usage stats), and
**Core** (`/core.html`, tombstones and container resource use).
Two things live on their own page instead, reached from the pages
menu: **Flow** (`/flow.html`, the raw live message stream across the
whole swarm) and **Stats** (`/stats.html`, swarm-wide usage stats).
Each agent also has its own page — a full terminal view of that
agent's session, reachable by selecting its name anywhere in the

View file

@ -71,7 +71,7 @@ at a given viewport width. Two columns:
- **Inbox / todos pills** (`📬 inbox · N` / `📋 todos · N`): hidden
when empty; selecting it opens the matching flyout in the side panel.
- **Links badge** (`🔗`): opens a popover listing `↑ dashboard`
(back-link to the host dashboard, `${dashboardBase}dashboard.html`)
(back-link to the host dashboard, `${dashboardBase}`)
followed by this agent's `agent_links()`-sourced navigation (stats,
screen when VNC is enabled, forge profile + config-repo mirror when
the agent has a forge account, any `services.hyperhive.agent.dashboardLinks`

View file

@ -8,29 +8,32 @@ surfaces — the agent roster, agent creation, linking external forge and
matrix accounts, the controller's job graph — are on the
[swarm UI](../swarm/ui.md).
hive-c0re serves the dashboard at `/dashboard.html` (with the home page at `/`).
hive-c0re serves the dashboard at `/`.
It has a fixed chrome header at the top and a `<main>` that shows exactly
one tab pane at a time. The URL hash (`#agents`, `#rebuild`, `#meta`,
`#buildlogs`, `#permissions`) drives which pane is
active; hash changes don't reload the page. FL0W and the other
standalone pages are reachable from the H0M3 hub at `/`, not
from the dashboard tab strip.
`#buildlogs`, `#kept`) drives which pane is
active; hash changes don't reload the page. FL0W and ST4TS
are standalone pages reached from the pages menu, not from the
dashboard tab strip.
**Chrome header** (fixed, overlays the active tab pane):
- **← home back-link**: top-left of the chrome, links to the H0M3 hub
at `/`. Every surface links back to the hub rather than to each other.
- **Pages menu**: a `☰` button at the left of the chrome (`links-menu.js`)
opening a popover with links to **flow** (`/flow.html`), **stats**
(`/stats.html`), **api docs** (`/api/docs/`) and **swarm** (the swarm
UI at the swarm apex domain, opened in a new tab). The swarm entry
comes from `state.swarm_public_url`, which hive-c0re reads from
`HIVE_SWARM_PUBLIC_URL` (set from `services.hyperhive.swarm.domain`);
with no domain set there is no swarm entry. The standalone pages link back to `/`.
- **Tab strip**: `◆ 4G3NTS ◆`, `◆ R3BU1LD QU3U3 ◆`, `◆ M3T4 1NPUTS ◆`,
`◆ BUILD L0GS ◆`. In-page tabs only —
the SYST3M panels live on the
standalone **C0R3** page (`/core.html`), and FL0W / ST4TS
live on their own pages too, all reachable from the
H0M3 hub (not the tab strip). Count pills on 4G3NTS
(stale containers) and R3BU1LD QU3U3 (rebuilds in flight).
`◆ BUILD L0GS ◆`, `◆ K3PT ST4T3 ◆`. In-page tabs only — FL0W / ST4TS
live on their own pages, reached from the pages menu. Count pills on
4G3NTS (stale containers), R3BU1LD QU3U3 (rebuilds in flight) and
K3PT ST4T3 (kept-state entries).
- **Banner-thin** (`░▒▓█▓▒░ HYPERHIVE / HIVE-C0RE / WE ARE THE WIRED ░▒▓█▓▒░`)
— sits below the tab strip.
- **Server-warnings banner** — a generic, sticky top-of-page strip shown
on **every** page (dashboard + every stand-alone page — FL0W,
H0M3, C0R3, ST4TS), injected at the top
on **every** page (the dashboard + the stand-alone FL0W
and ST4TS pages), injected at the top
of `<body>` by `renderServerWarnings` in `common.js`. Driven by
`state.server_warnings` — a list of `{ kind, level, message }` — and
coloured by `level` (`warn` amber / `crit` red). hive-c0re owns the
@ -52,7 +55,7 @@ from the dashboard tab strip.
to `<swarm> / <hive> // h1ve-c0re` once `hive_name` / `swarm_name`
arrive in the state snapshot.
The FL0W page uses a slim header (a `← home` back-link + the page
The FL0W page uses a slim header (a `← dashboard` back-link + the page
title) rather than the dashboard tab strip — it's a standalone surface,
not a tab pane.
@ -70,15 +73,12 @@ conflicting agent.
`↻ UPD4TE 4LL` button appears above the containers list when any
agent is stale.
## C0R3 page (`/core.html`)
## K3PT ST4T3 tab
Passive / rare-interaction state, reached from the **Core** tile on the
H0M3 hub (served at
`/core.html`), with minimal chrome: a `← home` back-link and a single pane,
**K3PT ST4T3**. The page is its own esbuild bundle (`core.js`) that
cold-loads `/api/state` and subscribes to `/api/dashboard/stream` for
`tombstones_changed`. (Rebuild queue and meta inputs are dashboard
tabs — see below.)
Passive / rare-interaction state. `kept.js` renders it from the
`/api/state` snapshot and keeps it live from the `tombstones_changed`
stream event. (Rebuild queue and meta inputs are dashboard tabs too —
see below.)
Per-container CPU, memory and disk load isn't shown in the hive UI. The
same figures go out as OTEL metrics and appear in Grafana (see
@ -220,8 +220,8 @@ entry with that id pre-expanded.
Hive-wide turn statistics, aggregated across every agent's
`hyperhive-turn-stats.sqlite` for the selected window. Not a dashboard
tab — a standalone page reached from the **Stats** tile on the H0M3
hub, same minimal chrome as `/flow.html`. Distinct from
tab — a standalone page reached from the dashboard's pages menu,
same minimal chrome as `/flow.html`. Distinct from
each agent's own `/stats` page (which carries the per-agent trend
charts): ST4TS is the swarm-level rollup.
@ -261,9 +261,9 @@ prices. Models not covered fall back to hive-c0re's built-in estimate.
## FL0W page (`/flow.html`)
A dedicated full-page terminal (not a tab pane — a separate HTML
page). Slim chrome: a `← home` back-link to the H0M3 hub, the FL0W
page). Slim chrome: a `← dashboard` back-link, the FL0W
title, and the agent-filter select (see below). No dashboard tab
strip — FL0W is a standalone surface reached from the hub.
strip — FL0W is a standalone surface reached from the pages menu.
**MESS4GE FL0W** — live broker tail wrapped in a `.terminal-wrap`.
Cold load backfills the last ~200 messages from `/api/dashboard/history`;
@ -294,17 +294,6 @@ Tab/Enter to confirm; `@*` broadcasts). `POST /api/op-send` drops
`{from:"operator", to, body}` into the broker; the resulting SSE frame
re-renders the terminal row. The operator addresses the root agent as `@root`.
## H0M3 page (`/`)
The H0M3 hub is the primary landing page (served at `/` by default). A
responsive grid of link tiles — Dashboard, Flow, Stats,
Core, API — each pointing to their respective
surfaces, all unconditionally shown (no gating). The page is a pure
portal with no tab-bar or SSE subscriptions. Typography + colours inherit
from the shared theme (Catppuccin Mocha via `common.css` + `theme.css`).
`home.js` fills the swarm/hive identity line at the top. All dashboard
sub-pages include a `← Home` back-link for navigation.
## Container row
A full-height **square agent icon** (5em, capped) on the left. The
@ -579,7 +568,7 @@ selected) before the bar appears.
## Dashboard endpoints
Also browsable interactively at `/api/docs` (a Swagger UI, linked from
the H0M3 hub's **API** tile), with the raw spec at `/api/openapi.json`.
the pages menu's **api docs** entry), with the raw spec at `/api/openapi.json`.
It's a growing supplement, not yet a full replacement for the list
below — some endpoints aren't in it yet.
@ -703,7 +692,7 @@ payload):
batch (`reminder_scheduler`).
- `tombstones_changed` (seq, tombstones: `Vec<TombstoneView>`) —
full snapshot of destroyed-but-state-kept agents. Emitted on
destroy, purge, and revive. C0R3 K3PT ST4T3 tab subscribes.
destroy, purge, and revive. The K3PT ST4T3 tab subscribes.
- `meta_inputs_changed` (seq, inputs: `Vec<MetaInput>`) — full
snapshot of `meta/flake.lock` inputs. The M3T4 1NPUTS tab
subscribes; same shape as `GET /api/meta-inputs`.

View file

@ -19,11 +19,9 @@ repeated here.
visual noise — the base16 palette has multiple accent slots
(`--purple`, `--cyan`, `--pink`, …); use more than one where it adds
distinction, not decoration for its own sake.
- **Whimsy** — small, delightful touches are welcome (the per-hive
dashboard's home-page matrix-rain background, `packages/dashboard/src/
home.js`, is the reference example — currently in the dashboard
package, not swarm-ui itself, but the pattern it sets applies here
too). Whimsy still has to clear the accessibility bar below (motion,
- **Whimsy** — small, delightful touches are welcome (the matrix-rain
background, `src/shell/MatrixRain.tsx`, is the reference example).
Whimsy still has to clear the accessibility bar below (motion,
in particular). swarm-ui's own reference example: `Panel`'s optional
`icon` prop (`src/ui/panel/Panel.tsx`), a small emoji glyph in a
panel's header, chosen per panel with no default — grew out of a
@ -49,12 +47,10 @@ home.js`, is the reference example — currently in the dashboard
**General rule, not case-by-case: every non-essential CSS animation
gates on `prefers-reduced-motion` and pauses when its tab/section is
hidden.** The dashboard's matrix-rain background (`packages/dashboard/
src/home.js`/`.css`) is the existing reference implementation of this
pattern — genuinely does both today, confirmed by reading it — even
though it lives in the dashboard package rather than swarm-ui; every
future swarm-ui animation follows the same pattern, not just whimsy
pieces.
hidden.** The matrix-rain background (`src/shell/MatrixRain.tsx`) is the
existing reference implementation of this pattern — it does both today,
confirmed by reading it; every future swarm-ui animation follows the
same pattern, not just whimsy pieces.
<!-- vale write-good.Passive = YES -->

View file

@ -19,8 +19,8 @@
from `@hive/shared` (was `hive-fr0nt`); the dashboard ships one
CSS bundle per page (`colors.css` + `theme.css` + `common.css`,
loaded by every page, plus a page-specific bundle —
`dashboard.css` / `flow.css` / `home.css` /
`stats.css` / `core.css`);
`dashboard.css` / `flow.css` /
`stats.css`);
`common.css` inlines `@hive/shared`'s
`base.css` + `terminal.css` + `tabs.css` + `chrome.css` +
`pill.css` via esbuild's `@import` resolution.

View file

@ -31,7 +31,7 @@ export interface MetaNavProps {
links: AgentLink[];
forgePublicUrl: string | null;
/** Absolute base URL of the host dashboard (`resolveDashboardBase`)
* — the `🧭 dashboard` item links to `${dashboardBase}dashboard.html`. */
* — the `🧭 dashboard` item links to `${dashboardBase}`. */
dashboardBase: string;
}
@ -86,7 +86,7 @@ export function MetaNav({
<div class="meta-nav-popover" role="menu" aria-label="agent links">
<a
class="meta-nav-item"
href={`${dashboardBase}dashboard.html`}
href={dashboardBase}
target="_blank"
rel="noopener"
role="menuitem"

View file

@ -507,15 +507,10 @@ window.Chart = Chart;
// When accessed via hive-gateway the page lives at `/agent/<name>/`
// on the same origin as the dashboard. Detect via path prefix
// rather than the direct port (which is unreachable or wrong scheme
// behind HTTPS TLS termination). The dashboard SPA lives at
// `dashboard.html` — the `/` root now serves the H0M3 menu hub.
// behind HTTPS TLS termination). The dashboard SPA is the `/` root.
dl.href = window.location.pathname.startsWith("/agent/")
? window.location.origin + "/dashboard.html"
: "http://" +
window.location.hostname +
":" +
s.dashboard_port +
"/dashboard.html";
? window.location.origin + "/"
: "http://" + window.location.hostname + ":" + s.dashboard_port + "/";
} catch (_) {
/* non-fatal */
}

View file

@ -1,5 +1,5 @@
// esbuild build for @hive/dashboard: one JS+CSS bundle per page (H0M3 at
// `/`, dashboard/flow/core/stats at their own
// esbuild build for @hive/dashboard: one JS+CSS bundle per page (the
// dashboard at `/`, flow/stats at their own
// `.html`), each named after its own entry point below — see the
// `entryPoints`/`for` lists for the exact map, not repeated here to
// avoid this comment drifting out of sync with the real build steps.
@ -9,10 +9,7 @@
// module into each bundle rather than emitting a shared chunk (no
// `splitting: true`). The Rust binary mounts `dist/` as a
// `tower_http::ServeDir` fallback (`append_index_html_on_directories`
// serves index.html at /); the dashboard SPA lives at /dashboard.html
// (a plain file, not a /dashboard/ prefix) so it never shadows the
// exact-match /dashboard/stream + /dashboard/history SSE routes
// registered before the ServeDir fallback.
// serves index.html at /); the dashboard SPA is that index.html.
import { build } from "esbuild";
import { mkdirSync, copyFileSync, rmSync } from "node:fs";
@ -32,13 +29,7 @@ mkdirSync(staticDir(""), { recursive: true });
// follow-up once asset sizes warrant it). esbuild writes each entry
// to `static/<name>.js` based on the entryPoint basename.
await build({
entryPoints: [
src("tabs.js"),
src("flow.js"),
src("home.js"),
src("stats.js"),
src("core.js"),
],
entryPoints: [src("tabs.js"), src("flow.js"), src("stats.js")],
outdir: staticDir(""),
bundle: true,
format: "esm",
@ -99,9 +90,7 @@ for (const entry of [
"common.css",
"dashboard.css",
"flow.css",
"home.css",
"stats.css",
"core.css",
]) {
await build({
entryPoints: [src(entry)],
@ -112,13 +101,7 @@ for (const entry of [
});
}
for (const html of [
"index.html",
"dashboard.html",
"flow.html",
"stats.html",
"core.html",
]) {
for (const html of ["index.html", "flow.html", "stats.html"]) {
copyFileSync(src(html), dist(html));
}

View file

@ -60,7 +60,7 @@ const selectionState = new Set();
//
// Rendered into #jobq-rollup-section, a sibling of #containers-section
// kept OUTSIDE that section's per-render `replaceChildren()` wipe (see
// dashboard.html) — a section that gets wiped on every container-state
// index.html) — a section that gets wiped on every container-state
// tick would defeat the component owning its own fetch lifecycle.
let jobqRollupToken = 0;
@ -587,7 +587,7 @@ function buildContainerLi(c, opts) {
export function renderContainers(s) {
const root = $("containers-section");
// #containers-section only exists on /dashboard.html. tabs.js is the
// #containers-section only exists on the dashboard. tabs.js is the
// bundle for that page only (flow.html loads flow.js instead).
// Belt-and-suspenders for any future page that adds tabs.js
// without a #containers-section — matches the

View file

@ -1,4 +1,4 @@
// The build lifecycle tabs of /dashboard.html — rebuild queue, live build
// The build lifecycle tabs of the dashboard — rebuild queue, live build
// log, meta inputs, and build log history. Not an entry point: tabs.js
// imports it, owns the tab strip and the dashboard event stream, and calls
// the exports below. Three tabs, the panes' ids matching the tab ids:
@ -168,7 +168,7 @@ function renderMetaInputs(s) {
// the generic component's.
// Split from `renderRebuildQueue` below: `replaceChildren()` only
// belongs on the *first* render, to clear the static "loading…"
// placeholder `#rebuild-queue-section` starts with in `dashboard.html` —
// placeholder `#rebuild-queue-section` starts with in `index.html` —
// Preact doesn't know that placeholder is there, so a later render
// call must NOT repeat `replaceChildren()` (it would wipe Preact's own
// tracked children out from under its diffing rather than let it

View file

@ -84,7 +84,7 @@ code {
Semantic colour variants only — shape comes from `hive-pill-sm`
(shared with the agent UI's pill/chip family, @hive/shared/pill.css),
carried directly in the markup alongside each of these classnames
(agents.js/core.js/builds.js). Used on container rows and build-log
(agents.js/kept.js/builds.js). Used on container rows and build-log
rows. */
.badge-warn {
color: var(--amber);
@ -561,12 +561,12 @@ hive-side-panel .md td {
}
/* ─── page-content gutter ─────────────────────────────────────────────
Inverted-gutter layout: the dashboard + H0M3 keep `<body>` full-bleed
and wrap their padded content in a single `.page-content` div that
Inverted-gutter layout: the dashboard keeps `<body>` full-bleed
and wraps their padded content in a single `.page-content` div that
carries the horizontal gutter. Full-width chrome (the tab strip, the
server-warnings banner, the footer) then spans edge-to-edge for free,
with no negative-margin breakout hacks. Only matches on pages that opt
in by adding the wrapper (dashboard.html, index.html); FL0W / ST4TS
in by adding the wrapper (index.html); FL0W / ST4TS
are full-bleed with their own `.<page>-main` padding. */
.page-content {
padding: 0 1.5em;
@ -590,7 +590,7 @@ hive-side-panel .md td {
by common.js (renderServerWarnings) on every page. One row per warning;
colour comes from the per-warning `level` (warn = amber, crit = red).
Stickiness lives on the `.sticky-top` wrapper, not here. The bar spans
the full width edge-to-edge; the dashboard + H0M3 are full-bleed at the
the full width edge-to-edge; the dashboard is full-bleed at the
body level (their 1.5em gutter lives on an inner `.page-content`
wrapper), so the banner is full-width for free. */
.server-warnings[hidden] {

View file

@ -1,4 +1,4 @@
// Shared dashboard helpers used by both dashboard.html (./tabs.js) and
// Shared dashboard helpers used by both index.html (./tabs.js) and
// flow.html (./flow.js): pure DOM helpers, the side-panel singleton,
// and the path-link / file-preview infrastructure for the side panel.
@ -564,7 +564,7 @@ export function appendLinkified(parent, text, refs) {
// ─── server warnings banner ──────────────────────────────────────────
// A generic top-of-page banner shown on every page (dashboard + the
// stand-alone FL0W / H0M3 pages). The backend decides what to
// stand-alone FL0W / ST4TS pages). The backend decides what to
// warn about — `/api/state.server_warnings` is a list of
// `{ kind, level, message }` — and this just renders it, coloured by
// `level` (`warn` amber / `crit` red). Adding a new system warning is a
@ -575,8 +575,8 @@ export function appendLinkified(parent, text, refs) {
// banner above the page's chrome (tab bar / page header), so the banner
// stacks with the chrome instead of being overlaid by it (two separate
// `top:0` stickies would otherwise collide). Built once by wrapping the
// page's existing chrome element; pages without a chrome (e.g. the H0M3
// hub) get a banner-only sticky region at the top of <body>.
// page's existing chrome element; pages without a chrome (none today)
// get a banner-only sticky region at the top of <body>.
function ensureStickyTop() {
let top = document.querySelector(".sticky-top");
if (top) return top;

View file

@ -1,66 +0,0 @@
/* ─── /core.html — C0R3 page ───────────────────────────────────────
Kept state (tombstones) and stale permission entries. Section rules
imported from system-sections.css (shared with dashboard.css). */
@import "./system-sections.css";
body.core-shell {
margin: 0;
padding: 0;
}
.core-main {
padding: 1.2em 1.5em 2em;
}
/* ─── K3PT ST4T3 container cards ────────────────────────────────────
core.html doesn't load dashboard.css (that's the operator SPA),
so the .container-row card styles aren't inherited. Redefine them
here so tombstone entries look like proper cards (similar to agent
cards on the main dashboard) rather than bare list items. */
.containers {
list-style: none;
padding: 0;
margin: 0.5em 0 0;
display: flex;
flex-direction: column;
gap: 0.5em;
}
.container-row {
background: var(--bg-elev);
border: 1px solid var(--border);
border-radius: 4px;
padding: 0.55em 0.8em 0.5em;
}
.container-row.tombstone {
border-style: dashed;
background: color-mix(in srgb, var(--bg-elev) 35%, transparent);
opacity: 0.85;
}
.container-row .head {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 0.5em;
margin-bottom: 0.4em;
}
.container-row .head .name {
font-size: 1.05em;
font-weight: bold;
}
.container-row.tombstone .head .name {
color: var(--muted);
}
.container-row .head .meta {
margin-left: auto;
font-size: 0.88em;
color: var(--muted);
}
.container-row .actions {
display: flex;
flex-wrap: wrap;
gap: 0.4em;
}
.container-row .actions form.inline {
display: inline-block;
margin: 0;
}

View file

@ -1,40 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>hyperhive // C0R3</title>
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="stylesheet" href="/static/colors.css" />
<link rel="stylesheet" href="/static/theme.css" />
<link rel="stylesheet" href="/static/common.css" />
<link rel="stylesheet" href="/static/core.css" />
</head>
<body class="core-shell">
<!-- C0R3: the host/coordinator surface. Standalone page (served at
/core.html) carved out of the dashboard's old SYST3M tab so the
dashboard tab strip stays lean: kept state (tombstones). Rebuild
queue + meta inputs are tabs of /dashboard.html. The section <div>
id (tombstones-section) matches what core.js's renderers target. Hive
infra containers (`hive-forge`, `hive-gateway`, `hive-matrix`) have
no dashboard panel — `hivectl stop`/`start`/`restart` is the only
control surface. -->
<header class="page-header">
<a class="page-back" href="/">← home</a>
<span class="page-title">K3PT ST4T3</span>
</header>
<main class="core-main">
<!-- tombstones-section shows destroyed agents (purge button). -->
<p class="meta">
kept state from previously tombstoned agents — recreating an agent with
the same name reuses it.
</p>
<div id="tombstones-section">
<p class="meta">loading…</p>
</div>
</main>
<script type="module" src="/static/core.js" defer></script>
</body>
</html>

View file

@ -1,156 +0,0 @@
// C0R3 page entry (/core.html). The host/coordinator surface, its own
// standalone page so the dashboard tab strip stays lean: kept state
// (tombstones).
//
// Rebuild queue + meta inputs are tabs of /dashboard.html, not here. This
// page is its own esbuild bundle with its own runtime: it cold-loads
// /api/state and subscribes to /dashboard/stream (the same broker event
// channel the dashboard uses), maintaining its own copy of the tombstones
// state.
import { $, form, openStream, initServerWarnings } from "./common.js";
import { el } from "@hive/shared/dom.js";
import { bindAsyncForms } from "@hive/shared/forms.js";
// ─── derived state (own copies; this bundle has its own runtime) ──────────
let tombstonesState = [];
function syncFromSnapshot(s) {
tombstonesState = (s.tombstones || []).slice();
}
// ─── kept state (tombstones) ──────────────────────────────────────────────
function renderTombstones(s) {
const root = $("tombstones-section");
if (!root) return;
root.replaceChildren();
if (!s.tombstones || !s.tombstones.length) {
root.append(el("p", { class: "empty" }, "no kept state — clean"));
return;
}
const fmtBytes = (n) => {
if (n < 1024) return n + " B";
if (n < 1024 * 1024) return (n / 1024).toFixed(1) + " KB";
if (n < 1024 * 1024 * 1024) return (n / (1024 * 1024)).toFixed(1) + " MB";
return (n / (1024 * 1024 * 1024)).toFixed(2) + " GB";
};
const fmtAgeDays = (ts) => {
if (!ts) return "?";
const d = Math.floor((Date.now() / 1000 - ts) / 86400);
if (d <= 0) return "today";
if (d === 1) return "1 day ago";
return d + " days ago";
};
// Only shown alongside actual rows — a caveat over an empty list is noise.
// Wording is deliberately about what the list *is* rather than what it
// isn't: nothing records a destroy, so "container absent" is the only thing
// the backend can actually tell.
const warn = el("hive-warn", { level: "warning" });
warn.append(
el("strong", {}, "shows every agent whose container is absent"),
" — not only destroyed ones. An agent part-way through being spawned " +
"looks identical here, because nothing records a destroy. Check it is " +
"really gone before you PURG3.",
);
root.append(warn);
const ul = el("ul", { class: "containers" });
for (const t of s.tombstones) {
const li = el("li", { class: "container-row tombstone" });
const head = el("div", { class: "head" });
head.append(
el("span", { class: "name" }, t.name),
// Was `destroyed`, which the backend cannot actually know — see the
// caveat above. `offline` is what the absence of a container proves.
el("span", { class: "hive-pill-sm badge-muted" }, "offline"),
);
if (t.has_creds)
head.append(
el("span", { class: "hive-pill-sm badge-muted" }, "creds kept"),
);
head.append(
el(
"span",
{ class: "meta" },
`${fmtBytes(t.state_bytes)} · ${fmtAgeDays(t.last_seen)}`,
),
);
li.append(head);
const actions = el("div", { class: "actions" });
actions.append(
form(
"/api/purge-tombstone/" + t.name,
"btn-destroy",
"PURG3",
"PURGE " +
t.name +
"? config history, claude creds, " +
"and notes are all WIPED. no undo.",
{},
{ noRefresh: true },
),
);
li.append(actions);
ul.append(li);
}
root.append(ul);
}
// ─── render-all (cold load + any full re-render) ──────────────────────────
function renderAll() {
renderTombstones({ tombstones: tombstonesState });
}
// ─── live SSE handlers ───────────────────────────────────────────────────
const SSE_HANDLERS = {
tombstones_changed(ev) {
tombstonesState = (ev.tombstones || []).slice();
renderTombstones({ tombstones: tombstonesState });
},
};
// ─── boot ─────────────────────────────────────────────────────────────────
// Re-fetch /api/state, re-sync the derived state, re-render. Used for the
// cold load and as the post-submit refresh for `data-async` forms whose
// mutation doesn't arrive via an SSE event (e.g. the meta-update / spawn /
// purge actions that opt out of `data-no-refresh`).
async function refreshState() {
try {
const resp = await fetch("/api/state");
if (resp.ok) syncFromSnapshot(await resp.json());
} catch {
// best-effort: the page keeps its last-rendered state
}
renderAll();
}
async function init() {
initServerWarnings();
// `data-async` form submit interceptor — without this the meta-update /
// cancel / respawn / purge buttons POST natively and the browser navigates
// to the bare `ok` response page.
bindAsyncForms(() => refreshState());
await refreshState();
// kinds= matches SSE_HANDLERS below verbatim — narrows the stream down
// to the kinds this page acts on. This page was one
// of 4 unfiltered `/api/dashboard/stream` subscribers before this
// (subscription discipline, part 1 of the dashboard-event-stream-
// split issue).
const es = openStream("/api/dashboard/stream?kinds=tombstones_changed");
if (es) {
es.onmessage = (e) => {
let ev;
try {
ev = JSON.parse(e.data);
} catch {
return;
}
const h = SSE_HANDLERS[ev.kind];
if (h) h(ev);
};
}
}
init();

View file

@ -3,9 +3,7 @@
component rules that this file depends on. */
/* SYST3M panel rules (rebuild queue / meta inputs / kept state)
live in system-sections.css, shared with /core.html. A few are still
referenced from the dashboard (`.rebuild-queue`), so the dashboard
pulls them in here. */
live in system-sections.css. */
@import "./system-sections.css";
@import "@hive/shared/jobq-rollup.css";
@import "@hive/shared/jobq-graph.css";
@ -29,7 +27,7 @@ body.dashboard-shell {
}
.dashboard-chrome {
/* Home back-link + tab strip share one row (flex). The chrome owns the
/* Pages menu + tab strip share one row (flex). The chrome owns the
horizontal gutter and the single full-width bottom divider; its
children drop their own side padding / border so they sit inline. */
display: flex;
@ -45,20 +43,64 @@ body.dashboard-shell {
padding: 0.3em 1em;
margin: 0 0 1em;
}
/* ← home back-link to the H0M3 hub. Small purple link top-left of the
chrome, mirroring the .page-back treatment the standalone pages use
(now shared from @hive/shared/chrome.css) so the navigation reads
consistently. The dashboard keeps this richer sticky chrome of its
own rather than folding into the simple page header. */
.dash-home-back {
/* Pages menu (links-menu.js): one icon button at the left of the chrome
that opens a popover of the standalone pages and the swarm UI. Same
shape as the swarm UI's LinksMenu — quiet until hovered or open. The
explicit `[hidden]` rule is needed because `display: flex` on the
popover would otherwise beat the UA `[hidden]` style. */
.links-menu {
position: relative;
flex: none;
}
.links-menu-button {
display: flex;
align-items: center;
justify-content: center;
width: 2em;
height: 2em;
padding: 0;
border: 1px solid transparent;
border-radius: 0.4em;
background: none;
color: var(--purple);
font-size: 1em;
line-height: 1;
cursor: pointer;
}
.links-menu-button:hover,
.links-menu-button[aria-expanded="true"] {
border-color: var(--border);
background: var(--bg-elev);
}
.links-menu-popover {
position: absolute;
top: calc(100% + 0.4em);
left: 0;
z-index: 10;
display: flex;
flex-direction: column;
min-width: 10em;
padding: 0.4em;
border: 1px solid var(--border);
border-radius: 0.5em;
background: var(--bg-elev);
box-shadow: 0 0.25em 0.75em rgba(0, 0, 0, 0.3);
}
.links-menu-popover[hidden] {
display: none;
}
.links-menu-item {
display: flex;
align-items: center;
gap: 0.5em;
padding: 0.4em 0.6em;
border-radius: 0.35em;
color: var(--fg);
text-decoration: none;
font-size: 0.82em;
white-space: nowrap;
}
.dash-home-back:hover {
text-decoration: underline;
.links-menu-item:hover {
background: var(--bg);
}
.banner-thin {
text-align: center;
@ -77,7 +119,7 @@ body.dashboard-shell {
flex-wrap: nowrap;
align-items: center;
gap: 0.2em;
/* On one row with the home link now; the chrome owns the gutter +
/* On one row with the pages menu; the chrome owns the gutter +
the bottom divider, so no outer padding / border here. */
}
/* Shares the flat-pill aesthetic of the shared `.hive-tab` (sub-tabs on
@ -443,7 +485,7 @@ hive-agent-menu {
shared <hive-warn> component — amber/no-pulse for
the standing caveat (a pulsing banner that's always present just
teaches you to stop seeing it), `severity="red" pulse` for the port
collision (an incident, not a caveat). See core.js/agents.js and
collision (an incident, not a caveat). See kept.js/agents.js and
hive-warn.css for the actual styling now. */
/* Build-queue summary banner on the 4G3NTS tab moved to the shared

View file

@ -1,260 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>hyperhive // h1ve-c0re</title>
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="stylesheet" href="/static/colors.css" />
<link rel="stylesheet" href="/static/theme.css" />
<link rel="stylesheet" href="/static/common.css" />
<link rel="stylesheet" href="/static/dashboard.css" />
</head>
<body class="dashboard-shell">
<!-- Sticky chrome — just the tab strip. The "WE ARE THE WIRED"
slug lives at the page footer below `<main>`; chrome is
navigation only. Tabs route via the URL hash so F5 / back-
button / shared links keep you on the same view. JS owns
the show/hide. -->
<header class="dashboard-chrome">
<!-- Back to the H0M3 menu hub (served at /). Sub-pages and the
dashboard all link back to the landing page rather than to each
other — H0M3 is the single navigation hub. -->
<a class="dash-home-back" href="/" title="back to the H0M3 menu"
>← home</a
>
<nav class="tabbar" id="tabbar" role="tablist">
<a
class="tab"
id="tab-agents"
href="#agents"
role="tab"
aria-controls="tab-pane-agents"
data-tab="agents"
>
<span class="tab-label">◆ 4G3NTS ◆</span>
<span class="tab-count" id="tab-count-agents" hidden></span>
</a>
<!-- SYST3M moved to its own standalone page (/core.html, "C0R3"),
reached from the H0M3 hub — keeps the dashboard tab strip lean.
The build lifecycle is three tabs here (builds.js): the rebuild
queue with its live log, the meta flake inputs, and the build log
history. -->
<a
class="tab"
id="tab-rebuild"
href="#rebuild"
role="tab"
aria-controls="tab-pane-rebuild"
data-tab="rebuild"
>
<span class="tab-label">◆ R3BU1LD QU3U3 ◆</span>
<span class="tab-count" id="tab-count-rebuild" hidden></span>
</a>
<a
class="tab"
id="tab-meta"
href="#meta"
role="tab"
aria-controls="tab-pane-meta"
data-tab="meta"
>
<span class="tab-label">◆ M3T4 1NPUTS ◆</span>
</a>
<a
class="tab"
id="tab-buildlogs"
href="#buildlogs"
role="tab"
aria-controls="tab-pane-buildlogs"
data-tab="buildlogs"
>
<span class="tab-label">◆ BUILD L0GS ◆</span>
</a>
<!-- ST4TS lives on its own page now (`/stats.html`), reached from
the H0M3 hub — the hive-wide rollup is a standalone read-only
view, not part of the operational tab strip. -->
<!-- (Peer hives are no longer a tab — they render as a headline
under 4G3NTS. See tab-pane-agents.) -->
<!-- FL0W and the other standalone pages are reachable from the
H0M3 hub (served at /), not from the dashboard tab strip —
the strip holds only in-page tabs. -->
</nav>
</header>
<!-- Tab panes. createTabStrip (@hive/shared/tabs.js) keeps exactly one
visible by toggling the `hidden` attribute, resolved from the URL
hash (default 4G3NTS). Panes start `hidden` to avoid a flash before
the script runs. -->
<!-- Padded content wrapper: carries the 1.5em side gutter
(.page-content, common.css) while <body> stays full-bleed so the
sticky chrome above + the footer below span the full width. -->
<div class="page-content">
<main class="dashboard-main">
<!-- 4G3NTS: the agents. Container cards (the central thing
the operator looks at) and rebuild queue / cascade visualisation
that drives them. No inline `C0NTAINERS` h2 heading + divider
— the tab label 4G3NTS already says it. -->
<section
class="tab-pane"
id="tab-pane-agents"
data-tab-pane="agents"
hidden
role="tabpanel"
aria-labelledby="tab-agents"
>
<!-- Swarm / hive identity headline. Populated by refreshState from
hive_name + swarm_name; stays hidden when neither is set. -->
<h2 id="swarm-identity" hidden></h2>
<!-- JobqRollup mount point, kept outside #containers-section
deliberately — that section is wiped + rebuilt on every
container-list render (see agents.js::renderContainers), which
would tear down and remount the Preact tree on every
container-state tick. Mounted once by agents.js::initJobqRollup,
refreshed via its own handle rather than by re-rendering. -->
<div id="jobq-rollup-section"></div>
<div id="containers-section">
<p class="meta">loading…</p>
</div>
</section>
<!-- R3BU1LD QU3U3: pending + running rebuilds, meta-updates, and
first-spawns. Rendered from GET /api/jobq/graph by the
`JobqGraph` Preact component (@hive/shared/jobq-graph.js);
`rebuild_queue_changed` over /api/dashboard/stream is the
refresh trigger and carries no payload of its own. -->
<section
class="tab-pane"
id="tab-pane-rebuild"
data-tab-pane="rebuild"
hidden
role="tabpanel"
aria-labelledby="tab-rebuild"
>
<p class="meta">
pending + running rebuilds, meta-updates, and first-spawns. one runs
at a time; meta-update cascades nest under their parent. dedup:
re-enqueueing a still-queued op collapses into the existing entry.
</p>
<div id="rebuild-queue-section">
<p class="meta">loading…</p>
</div>
<!-- Live build log of the currently-running rebuild (one runs at a
time). Managed by renderRebuildLiveLog in builds.js, separate from
rebuild-queue-section so the queue's row re-render never disturbs
the open poll. -->
<div id="rebuild-live-log" class="rebuild-live-log" hidden></div>
</section>
<!-- M3T4 1NPUTS: select inputs to nix flake update in /meta/. -->
<section
class="tab-pane"
id="tab-pane-meta"
data-tab-pane="meta"
hidden
role="tabpanel"
aria-labelledby="tab-meta"
>
<p class="meta">
select inputs to <code>nix flake update</code> in
<code>/meta/</code>. selected agents rebuild in sequence after the
lock bump; the submitting agent learns each outcome via the usual
<code>rebuilt</code> system event.
</p>
<div id="meta-inputs-section">
<p class="meta">loading…</p>
</div>
</section>
<!-- BUILD L0GS: all-agents build log history. Click a row to expand
stdout + stderr. Live builds stream in real time. Lazy-loaded the
first time the tab is shown; auto-refreshes when
rebuild_queue_changed fires. Deep-link: ?id=N#buildlogs. -->
<section
class="tab-pane"
id="tab-pane-buildlogs"
data-tab-pane="buildlogs"
hidden
role="tabpanel"
aria-labelledby="tab-buildlogs"
>
<p class="meta">
all-agents build log history. click a row to expand stdout + stderr.
live builds stream in real time.
</p>
<div class="logs-toolbar">
<button type="button" class="btn btn-restart" id="build-refresh">
↻ refresh
</button>
</div>
<div id="build-list"><p class="meta">loading…</p></div>
</section>
<!-- ST4TS: hive-wide turn-stats aggregate lives on its own page now
(`/stats.html`), reached from the H0M3 hub. The markup + the
hive-stats render JS moved there; when tabs.js boots on the
dashboard the renderers are simply gone (no stats tab to
activate). -->
<!-- FL0W: lives on its own page now (`/flow.html`). The
message-flow + inbox + compose DOM only exists there — when
tabs.js boots on this page the corresponding renderers
no-op silently (each guard is `if (!el) return`). -->
</main>
</div>
<!-- /.page-content -->
<footer>
<pre class="banner banner-thin">
░▒▓█▓▒░ HYPERHIVE / HIVE-C0RE / WE ARE THE WIRED ░▒▓█▓▒░</pre
>
<div class="divider">
══════════════════════════════════════════════════════════════
</div>
<p>
▲△▲
<a href="https://forge.darkest.space/hyperhive/hyperhive">hyperhive</a>
▲△▲ hive-c0re on this host ▲△▲
</p>
</footer>
<!-- Slide-in detail panel (clicked file previews, applied config)
is a <hive-side-panel> element
(@hive/shared/side-panel.js) — the Panel singleton in common.js
creates + appends it to <body> lazily on first use, so nothing
needs to be pre-declared here. -->
<!-- Selection action bar. Sticky-bottom strip that slides into
view when one or more agent cards is selected (click the icon
to toggle). See docs/web-ui/dashboard.md::Selection bar for the bulk
action gating + clear semantics. -->
<div
id="selection-bar"
class="selection-bar"
hidden
role="toolbar"
aria-label="bulk agent actions"
>
<span class="selection-count" id="selection-count"></span>
<span class="selection-names" id="selection-names"></span>
<span class="selection-actions" id="selection-actions"></span>
<button
type="button"
class="btn selection-clear"
id="selection-clear"
title="clear selection (esc)"
>
✕ clear
</button>
</div>
<!-- Single bundled entry — tabs.js is the dashboard tabs surface;
flow.html has its own flow.js bundle. esbuild folds
@hive/shared/terminal.js and the marked npm package into
tabs.js. -->
<script type="module" src="/static/tabs.js" defer></script>
</body>
</html>

View file

@ -11,12 +11,11 @@
<link rel="stylesheet" href="/static/flow.css" />
</head>
<body class="flow-shell">
<!-- Minimal chrome: just a back link to the H0M3 hub (served at /).
<!-- Minimal chrome: just a back link to the dashboard (served at /).
No full tabbar — the flow page is a dedicated full-viewport
terminal surface; navigating back to the menu is the only chrome
needed. Pages link back to H0M3, not the dashboard. -->
terminal surface; navigating back is the only chrome needed. -->
<header class="page-header">
<a class="page-back" href="/">← home</a>
<a class="page-back" href="/">← dashboard</a>
<span class="page-title">FL0W</span>
<select
id="flow-agent-filter"

View file

@ -5,7 +5,7 @@
//
// Does NOT contain the dashboard's tab renderers, mutation-event
// dispatchers, or refreshState — that's `./tabs.js`, loaded only by
// /dashboard.html. The flow page runs purely on the broker stream + an
// the dashboard. The flow page runs purely on the broker stream + an
// initial /api/state fetch (compose autocomplete needs the live
// container list).
@ -24,7 +24,7 @@ import { epochSec } from "./util.js";
// ─── local containers cache (for compose autocomplete) ──────────────────
// The compose box's @-mention completion suggests known agent names.
// /dashboard.html (tabs.js) maintains the canonical `containersState`
// the dashboard (tabs.js) maintains the canonical `containersState`
// from /api/state + SSE; here we keep a small local mirror updated
// by the same `container_state_changed` / `container_removed` events
// the dashboard would handle.
@ -261,7 +261,7 @@ import { epochSec } from "./util.js";
// Drop every other mutation kind silently — without this
// they'd fall through to the terminal module's default
// renderer and clutter the log with JSON dumps. The dashboard
// tabs handle these on /dashboard.html via tabs.js.
// tabs handle these on the dashboard via tabs.js.
_default: () => {},
},
// Re-sync the local containers cache on every SSE (re)connect.

View file

@ -1,94 +0,0 @@
/* H0M3 — the menu-hub page. Minimal portal styling: a banner
+ a responsive grid of link tiles. Colours come from theme.css (the
shared palette); base typography from common.css. When the shared
page-chrome component lands (a later refactor step) the banner + tile
styling can fold into it. */
body.home-shell {
/* Full-bleed body; the 1.5em side gutter lives on the inner
`.page-content` wrapper (common.css) so the server-warnings banner
spans full width. `padding-bottom` keeps breathing room at the foot. */
margin: 0;
padding-bottom: 2em;
min-height: 100vh;
}
/* Matrix-rain backdrop (driven by home.js). Fixed, full-viewport, dimmed,
and behind every other element (z-index below the auto-stacked content).
Decorative only — pointer-events off so it never intercepts clicks. The
low opacity keeps the menu tiles legible over it; glyph + fade colours
are themed from the stylix palette in JS. */
#matrix-rain {
position: fixed;
inset: 0;
width: 100%;
height: 100%;
z-index: -1;
opacity: 0.16;
pointer-events: none;
}
.home-header {
text-align: center;
padding: 1.5em 0 0.5em;
}
.home-header .banner {
font-size: 1.05em;
}
.home-main {
max-width: 60em;
margin: 1.5em auto 0;
}
.home-menu {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(16em, 1fr));
gap: 1em;
}
.home-tile {
display: flex;
flex-direction: column;
gap: 0.35em;
padding: 1.1em 1.2em;
border: 1px solid var(--purple-dim);
border-radius: 6px;
background: color-mix(in srgb, var(--bg-elev) 55%, transparent);
text-decoration: none;
color: var(--fg);
transition:
color 0.15s ease,
background 0.15s ease,
border-color 0.15s ease,
box-shadow 0.15s ease;
}
.home-tile:hover {
border-color: var(--purple);
background: color-mix(in srgb, var(--purple) 6%, transparent);
box-shadow: 0 -2px 14px -6px
color-mix(in srgb, var(--purple) 50%, transparent);
}
/* Icon + label share the top row of each tile; the description sits
below. The icon is decorative (aria-hidden) — the label carries the
accessible name. */
.home-tile-head {
display: flex;
align-items: center;
gap: 0.55em;
}
.home-tile-icon {
font-size: 1.25em;
line-height: 1;
flex: none;
}
.home-tile-label {
font-weight: bold;
letter-spacing: 0.08em;
color: var(--purple);
}
.home-tile-desc {
font-size: 0.85em;
color: var(--muted);
}

Some files were not shown because too many files have changed in this diff Show more