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.
This commit is contained in:
parent
265f8a47cc
commit
0622736b30
35 changed files with 661 additions and 986 deletions
|
|
@ -10,24 +10,25 @@ what you actually do here.
|
||||||
|
|
||||||
## Where things are
|
## Where things are
|
||||||
|
|
||||||
Everything starts at the **H0M3 hub**, served at `/` — a grid of tiles
|
Everything starts at the **dashboard**, served at `/`. A pages menu
|
||||||
linking to every surface (Dashboard, Flow, Logs, Stats,
|
(the `☰` button at the left of the tab strip) links to the other
|
||||||
Settings, Core). Every page links back to H0M3, so you're
|
surfaces — Flow, Stats, the API docs and the swarm UI — and each of
|
||||||
never more than one step from the hub.
|
those pages links back to the dashboard.
|
||||||
|
|
||||||
The **dashboard** itself (`/dashboard.html`) is where you'll spend most
|
The dashboard is where you'll spend most of your time. It's a single
|
||||||
of your time. It's a single page with four tabs:
|
page with five tabs:
|
||||||
|
|
||||||
- **4G3NTS** — every agent, live. This is the default tab and the one
|
- **4G3NTS** — every agent, live. This is the default tab and the one
|
||||||
you'll check most.
|
you'll check most.
|
||||||
- **R3BU1LD QU3U3**, **M3T4 1NPUTS** and **BUILD L0GS** — the build
|
- **R3BU1LD QU3U3**, **M3T4 1NPUTS** and **BUILD L0GS** — the build
|
||||||
lifecycle: the rebuild queue with a live log of whatever is building,
|
lifecycle: the rebuild queue with a live log of whatever is building,
|
||||||
the meta flake inputs you can bump, and the build log history.
|
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
|
Two things live on their own page instead, reached from the pages
|
||||||
H0M3 hub: **Flow** (`/flow.html`, the raw live message stream across
|
menu: **Flow** (`/flow.html`, the raw live message stream across the
|
||||||
the whole swarm), **Stats** (`/stats.html`, swarm-wide usage stats), and
|
whole swarm) and **Stats** (`/stats.html`, swarm-wide usage stats).
|
||||||
**Core** (`/core.html`, tombstones and container resource use).
|
|
||||||
|
|
||||||
Each agent also has its own page — a full terminal view of that
|
Each agent also has its own page — a full terminal view of that
|
||||||
agent's session, reachable by selecting its name anywhere in the
|
agent's session, reachable by selecting its name anywhere in the
|
||||||
|
|
|
||||||
|
|
@ -71,7 +71,7 @@ at a given viewport width. Two columns:
|
||||||
- **Inbox / todos pills** (`📬 inbox · N` / `📋 todos · N`): hidden
|
- **Inbox / todos pills** (`📬 inbox · N` / `📋 todos · N`): hidden
|
||||||
when empty; selecting it opens the matching flyout in the side panel.
|
when empty; selecting it opens the matching flyout in the side panel.
|
||||||
- **Links badge** (`🔗`): opens a popover listing `↑ dashboard`
|
- **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,
|
followed by this agent's `agent_links()`-sourced navigation (stats,
|
||||||
screen when VNC is enabled, forge profile + config-repo mirror when
|
screen when VNC is enabled, forge profile + config-repo mirror when
|
||||||
the agent has a forge account, any `services.hyperhive.agent.dashboardLinks`
|
the agent has a forge account, any `services.hyperhive.agent.dashboardLinks`
|
||||||
|
|
|
||||||
|
|
@ -8,29 +8,32 @@ surfaces — the agent roster, agent creation, linking external forge and
|
||||||
matrix accounts, the controller's job graph — are on the
|
matrix accounts, the controller's job graph — are on the
|
||||||
[swarm UI](../swarm/ui.md).
|
[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
|
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`,
|
one tab pane at a time. The URL hash (`#agents`, `#rebuild`, `#meta`,
|
||||||
`#buildlogs`, `#permissions`) drives which pane is
|
`#buildlogs`, `#kept`) drives which pane is
|
||||||
active; hash changes don't reload the page. FL0W and the other
|
active; hash changes don't reload the page. FL0W and ST4TS
|
||||||
standalone pages are reachable from the H0M3 hub at `/`, not
|
are standalone pages reached from the pages menu, not from the
|
||||||
from the dashboard tab strip.
|
dashboard tab strip.
|
||||||
|
|
||||||
**Chrome header** (fixed, overlays the active tab pane):
|
**Chrome header** (fixed, overlays the active tab pane):
|
||||||
- **← home back-link**: top-left of the chrome, links to the H0M3 hub
|
- **Pages menu**: a `☰` button at the left of the chrome (`links-menu.js`)
|
||||||
at `/`. Every surface links back to the hub rather than to each other.
|
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 ◆`,
|
- **Tab strip**: `◆ 4G3NTS ◆`, `◆ R3BU1LD QU3U3 ◆`, `◆ M3T4 1NPUTS ◆`,
|
||||||
`◆ BUILD L0GS ◆`. In-page tabs only —
|
`◆ BUILD L0GS ◆`, `◆ K3PT ST4T3 ◆`. In-page tabs only — FL0W / ST4TS
|
||||||
the SYST3M panels live on the
|
live on their own pages, reached from the pages menu. Count pills on
|
||||||
standalone **C0R3** page (`/core.html`), and FL0W / ST4TS
|
4G3NTS (stale containers), R3BU1LD QU3U3 (rebuilds in flight) and
|
||||||
live on their own pages too, all reachable from the
|
K3PT ST4T3 (kept-state entries).
|
||||||
H0M3 hub (not the tab strip). Count pills on 4G3NTS
|
|
||||||
(stale containers) and R3BU1LD QU3U3 (rebuilds in flight).
|
|
||||||
- **Banner-thin** (`░▒▓█▓▒░ HYPERHIVE / HIVE-C0RE / WE ARE THE WIRED ░▒▓█▓▒░`)
|
- **Banner-thin** (`░▒▓█▓▒░ HYPERHIVE / HIVE-C0RE / WE ARE THE WIRED ░▒▓█▓▒░`)
|
||||||
— sits below the tab strip.
|
— sits below the tab strip.
|
||||||
- **Server-warnings banner** — a generic, sticky top-of-page strip shown
|
- **Server-warnings banner** — a generic, sticky top-of-page strip shown
|
||||||
on **every** page (dashboard + every stand-alone page — FL0W,
|
on **every** page (the dashboard + the stand-alone FL0W
|
||||||
H0M3, C0R3, ST4TS), injected at the top
|
and ST4TS pages), injected at the top
|
||||||
of `<body>` by `renderServerWarnings` in `common.js`. Driven by
|
of `<body>` by `renderServerWarnings` in `common.js`. Driven by
|
||||||
`state.server_warnings` — a list of `{ kind, level, message }` — and
|
`state.server_warnings` — a list of `{ kind, level, message }` — and
|
||||||
coloured by `level` (`warn` amber / `crit` red). hive-c0re owns the
|
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`
|
to `<swarm> / <hive> // h1ve-c0re` once `hive_name` / `swarm_name`
|
||||||
arrive in the state snapshot.
|
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,
|
title) rather than the dashboard tab strip — it's a standalone surface,
|
||||||
not a tab pane.
|
not a tab pane.
|
||||||
|
|
||||||
|
|
@ -70,15 +73,12 @@ conflicting agent.
|
||||||
`↻ UPD4TE 4LL` button appears above the containers list when any
|
`↻ UPD4TE 4LL` button appears above the containers list when any
|
||||||
agent is stale.
|
agent is stale.
|
||||||
|
|
||||||
## C0R3 page (`/core.html`)
|
## K3PT ST4T3 tab
|
||||||
|
|
||||||
Passive / rare-interaction state, reached from the **Core** tile on the
|
Passive / rare-interaction state. `kept.js` renders it from the
|
||||||
H0M3 hub (served at
|
`/api/state` snapshot and keeps it live from the `tombstones_changed`
|
||||||
`/core.html`), with minimal chrome: a `← home` back-link and a single pane,
|
stream event. (Rebuild queue and meta inputs are dashboard tabs too —
|
||||||
**K3PT ST4T3**. The page is its own esbuild bundle (`core.js`) that
|
see below.)
|
||||||
cold-loads `/api/state` and subscribes to `/api/dashboard/stream` for
|
|
||||||
`tombstones_changed`. (Rebuild queue and meta inputs are dashboard
|
|
||||||
tabs — see below.)
|
|
||||||
|
|
||||||
Per-container CPU, memory and disk load isn't shown in the hive UI. The
|
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
|
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
|
Hive-wide turn statistics, aggregated across every agent's
|
||||||
`hyperhive-turn-stats.sqlite` for the selected window. Not a dashboard
|
`hyperhive-turn-stats.sqlite` for the selected window. Not a dashboard
|
||||||
tab — a standalone page reached from the **Stats** tile on the H0M3
|
tab — a standalone page reached from the dashboard's pages menu,
|
||||||
hub, same minimal chrome as `/flow.html`. Distinct from
|
same minimal chrome as `/flow.html`. Distinct from
|
||||||
each agent's own `/stats` page (which carries the per-agent trend
|
each agent's own `/stats` page (which carries the per-agent trend
|
||||||
charts): ST4TS is the swarm-level rollup.
|
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`)
|
## FL0W page (`/flow.html`)
|
||||||
|
|
||||||
A dedicated full-page terminal (not a tab pane — a separate 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
|
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`.
|
**MESS4GE FL0W** — live broker tail wrapped in a `.terminal-wrap`.
|
||||||
Cold load backfills the last ~200 messages from `/api/dashboard/history`;
|
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
|
`{from:"operator", to, body}` into the broker; the resulting SSE frame
|
||||||
re-renders the terminal row. The operator addresses the root agent as `@root`.
|
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
|
## Container row
|
||||||
|
|
||||||
A full-height **square agent icon** (5em, capped) on the left. The
|
A full-height **square agent icon** (5em, capped) on the left. The
|
||||||
|
|
@ -579,7 +568,7 @@ selected) before the bar appears.
|
||||||
## Dashboard endpoints
|
## Dashboard endpoints
|
||||||
|
|
||||||
Also browsable interactively at `/api/docs` (a Swagger UI, linked from
|
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
|
It's a growing supplement, not yet a full replacement for the list
|
||||||
below — some endpoints aren't in it yet.
|
below — some endpoints aren't in it yet.
|
||||||
|
|
||||||
|
|
@ -703,7 +692,7 @@ payload):
|
||||||
batch (`reminder_scheduler`).
|
batch (`reminder_scheduler`).
|
||||||
- `tombstones_changed` (seq, tombstones: `Vec<TombstoneView>`) —
|
- `tombstones_changed` (seq, tombstones: `Vec<TombstoneView>`) —
|
||||||
full snapshot of destroyed-but-state-kept agents. Emitted on
|
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
|
- `meta_inputs_changed` (seq, inputs: `Vec<MetaInput>`) — full
|
||||||
snapshot of `meta/flake.lock` inputs. The M3T4 1NPUTS tab
|
snapshot of `meta/flake.lock` inputs. The M3T4 1NPUTS tab
|
||||||
subscribes; same shape as `GET /api/meta-inputs`.
|
subscribes; same shape as `GET /api/meta-inputs`.
|
||||||
|
|
|
||||||
|
|
@ -19,11 +19,9 @@ repeated here.
|
||||||
visual noise — the base16 palette has multiple accent slots
|
visual noise — the base16 palette has multiple accent slots
|
||||||
(`--purple`, `--cyan`, `--pink`, …); use more than one where it adds
|
(`--purple`, `--cyan`, `--pink`, …); use more than one where it adds
|
||||||
distinction, not decoration for its own sake.
|
distinction, not decoration for its own sake.
|
||||||
- **Whimsy** — small, delightful touches are welcome (the per-hive
|
- **Whimsy** — small, delightful touches are welcome (the matrix-rain
|
||||||
dashboard's home-page matrix-rain background, `packages/dashboard/src/
|
background, `src/shell/MatrixRain.tsx`, is the reference example).
|
||||||
home.js`, is the reference example — currently in the dashboard
|
Whimsy still has to clear the accessibility bar below (motion,
|
||||||
package, not swarm-ui itself, but the pattern it sets applies here
|
|
||||||
too). Whimsy still has to clear the accessibility bar below (motion,
|
|
||||||
in particular). swarm-ui's own reference example: `Panel`'s optional
|
in particular). swarm-ui's own reference example: `Panel`'s optional
|
||||||
`icon` prop (`src/ui/panel/Panel.tsx`), a small emoji glyph in a
|
`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
|
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
|
**General rule, not case-by-case: every non-essential CSS animation
|
||||||
gates on `prefers-reduced-motion` and pauses when its tab/section is
|
gates on `prefers-reduced-motion` and pauses when its tab/section is
|
||||||
hidden.** The dashboard's matrix-rain background (`packages/dashboard/
|
hidden.** The matrix-rain background (`src/shell/MatrixRain.tsx`) is the
|
||||||
src/home.js`/`.css`) is the existing reference implementation of this
|
existing reference implementation of this pattern — it does both today,
|
||||||
pattern — genuinely does both today, confirmed by reading it — even
|
confirmed by reading it; every future swarm-ui animation follows the
|
||||||
though it lives in the dashboard package rather than swarm-ui; every
|
same pattern, not just whimsy pieces.
|
||||||
future swarm-ui animation follows the same pattern, not just whimsy
|
|
||||||
pieces.
|
|
||||||
|
|
||||||
<!-- vale write-good.Passive = YES -->
|
<!-- vale write-good.Passive = YES -->
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,8 +19,8 @@
|
||||||
from `@hive/shared` (was `hive-fr0nt`); the dashboard ships one
|
from `@hive/shared` (was `hive-fr0nt`); the dashboard ships one
|
||||||
CSS bundle per page (`colors.css` + `theme.css` + `common.css`,
|
CSS bundle per page (`colors.css` + `theme.css` + `common.css`,
|
||||||
loaded by every page, plus a page-specific bundle —
|
loaded by every page, plus a page-specific bundle —
|
||||||
`dashboard.css` / `flow.css` / `home.css` /
|
`dashboard.css` / `flow.css` /
|
||||||
`stats.css` / `core.css`);
|
`stats.css`);
|
||||||
`common.css` inlines `@hive/shared`'s
|
`common.css` inlines `@hive/shared`'s
|
||||||
`base.css` + `terminal.css` + `tabs.css` + `chrome.css` +
|
`base.css` + `terminal.css` + `tabs.css` + `chrome.css` +
|
||||||
`pill.css` via esbuild's `@import` resolution.
|
`pill.css` via esbuild's `@import` resolution.
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ export interface MetaNavProps {
|
||||||
links: AgentLink[];
|
links: AgentLink[];
|
||||||
forgePublicUrl: string | null;
|
forgePublicUrl: string | null;
|
||||||
/** Absolute base URL of the host dashboard (`resolveDashboardBase`)
|
/** Absolute base URL of the host dashboard (`resolveDashboardBase`)
|
||||||
* — the `🧭 dashboard` item links to `${dashboardBase}dashboard.html`. */
|
* — the `🧭 dashboard` item links to `${dashboardBase}`. */
|
||||||
dashboardBase: string;
|
dashboardBase: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -86,7 +86,7 @@ export function MetaNav({
|
||||||
<div class="meta-nav-popover" role="menu" aria-label="agent links">
|
<div class="meta-nav-popover" role="menu" aria-label="agent links">
|
||||||
<a
|
<a
|
||||||
class="meta-nav-item"
|
class="meta-nav-item"
|
||||||
href={`${dashboardBase}dashboard.html`}
|
href={dashboardBase}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener"
|
rel="noopener"
|
||||||
role="menuitem"
|
role="menuitem"
|
||||||
|
|
|
||||||
|
|
@ -507,15 +507,10 @@ window.Chart = Chart;
|
||||||
// When accessed via hive-gateway the page lives at `/agent/<name>/`
|
// When accessed via hive-gateway the page lives at `/agent/<name>/`
|
||||||
// on the same origin as the dashboard. Detect via path prefix
|
// on the same origin as the dashboard. Detect via path prefix
|
||||||
// rather than the direct port (which is unreachable or wrong scheme
|
// rather than the direct port (which is unreachable or wrong scheme
|
||||||
// behind HTTPS TLS termination). The dashboard SPA lives at
|
// behind HTTPS TLS termination). The dashboard SPA is the `/` root.
|
||||||
// `dashboard.html` — the `/` root now serves the H0M3 menu hub.
|
|
||||||
dl.href = window.location.pathname.startsWith("/agent/")
|
dl.href = window.location.pathname.startsWith("/agent/")
|
||||||
? window.location.origin + "/dashboard.html"
|
? window.location.origin + "/"
|
||||||
: "http://" +
|
: "http://" + window.location.hostname + ":" + s.dashboard_port + "/";
|
||||||
window.location.hostname +
|
|
||||||
":" +
|
|
||||||
s.dashboard_port +
|
|
||||||
"/dashboard.html";
|
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
/* non-fatal */
|
/* non-fatal */
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
// esbuild build for @hive/dashboard: one JS+CSS bundle per page (H0M3 at
|
// esbuild build for @hive/dashboard: one JS+CSS bundle per page (the
|
||||||
// `/`, dashboard/flow/core/stats at their own
|
// dashboard at `/`, flow/stats at their own
|
||||||
// `.html`), each named after its own entry point below — see the
|
// `.html`), each named after its own entry point below — see the
|
||||||
// `entryPoints`/`for` lists for the exact map, not repeated here to
|
// `entryPoints`/`for` lists for the exact map, not repeated here to
|
||||||
// avoid this comment drifting out of sync with the real build steps.
|
// 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
|
// module into each bundle rather than emitting a shared chunk (no
|
||||||
// `splitting: true`). The Rust binary mounts `dist/` as a
|
// `splitting: true`). The Rust binary mounts `dist/` as a
|
||||||
// `tower_http::ServeDir` fallback (`append_index_html_on_directories`
|
// `tower_http::ServeDir` fallback (`append_index_html_on_directories`
|
||||||
// serves index.html at /); the dashboard SPA lives at /dashboard.html
|
// serves index.html at /); the dashboard SPA is that index.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.
|
|
||||||
|
|
||||||
import { build } from "esbuild";
|
import { build } from "esbuild";
|
||||||
import { mkdirSync, copyFileSync, rmSync } from "node:fs";
|
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
|
// follow-up once asset sizes warrant it). esbuild writes each entry
|
||||||
// to `static/<name>.js` based on the entryPoint basename.
|
// to `static/<name>.js` based on the entryPoint basename.
|
||||||
await build({
|
await build({
|
||||||
entryPoints: [
|
entryPoints: [src("tabs.js"), src("flow.js"), src("stats.js")],
|
||||||
src("tabs.js"),
|
|
||||||
src("flow.js"),
|
|
||||||
src("home.js"),
|
|
||||||
src("stats.js"),
|
|
||||||
src("core.js"),
|
|
||||||
],
|
|
||||||
outdir: staticDir(""),
|
outdir: staticDir(""),
|
||||||
bundle: true,
|
bundle: true,
|
||||||
format: "esm",
|
format: "esm",
|
||||||
|
|
@ -99,9 +90,7 @@ for (const entry of [
|
||||||
"common.css",
|
"common.css",
|
||||||
"dashboard.css",
|
"dashboard.css",
|
||||||
"flow.css",
|
"flow.css",
|
||||||
"home.css",
|
|
||||||
"stats.css",
|
"stats.css",
|
||||||
"core.css",
|
|
||||||
]) {
|
]) {
|
||||||
await build({
|
await build({
|
||||||
entryPoints: [src(entry)],
|
entryPoints: [src(entry)],
|
||||||
|
|
@ -112,13 +101,7 @@ for (const entry of [
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const html of [
|
for (const html of ["index.html", "flow.html", "stats.html"]) {
|
||||||
"index.html",
|
|
||||||
"dashboard.html",
|
|
||||||
"flow.html",
|
|
||||||
"stats.html",
|
|
||||||
"core.html",
|
|
||||||
]) {
|
|
||||||
copyFileSync(src(html), dist(html));
|
copyFileSync(src(html), dist(html));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -60,7 +60,7 @@ const selectionState = new Set();
|
||||||
//
|
//
|
||||||
// Rendered into #jobq-rollup-section, a sibling of #containers-section
|
// Rendered into #jobq-rollup-section, a sibling of #containers-section
|
||||||
// kept OUTSIDE that section's per-render `replaceChildren()` wipe (see
|
// 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.
|
// tick would defeat the component owning its own fetch lifecycle.
|
||||||
let jobqRollupToken = 0;
|
let jobqRollupToken = 0;
|
||||||
|
|
||||||
|
|
@ -587,7 +587,7 @@ function buildContainerLi(c, opts) {
|
||||||
|
|
||||||
export function renderContainers(s) {
|
export function renderContainers(s) {
|
||||||
const root = $("containers-section");
|
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).
|
// bundle for that page only (flow.html loads flow.js instead).
|
||||||
// Belt-and-suspenders for any future page that adds tabs.js
|
// Belt-and-suspenders for any future page that adds tabs.js
|
||||||
// without a #containers-section — matches the
|
// without a #containers-section — matches the
|
||||||
|
|
|
||||||
|
|
@ -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
|
// 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
|
// 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:
|
// the exports below. Three tabs, the panes' ids matching the tab ids:
|
||||||
|
|
@ -168,7 +168,7 @@ function renderMetaInputs(s) {
|
||||||
// the generic component's.
|
// the generic component's.
|
||||||
// Split from `renderRebuildQueue` below: `replaceChildren()` only
|
// Split from `renderRebuildQueue` below: `replaceChildren()` only
|
||||||
// belongs on the *first* render, to clear the static "loading…"
|
// 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
|
// Preact doesn't know that placeholder is there, so a later render
|
||||||
// call must NOT repeat `replaceChildren()` (it would wipe Preact's own
|
// call must NOT repeat `replaceChildren()` (it would wipe Preact's own
|
||||||
// tracked children out from under its diffing rather than let it
|
// tracked children out from under its diffing rather than let it
|
||||||
|
|
|
||||||
|
|
@ -84,7 +84,7 @@ code {
|
||||||
Semantic colour variants only — shape comes from `hive-pill-sm`
|
Semantic colour variants only — shape comes from `hive-pill-sm`
|
||||||
(shared with the agent UI's pill/chip family, @hive/shared/pill.css),
|
(shared with the agent UI's pill/chip family, @hive/shared/pill.css),
|
||||||
carried directly in the markup alongside each of these classnames
|
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. */
|
rows. */
|
||||||
.badge-warn {
|
.badge-warn {
|
||||||
color: var(--amber);
|
color: var(--amber);
|
||||||
|
|
@ -561,12 +561,12 @@ hive-side-panel .md td {
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ─── page-content gutter ─────────────────────────────────────────────
|
/* ─── page-content gutter ─────────────────────────────────────────────
|
||||||
Inverted-gutter layout: the dashboard + H0M3 keep `<body>` full-bleed
|
Inverted-gutter layout: the dashboard keeps `<body>` full-bleed
|
||||||
and wrap their padded content in a single `.page-content` div that
|
and wraps their padded content in a single `.page-content` div that
|
||||||
carries the horizontal gutter. Full-width chrome (the tab strip, the
|
carries the horizontal gutter. Full-width chrome (the tab strip, the
|
||||||
server-warnings banner, the footer) then spans edge-to-edge for free,
|
server-warnings banner, the footer) then spans edge-to-edge for free,
|
||||||
with no negative-margin breakout hacks. Only matches on pages that opt
|
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. */
|
are full-bleed with their own `.<page>-main` padding. */
|
||||||
.page-content {
|
.page-content {
|
||||||
padding: 0 1.5em;
|
padding: 0 1.5em;
|
||||||
|
|
@ -590,7 +590,7 @@ hive-side-panel .md td {
|
||||||
by common.js (renderServerWarnings) on every page. One row per warning;
|
by common.js (renderServerWarnings) on every page. One row per warning;
|
||||||
colour comes from the per-warning `level` (warn = amber, crit = red).
|
colour comes from the per-warning `level` (warn = amber, crit = red).
|
||||||
Stickiness lives on the `.sticky-top` wrapper, not here. The bar spans
|
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`
|
body level (their 1.5em gutter lives on an inner `.page-content`
|
||||||
wrapper), so the banner is full-width for free. */
|
wrapper), so the banner is full-width for free. */
|
||||||
.server-warnings[hidden] {
|
.server-warnings[hidden] {
|
||||||
|
|
|
||||||
|
|
@ -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,
|
// flow.html (./flow.js): pure DOM helpers, the side-panel singleton,
|
||||||
// and the path-link / file-preview infrastructure for the side panel.
|
// and the path-link / file-preview infrastructure for the side panel.
|
||||||
|
|
||||||
|
|
@ -564,7 +564,7 @@ export function appendLinkified(parent, text, refs) {
|
||||||
|
|
||||||
// ─── server warnings banner ──────────────────────────────────────────
|
// ─── server warnings banner ──────────────────────────────────────────
|
||||||
// A generic top-of-page banner shown on every page (dashboard + the
|
// 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
|
// warn about — `/api/state.server_warnings` is a list of
|
||||||
// `{ kind, level, message }` — and this just renders it, coloured by
|
// `{ kind, level, message }` — and this just renders it, coloured by
|
||||||
// `level` (`warn` amber / `crit` red). Adding a new system warning is a
|
// `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
|
// 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
|
// stacks with the chrome instead of being overlaid by it (two separate
|
||||||
// `top:0` stickies would otherwise collide). Built once by wrapping the
|
// `top:0` stickies would otherwise collide). Built once by wrapping the
|
||||||
// page's existing chrome element; pages without a chrome (e.g. the H0M3
|
// page's existing chrome element; pages without a chrome (none today)
|
||||||
// hub) get a banner-only sticky region at the top of <body>.
|
// get a banner-only sticky region at the top of <body>.
|
||||||
function ensureStickyTop() {
|
function ensureStickyTop() {
|
||||||
let top = document.querySelector(".sticky-top");
|
let top = document.querySelector(".sticky-top");
|
||||||
if (top) return top;
|
if (top) return top;
|
||||||
|
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
@ -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>
|
|
||||||
|
|
@ -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();
|
|
||||||
|
|
@ -3,9 +3,7 @@
|
||||||
component rules that this file depends on. */
|
component rules that this file depends on. */
|
||||||
|
|
||||||
/* SYST3M panel rules (rebuild queue / meta inputs / kept state)
|
/* SYST3M panel rules (rebuild queue / meta inputs / kept state)
|
||||||
live in system-sections.css, shared with /core.html. A few are still
|
live in system-sections.css. */
|
||||||
referenced from the dashboard (`.rebuild-queue`), so the dashboard
|
|
||||||
pulls them in here. */
|
|
||||||
@import "./system-sections.css";
|
@import "./system-sections.css";
|
||||||
@import "@hive/shared/jobq-rollup.css";
|
@import "@hive/shared/jobq-rollup.css";
|
||||||
@import "@hive/shared/jobq-graph.css";
|
@import "@hive/shared/jobq-graph.css";
|
||||||
|
|
@ -29,7 +27,7 @@ body.dashboard-shell {
|
||||||
}
|
}
|
||||||
|
|
||||||
.dashboard-chrome {
|
.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
|
horizontal gutter and the single full-width bottom divider; its
|
||||||
children drop their own side padding / border so they sit inline. */
|
children drop their own side padding / border so they sit inline. */
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
@ -45,20 +43,64 @@ body.dashboard-shell {
|
||||||
padding: 0.3em 1em;
|
padding: 0.3em 1em;
|
||||||
margin: 0 0 1em;
|
margin: 0 0 1em;
|
||||||
}
|
}
|
||||||
/* ← home back-link to the H0M3 hub. Small purple link top-left of the
|
/* Pages menu (links-menu.js): one icon button at the left of the chrome
|
||||||
chrome, mirroring the .page-back treatment the standalone pages use
|
that opens a popover of the standalone pages and the swarm UI. Same
|
||||||
(now shared from @hive/shared/chrome.css) so the navigation reads
|
shape as the swarm UI's LinksMenu — quiet until hovered or open. The
|
||||||
consistently. The dashboard keeps this richer sticky chrome of its
|
explicit `[hidden]` rule is needed because `display: flex` on the
|
||||||
own rather than folding into the simple page header. */
|
popover would otherwise beat the UA `[hidden]` style. */
|
||||||
.dash-home-back {
|
.links-menu {
|
||||||
|
position: relative;
|
||||||
flex: none;
|
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);
|
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;
|
text-decoration: none;
|
||||||
font-size: 0.82em;
|
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
.dash-home-back:hover {
|
.links-menu-item:hover {
|
||||||
text-decoration: underline;
|
background: var(--bg);
|
||||||
}
|
}
|
||||||
.banner-thin {
|
.banner-thin {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
@ -77,7 +119,7 @@ body.dashboard-shell {
|
||||||
flex-wrap: nowrap;
|
flex-wrap: nowrap;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.2em;
|
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. */
|
the bottom divider, so no outer padding / border here. */
|
||||||
}
|
}
|
||||||
/* Shares the flat-pill aesthetic of the shared `.hive-tab` (sub-tabs on
|
/* Shares the flat-pill aesthetic of the shared `.hive-tab` (sub-tabs on
|
||||||
|
|
|
||||||
|
|
@ -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>
|
|
||||||
|
|
@ -11,12 +11,11 @@
|
||||||
<link rel="stylesheet" href="/static/flow.css" />
|
<link rel="stylesheet" href="/static/flow.css" />
|
||||||
</head>
|
</head>
|
||||||
<body class="flow-shell">
|
<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
|
No full tabbar — the flow page is a dedicated full-viewport
|
||||||
terminal surface; navigating back to the menu is the only chrome
|
terminal surface; navigating back is the only chrome needed. -->
|
||||||
needed. Pages link back to H0M3, not the dashboard. -->
|
|
||||||
<header class="page-header">
|
<header class="page-header">
|
||||||
<a class="page-back" href="/">← home</a>
|
<a class="page-back" href="/">← dashboard</a>
|
||||||
<span class="page-title">FL0W</span>
|
<span class="page-title">FL0W</span>
|
||||||
<select
|
<select
|
||||||
id="flow-agent-filter"
|
id="flow-agent-filter"
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
//
|
//
|
||||||
// Does NOT contain the dashboard's tab renderers, mutation-event
|
// Does NOT contain the dashboard's tab renderers, mutation-event
|
||||||
// dispatchers, or refreshState — that's `./tabs.js`, loaded only by
|
// 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
|
// initial /api/state fetch (compose autocomplete needs the live
|
||||||
// container list).
|
// container list).
|
||||||
|
|
||||||
|
|
@ -24,7 +24,7 @@ import { epochSec } from "./util.js";
|
||||||
|
|
||||||
// ─── local containers cache (for compose autocomplete) ──────────────────
|
// ─── local containers cache (for compose autocomplete) ──────────────────
|
||||||
// The compose box's @-mention completion suggests known agent names.
|
// 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
|
// from /api/state + SSE; here we keep a small local mirror updated
|
||||||
// by the same `container_state_changed` / `container_removed` events
|
// by the same `container_state_changed` / `container_removed` events
|
||||||
// the dashboard would handle.
|
// the dashboard would handle.
|
||||||
|
|
@ -261,7 +261,7 @@ import { epochSec } from "./util.js";
|
||||||
// Drop every other mutation kind silently — without this
|
// Drop every other mutation kind silently — without this
|
||||||
// they'd fall through to the terminal module's default
|
// they'd fall through to the terminal module's default
|
||||||
// renderer and clutter the log with JSON dumps. The dashboard
|
// 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: () => {},
|
_default: () => {},
|
||||||
},
|
},
|
||||||
// Re-sync the local containers cache on every SSE (re)connect.
|
// Re-sync the local containers cache on every SSE (re)connect.
|
||||||
|
|
|
||||||
|
|
@ -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);
|
|
||||||
}
|
|
||||||
|
|
@ -1,145 +0,0 @@
|
||||||
// H0M3 page script. The page is static markup; this only does
|
|
||||||
// a few small things off a single `/api/state` read:
|
|
||||||
// 1. fill the swarm/hive identity line when configured;
|
|
||||||
// 2. render the shared server-warnings banner (top of every page).
|
|
||||||
// No SSE — a portal doesn't need live updates.
|
|
||||||
//
|
|
||||||
// It also drives the decorative matrix-rain backdrop (#matrix-rain) —
|
|
||||||
// see startMatrixRain() at the foot.
|
|
||||||
|
|
||||||
import { renderServerWarnings } from "./common.js";
|
|
||||||
|
|
||||||
const $ = (id) => document.getElementById(id);
|
|
||||||
|
|
||||||
async function init() {
|
|
||||||
let state;
|
|
||||||
try {
|
|
||||||
const resp = await fetch("/api/state");
|
|
||||||
if (!resp.ok) return;
|
|
||||||
state = await resp.json();
|
|
||||||
} catch {
|
|
||||||
return; // best-effort: the tiles still work without it
|
|
||||||
}
|
|
||||||
|
|
||||||
renderServerWarnings(state.server_warnings);
|
|
||||||
|
|
||||||
const ident = $("hive-identity");
|
|
||||||
if (ident && (state.swarm_name || state.hive_name)) {
|
|
||||||
const parts = [state.swarm_name, state.hive_name].filter(Boolean);
|
|
||||||
ident.textContent = parts.join(" / ");
|
|
||||||
ident.hidden = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Rev line: absent when the flake ref isn't a local path pin (a bare
|
|
||||||
// `github:` url has no on-disk rev to canonicalize) — hidden rather
|
|
||||||
// than showing a placeholder in that case.
|
|
||||||
//
|
|
||||||
// `current_flake_rev` canonicalizes to a nix store path
|
|
||||||
// (/nix/store/<hash>-<pname>) — the part that actually varies between
|
|
||||||
// builds is the hash right after `/nix/store/`, not the trailing
|
|
||||||
// `-<pname>` (constant across revs). Slice the hash out of the front
|
|
||||||
// rather than truncating from the tail, so two different builds don't
|
|
||||||
// render as the same string. Falls back to a plain head-slice for a
|
|
||||||
// non-store path (e.g. a bare local dir during dev).
|
|
||||||
const rev = $("hive-rev");
|
|
||||||
if (rev && state.hyperhive_rev) {
|
|
||||||
const storeMatch = state.hyperhive_rev.match(/^\/nix\/store\/([^-]+)/);
|
|
||||||
const short = storeMatch
|
|
||||||
? storeMatch[1].slice(0, 12)
|
|
||||||
: state.hyperhive_rev.slice(0, 12);
|
|
||||||
rev.textContent = `rev ${short}`;
|
|
||||||
rev.title = state.hyperhive_rev;
|
|
||||||
rev.hidden = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
init();
|
|
||||||
|
|
||||||
// ── matrix-rain backdrop ─────────────────────────────────────────────
|
|
||||||
// Classic falling-glyph "digital rain" on the #matrix-rain canvas, purely
|
|
||||||
// decorative. Colours are resolved from the live stylix palette (--green
|
|
||||||
// glyphs, --bg fade-trail) so a theme swap re-colours it; the CSS already
|
|
||||||
// dims it + parks it behind the content. Stepped via setInterval (the
|
|
||||||
// steppy cadence is the look and costs little CPU); paused while the tab is
|
|
||||||
// hidden, and skipped entirely under prefers-reduced-motion.
|
|
||||||
function startMatrixRain() {
|
|
||||||
const canvas = $("matrix-rain");
|
|
||||||
if (!canvas || !canvas.getContext) return;
|
|
||||||
if (
|
|
||||||
window.matchMedia &&
|
|
||||||
window.matchMedia("(prefers-reduced-motion: reduce)").matches
|
|
||||||
)
|
|
||||||
return;
|
|
||||||
const ctx = canvas.getContext("2d");
|
|
||||||
|
|
||||||
// getComputedStyle on a custom property returns its *declared* value
|
|
||||||
// (e.g. "var(--base0B)") unresolved, so resolve to a concrete rgb() by
|
|
||||||
// probing a real `color` computation instead.
|
|
||||||
function resolveColor(varName, fallback) {
|
|
||||||
const probe = document.createElement("span");
|
|
||||||
probe.style.color = `var(${varName})`;
|
|
||||||
probe.style.display = "none";
|
|
||||||
document.body.appendChild(probe);
|
|
||||||
const c = getComputedStyle(probe).color;
|
|
||||||
probe.remove();
|
|
||||||
return /^rgb/.test(c) ? c : fallback;
|
|
||||||
}
|
|
||||||
const glyphColor = resolveColor("--green", "rgb(166, 227, 161)");
|
|
||||||
// Fade-trail = the bg at low alpha. Pull r,g,b numerically rather than
|
|
||||||
// string-rewriting the rgb() — robust to whatever rgb()/rgba() spacing
|
|
||||||
// getComputedStyle hands back, so the trail always fades (never fills
|
|
||||||
// solid) even if the format shifts.
|
|
||||||
const bgRGB = resolveColor("--bg", "rgb(30, 30, 46)").match(
|
|
||||||
/(\d+)[^\d]+(\d+)[^\d]+(\d+)/,
|
|
||||||
);
|
|
||||||
const fadeColor = bgRGB
|
|
||||||
? `rgba(${bgRGB[1]}, ${bgRGB[2]}, ${bgRGB[3]}, 0.09)`
|
|
||||||
: "rgba(30, 30, 46, 0.09)";
|
|
||||||
|
|
||||||
const GLYPHS = "アイウエオカキクケコサシスセソタチツテト0123456789:=*+-<>¦";
|
|
||||||
const CELL = 16; // glyph cell size (px)
|
|
||||||
let cols = 0;
|
|
||||||
let drops = [];
|
|
||||||
|
|
||||||
function resize() {
|
|
||||||
canvas.width = canvas.offsetWidth;
|
|
||||||
canvas.height = canvas.offsetHeight;
|
|
||||||
cols = Math.max(1, Math.ceil(canvas.width / CELL));
|
|
||||||
drops = Array.from({ length: cols }, () => Math.floor(Math.random() * -40));
|
|
||||||
ctx.font = `${CELL}px monospace`; // canvas reset on resize clears this
|
|
||||||
}
|
|
||||||
|
|
||||||
function tick() {
|
|
||||||
// Translucent bg wash leaves a fading trail behind each glyph.
|
|
||||||
ctx.fillStyle = fadeColor;
|
|
||||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
||||||
ctx.fillStyle = glyphColor;
|
|
||||||
for (let i = 0; i < cols; i++) {
|
|
||||||
const ch = GLYPHS[(Math.random() * GLYPHS.length) | 0];
|
|
||||||
ctx.fillText(ch, i * CELL, drops[i] * CELL);
|
|
||||||
if (drops[i] * CELL > canvas.height && Math.random() > 0.975)
|
|
||||||
drops[i] = 0;
|
|
||||||
drops[i]++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let timer = null;
|
|
||||||
const play = () => {
|
|
||||||
if (!timer) timer = setInterval(tick, 55);
|
|
||||||
};
|
|
||||||
const pause = () => {
|
|
||||||
if (timer) {
|
|
||||||
clearInterval(timer);
|
|
||||||
timer = null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
resize();
|
|
||||||
window.addEventListener("resize", resize);
|
|
||||||
document.addEventListener("visibilitychange", () =>
|
|
||||||
document.hidden ? pause() : play(),
|
|
||||||
);
|
|
||||||
play();
|
|
||||||
}
|
|
||||||
|
|
||||||
startMatrixRain();
|
|
||||||
|
|
@ -3,90 +3,277 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<title>hyperhive // h0m3</title>
|
<title>hyperhive // h1ve-c0re</title>
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
<link rel="stylesheet" href="/static/colors.css" />
|
<link rel="stylesheet" href="/static/colors.css" />
|
||||||
<link rel="stylesheet" href="/static/theme.css" />
|
<link rel="stylesheet" href="/static/theme.css" />
|
||||||
<link rel="stylesheet" href="/static/common.css" />
|
<link rel="stylesheet" href="/static/common.css" />
|
||||||
<link rel="stylesheet" href="/static/home.css" />
|
<link rel="stylesheet" href="/static/dashboard.css" />
|
||||||
</head>
|
</head>
|
||||||
<body class="home-shell">
|
<body class="dashboard-shell">
|
||||||
<!-- Decorative matrix-rain backdrop: a dimmed full-viewport canvas of
|
<!-- Sticky chrome — just the tab strip. The "WE ARE THE WIRED"
|
||||||
falling glyphs behind everything (home.js drives it). aria-hidden +
|
slug lives at the page footer below `<main>`; chrome is
|
||||||
pointer-events:none so it's purely cosmetic; glyph/bg colours are
|
navigation only. Tabs route via the URL hash so F5 / back-
|
||||||
read from the stylix palette at runtime so a theme swap re-colours
|
button / shared links keep you on the same view. JS owns
|
||||||
it, and it's disabled under prefers-reduced-motion. -->
|
the show/hide. -->
|
||||||
<canvas id="matrix-rain" aria-hidden="true"></canvas>
|
<header class="dashboard-chrome">
|
||||||
|
<!-- Pages menu: the standalone pages (flow, stats, API docs) and the
|
||||||
|
swarm UI. Filled by links-menu.js from /api/state. The pages link
|
||||||
|
back to `/`, which is this dashboard. -->
|
||||||
|
<div class="links-menu" id="links-menu"></div>
|
||||||
|
<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>
|
||||||
|
<!-- 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. Kept state (kept.js) is the fourth. -->
|
||||||
|
<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>
|
||||||
|
<a
|
||||||
|
class="tab"
|
||||||
|
id="tab-kept"
|
||||||
|
href="#kept"
|
||||||
|
role="tab"
|
||||||
|
aria-controls="tab-pane-kept"
|
||||||
|
data-tab="kept"
|
||||||
|
>
|
||||||
|
<span class="tab-label">◆ K3PT ST4T3 ◆</span>
|
||||||
|
<span class="tab-count" id="tab-count-kept" hidden></span>
|
||||||
|
</a>
|
||||||
|
|
||||||
<!-- H0M3: the menu hub. A plain grid of links to every top-level
|
<!-- (Peer hives are no longer a tab — they render as a headline
|
||||||
surface. This is the page served at `/` — the landing page — with
|
under 4G3NTS. See tab-pane-agents.) -->
|
||||||
the dashboard relocated to /dashboard.html. No tabbar / SSE —
|
|
||||||
it's a static portal. -->
|
<!-- FL0W and ST4TS are standalone pages reached from the pages
|
||||||
|
menu at the left of this strip, not tabs — 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
|
<!-- Padded content wrapper: carries the 1.5em side gutter
|
||||||
(.page-content, common.css) while <body> stays full-bleed so the
|
(.page-content, common.css) while <body> stays full-bleed so the
|
||||||
server-warnings banner spans the full width. -->
|
sticky chrome above + the footer below span the full width. -->
|
||||||
<div class="page-content">
|
<div class="page-content">
|
||||||
<header class="home-header">
|
<main class="dashboard-main">
|
||||||
<p class="banner-thin" id="hive-identity" hidden></p>
|
<!-- 4G3NTS: the agents. Container cards (the central thing
|
||||||
<pre class="banner">
|
the operator looks at) and rebuild queue / cascade visualisation
|
||||||
░▒▓█▓▒░ ░▒▓█▓▒░ H Y P E R H I V E · H0M3 ░▒▓█▓▒░ ░▒▓█▓▒░</pre
|
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"
|
||||||
>
|
>
|
||||||
<p class="banner-thin" id="hive-rev" hidden></p>
|
<!-- Swarm / hive identity headline. Populated by refreshState from
|
||||||
</header>
|
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>
|
||||||
|
|
||||||
<main class="home-main">
|
<!-- R3BU1LD QU3U3: pending + running rebuilds, meta-updates, and
|
||||||
<nav class="home-menu" aria-label="hyperhive surfaces">
|
first-spawns. Rendered from GET /api/jobq/graph by the
|
||||||
<a class="home-tile" href="/dashboard.html">
|
`JobqGraph` Preact component (@hive/shared/jobq-graph.js);
|
||||||
<span class="home-tile-head">
|
`rebuild_queue_changed` over /api/dashboard/stream is the
|
||||||
<span class="home-tile-icon" aria-hidden="true">🖥</span>
|
refresh trigger and carries no payload of its own. -->
|
||||||
<span class="home-tile-label">Dashboard</span>
|
<section
|
||||||
</span>
|
class="tab-pane"
|
||||||
<span class="home-tile-desc">containers · builds</span>
|
id="tab-pane-rebuild"
|
||||||
</a>
|
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>
|
||||||
|
|
||||||
<a class="home-tile" href="/flow.html">
|
<!-- M3T4 1NPUTS: select inputs to nix flake update in /meta/. -->
|
||||||
<span class="home-tile-head">
|
<section
|
||||||
<span class="home-tile-icon" aria-hidden="true">📡</span>
|
class="tab-pane"
|
||||||
<span class="home-tile-label">Flow</span>
|
id="tab-pane-meta"
|
||||||
</span>
|
data-tab-pane="meta"
|
||||||
<span class="home-tile-desc">live all-agents message firehose</span>
|
hidden
|
||||||
</a>
|
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>
|
||||||
|
|
||||||
<a class="home-tile" href="/stats.html">
|
<!-- BUILD L0GS: all-agents build log history. Click a row to expand
|
||||||
<span class="home-tile-head">
|
stdout + stderr. Live builds stream in real time. Lazy-loaded the
|
||||||
<span class="home-tile-icon" aria-hidden="true">📊</span>
|
first time the tab is shown; auto-refreshes when
|
||||||
<span class="home-tile-label">Stats</span>
|
rebuild_queue_changed fires. Deep-link: ?id=N#buildlogs. -->
|
||||||
</span>
|
<section
|
||||||
<span class="home-tile-desc"
|
class="tab-pane"
|
||||||
>hive-wide turn stats · cost · model mix</span
|
id="tab-pane-buildlogs"
|
||||||
>
|
data-tab-pane="buildlogs"
|
||||||
</a>
|
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>
|
||||||
|
|
||||||
<a class="home-tile" href="/core.html">
|
<!-- K3PT ST4T3: kept state from agents whose container is absent,
|
||||||
<span class="home-tile-head">
|
with a purge button per row. Rendered by kept.js; refreshed by the
|
||||||
<span class="home-tile-icon" aria-hidden="true">⬡</span>
|
`tombstones_changed` stream event. -->
|
||||||
<span class="home-tile-label">Core</span>
|
<section
|
||||||
</span>
|
class="tab-pane"
|
||||||
<span class="home-tile-desc">kept state</span>
|
id="tab-pane-kept"
|
||||||
</a>
|
data-tab-pane="kept"
|
||||||
|
hidden
|
||||||
|
role="tabpanel"
|
||||||
|
aria-labelledby="tab-kept"
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
</section>
|
||||||
|
|
||||||
<!-- API tile: the OpenAPI spec + Swagger UI are always served by
|
<!-- ST4TS and FL0W live on their own pages (`/stats.html`,
|
||||||
hive-c0re itself (docs/web-ui/dashboard.md::Dashboard
|
`/flow.html`). Their DOM only exists there — when tabs.js boots
|
||||||
endpoints), so this tile is never gated/hidden. -->
|
on this page the corresponding renderers no-op silently (each
|
||||||
<a class="home-tile" href="/api/docs">
|
guard is `if (!el) return`). -->
|
||||||
<span class="home-tile-head">
|
|
||||||
<span class="home-tile-icon" aria-hidden="true">🧬</span>
|
|
||||||
<span class="home-tile-label">API</span>
|
|
||||||
</span>
|
|
||||||
<span class="home-tile-desc"
|
|
||||||
>interactive OpenAPI spec (Swagger UI)</span
|
|
||||||
>
|
|
||||||
</a>
|
|
||||||
</nav>
|
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
<!-- /.page-content -->
|
<!-- /.page-content -->
|
||||||
|
|
||||||
<script type="module" src="/static/home.js" defer></script>
|
<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>
|
||||||
|
<!-- Short build id of the running hive; filled from hyperhive_rev. -->
|
||||||
|
<p class="meta" id="hive-rev" hidden></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>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
102
frontend/packages/dashboard/src/kept.js
Normal file
102
frontend/packages/dashboard/src/kept.js
Normal file
|
|
@ -0,0 +1,102 @@
|
||||||
|
// K3PT ST4T3 tab of the dashboard: kept state from agents whose container
|
||||||
|
// is absent (tombstones), with a purge button per row. Owns its slice of
|
||||||
|
// state like agents.js / builds.js do; tabs.js feeds it the /api/state
|
||||||
|
// snapshot and the `tombstones_changed` stream event.
|
||||||
|
|
||||||
|
import { $, form } from "./common.js";
|
||||||
|
import { el } from "@hive/shared/dom.js";
|
||||||
|
|
||||||
|
let tombstonesState = [];
|
||||||
|
|
||||||
|
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";
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Number of kept-state entries, for the tab's count pill. */
|
||||||
|
export function keptCount() {
|
||||||
|
return tombstonesState.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function syncKeptFromSnapshot(s) {
|
||||||
|
tombstonesState = (s.tombstones || []).slice();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function renderKept() {
|
||||||
|
const root = $("tombstones-section");
|
||||||
|
if (!root) return;
|
||||||
|
root.replaceChildren();
|
||||||
|
if (!tombstonesState.length) {
|
||||||
|
root.append(el("p", { class: "empty" }, "no kept state — clean"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 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 tombstonesState) {
|
||||||
|
const li = el("li", { class: "container-row tombstone" });
|
||||||
|
const head = el("div", { class: "head" });
|
||||||
|
head.append(
|
||||||
|
el("span", { class: "name" }, t.name),
|
||||||
|
// `offline` is what the absence of a container proves; `destroyed`
|
||||||
|
// is something the backend cannot know — see the caveat above.
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** `tombstones_changed` stream event: replace the list and re-render. */
|
||||||
|
export function applyTombstonesChanged(ev) {
|
||||||
|
tombstonesState = (ev.tombstones || []).slice();
|
||||||
|
renderKept();
|
||||||
|
}
|
||||||
99
frontend/packages/dashboard/src/links-menu.js
Normal file
99
frontend/packages/dashboard/src/links-menu.js
Normal file
|
|
@ -0,0 +1,99 @@
|
||||||
|
// Pages menu in the dashboard chrome: one button that opens a popover
|
||||||
|
// linking the standalone pages (flow, stats, API docs) and the swarm UI.
|
||||||
|
// Modelled on the swarm UI's `LinksMenu`. The swarm entry only exists
|
||||||
|
// when `/api/state` carries `swarm_public_url`, so a hive without a
|
||||||
|
// configured swarm domain shows no dead link.
|
||||||
|
|
||||||
|
import { $ } from "./common.js";
|
||||||
|
import { el } from "@hive/shared/dom.js";
|
||||||
|
|
||||||
|
/** Menu entries for a state snapshot. `external` opens in a new tab. */
|
||||||
|
export function menuLinks(state) {
|
||||||
|
const links = [
|
||||||
|
{ icon: "📡", label: "flow", url: "/flow.html" },
|
||||||
|
{ icon: "📊", label: "stats", url: "/stats.html" },
|
||||||
|
{ icon: "🧬", label: "api docs", url: "/api/docs/" },
|
||||||
|
];
|
||||||
|
if (state && state.swarm_public_url) {
|
||||||
|
links.push({
|
||||||
|
icon: "🐝",
|
||||||
|
label: "swarm",
|
||||||
|
url: state.swarm_public_url,
|
||||||
|
external: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return links;
|
||||||
|
}
|
||||||
|
|
||||||
|
let wired = false;
|
||||||
|
|
||||||
|
function closeMenu(root) {
|
||||||
|
const pop = root.querySelector(".links-menu-popover");
|
||||||
|
const btn = root.querySelector(".links-menu-button");
|
||||||
|
if (pop) pop.hidden = true;
|
||||||
|
if (btn) btn.setAttribute("aria-expanded", "false");
|
||||||
|
}
|
||||||
|
|
||||||
|
function wire(root) {
|
||||||
|
if (wired) return;
|
||||||
|
wired = true;
|
||||||
|
root.querySelector(".links-menu-button").addEventListener("click", () => {
|
||||||
|
const pop = root.querySelector(".links-menu-popover");
|
||||||
|
const btn = root.querySelector(".links-menu-button");
|
||||||
|
pop.hidden = !pop.hidden;
|
||||||
|
btn.setAttribute("aria-expanded", String(!pop.hidden));
|
||||||
|
});
|
||||||
|
// Outside click / Escape close the popover.
|
||||||
|
document.addEventListener("pointerdown", (e) => {
|
||||||
|
if (!root.contains(e.target)) closeMenu(root);
|
||||||
|
});
|
||||||
|
document.addEventListener("keydown", (e) => {
|
||||||
|
if (e.key === "Escape") closeMenu(root);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** (Re)render the menu into `#links-menu`. Safe to call on every state
|
||||||
|
* refresh: the open/closed state lives on the popover element, which is
|
||||||
|
* kept; only its items are rebuilt. */
|
||||||
|
export function renderLinksMenu(state) {
|
||||||
|
const root = $("links-menu");
|
||||||
|
if (!root) return;
|
||||||
|
if (!root.firstChild) {
|
||||||
|
root.append(
|
||||||
|
el(
|
||||||
|
"button",
|
||||||
|
{
|
||||||
|
type: "button",
|
||||||
|
class: "links-menu-button",
|
||||||
|
"aria-haspopup": "true",
|
||||||
|
"aria-expanded": "false",
|
||||||
|
"aria-label": "pages",
|
||||||
|
title: "pages",
|
||||||
|
},
|
||||||
|
"☰",
|
||||||
|
),
|
||||||
|
el("div", { class: "links-menu-popover", role: "menu", hidden: "" }),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const pop = root.querySelector(".links-menu-popover");
|
||||||
|
pop.replaceChildren(
|
||||||
|
...menuLinks(state).map((link) => {
|
||||||
|
const attrs = {
|
||||||
|
class: "links-menu-item",
|
||||||
|
href: link.url,
|
||||||
|
role: "menuitem",
|
||||||
|
};
|
||||||
|
if (link.external) {
|
||||||
|
attrs.target = "_blank";
|
||||||
|
attrs.rel = "noreferrer";
|
||||||
|
}
|
||||||
|
return el(
|
||||||
|
"a",
|
||||||
|
attrs,
|
||||||
|
el("span", { "aria-hidden": "true" }, link.icon),
|
||||||
|
link.label,
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
wire(root);
|
||||||
|
}
|
||||||
|
|
@ -13,12 +13,12 @@
|
||||||
<body class="stats-shell">
|
<body class="stats-shell">
|
||||||
<!-- Minimal chrome: back link + title. Same pattern as flow.html —
|
<!-- Minimal chrome: back link + title. Same pattern as flow.html —
|
||||||
no full dashboard tabbar. Back link points to the
|
no full dashboard tabbar. Back link points to the
|
||||||
H0M3 hub (served at /), not the dashboard. -->
|
dashboard (served at /). -->
|
||||||
<header class="page-header">
|
<header class="page-header">
|
||||||
<a class="page-back" href="/">← home</a>
|
<a class="page-back" href="/">← dashboard</a>
|
||||||
<span class="page-title">ST4TS</span>
|
<span class="page-title">ST4TS</span>
|
||||||
<!-- Window selector lives in the header so it behaves like a tab strip
|
<!-- Window selector lives in the header so it behaves like a tab strip.
|
||||||
(matches the sub-tab strips on /core.html). Hash-routed via createTabStrip. -->
|
Hash-routed via createTabStrip. -->
|
||||||
<nav class="hive-stats-windows" id="hive-stats-windows" role="tablist">
|
<nav class="hive-stats-windows" id="hive-stats-windows" role="tablist">
|
||||||
<button type="button" class="btn" data-tab="1h">1h</button>
|
<button type="button" class="btn" data-tab="1h">1h</button>
|
||||||
<button type="button" class="btn" data-tab="4h">4h</button>
|
<button type="button" class="btn" data-tab="4h">4h</button>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
/* ─── SYST3M/BU1LDS section panels — shared by dashboard and /core.html ───
|
/* ─── SYST3M/BU1LDS section panels — used by the dashboard ───
|
||||||
Rebuild queue, meta inputs and kept-state panels.
|
Rebuild queue, meta inputs and kept-state panels.
|
||||||
Imported by core.css and dashboard.css. */
|
Imported by dashboard.css. */
|
||||||
|
|
||||||
/* ─── meta inputs panel ────────────────────────────────────────────── */
|
/* ─── meta inputs panel ────────────────────────────────────────────── */
|
||||||
.meta-inputs {
|
.meta-inputs {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
// /dashboard.html entry point: tab renderers + tab routing + refreshState.
|
// the dashboard entry point: tab renderers + tab routing + refreshState.
|
||||||
// Reads /api/state on cold load and after every async-form submit; live
|
// Reads /api/state on cold load and after every async-form submit; live
|
||||||
// updates run through `applyXxx` mutation handlers triggered by the
|
// updates run through `applyXxx` mutation handlers triggered by the
|
||||||
// dashboard event stream (subscribed via `openStream` from common.js). See
|
// dashboard event stream (subscribed via `openStream` from common.js). See
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
// Pure helpers (DOM, side panel, path linkification)
|
// Pure helpers (DOM, side panel, path linkification)
|
||||||
// live in `./common.js`; the flow-page surface (broker
|
// live in `./common.js`; the flow-page surface (broker
|
||||||
// terminal, @-mention composer) lives in `./flow.js` — `flow.html` and
|
// terminal, @-mention composer) lives in `./flow.js` — `flow.html` and
|
||||||
// `dashboard.html` each load their own bundle.
|
// `index.html` each load their own bundle.
|
||||||
// 4G3NTS (containers) domain lives in `./agents.js`.
|
// 4G3NTS (containers) domain lives in `./agents.js`.
|
||||||
|
|
||||||
import { marked } from "marked";
|
import { marked } from "marked";
|
||||||
|
|
@ -36,6 +36,13 @@ import {
|
||||||
rebuildCount,
|
rebuildCount,
|
||||||
syncBuildsFromSnapshot,
|
syncBuildsFromSnapshot,
|
||||||
} from "./builds.js";
|
} from "./builds.js";
|
||||||
|
import {
|
||||||
|
applyTombstonesChanged,
|
||||||
|
keptCount,
|
||||||
|
renderKept,
|
||||||
|
syncKeptFromSnapshot,
|
||||||
|
} from "./kept.js";
|
||||||
|
import { renderLinksMenu } from "./links-menu.js";
|
||||||
|
|
||||||
// mdNode (in common.js) reads `window.marked` for the markdown side
|
// mdNode (in common.js) reads `window.marked` for the markdown side
|
||||||
// panel preview path. Set it here on the dashboard entry so file
|
// panel preview path. Set it here on the dashboard entry so file
|
||||||
|
|
@ -44,8 +51,8 @@ window.marked = marked;
|
||||||
|
|
||||||
(() => {
|
(() => {
|
||||||
// ─── async forms ────────────────────────────────────────────────────────
|
// ─── async forms ────────────────────────────────────────────────────────
|
||||||
// Shared `data-async` submit interceptor (now in common.js so /core.html's
|
// Shared `data-async` submit interceptor (in common.js, shared with the other
|
||||||
// bundle gets it too). On success it re-runs refreshState unless the form
|
// pages' bundles). On success it re-runs refreshState unless the form
|
||||||
// opts out via `data-no-refresh` (its mutation arrives via an SSE event).
|
// opts out via `data-no-refresh` (its mutation arrives via an SSE event).
|
||||||
bindAsyncForms(() => refreshState());
|
bindAsyncForms(() => refreshState());
|
||||||
|
|
||||||
|
|
@ -95,6 +102,22 @@ window.marked = marked;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Rev line in the footer: absent when the flake ref isn't a local path
|
||||||
|
// pin (a bare `github:` url has no on-disk rev to canonicalize), so it
|
||||||
|
// stays hidden rather than showing a placeholder. `hyperhive_rev`
|
||||||
|
// canonicalizes to a nix store path (/nix/store/<hash>-<pname>); the
|
||||||
|
// part that varies between builds is the hash, so slice that out rather
|
||||||
|
// than truncating from the tail. Falls back to a plain head-slice for a
|
||||||
|
// non-store path (e.g. a bare local dir during dev).
|
||||||
|
function renderHiveRev(rev) {
|
||||||
|
const node = $("hive-rev");
|
||||||
|
if (!node || !rev) return;
|
||||||
|
const storeMatch = rev.match(/^\/nix\/store\/([^-]+)/);
|
||||||
|
node.textContent = `rev ${(storeMatch ? storeMatch[1] : rev).slice(0, 12)}`;
|
||||||
|
node.title = rev;
|
||||||
|
node.hidden = false;
|
||||||
|
}
|
||||||
|
|
||||||
async function refreshState() {
|
async function refreshState() {
|
||||||
// Don't yank the form out from under the operator. Try again
|
// Don't yank the form out from under the operator. Try again
|
||||||
// shortly on the next tick; eventually they'll blur and the
|
// shortly on the next tick; eventually they'll blur and the
|
||||||
|
|
@ -144,7 +167,11 @@ window.marked = marked;
|
||||||
// mount's own effect handles the actual fetch.
|
// mount's own effect handles the actual fetch.
|
||||||
initJobqRollup();
|
initJobqRollup();
|
||||||
syncBuildsFromSnapshot(s);
|
syncBuildsFromSnapshot(s);
|
||||||
|
syncKeptFromSnapshot(s);
|
||||||
renderContainers(s);
|
renderContainers(s);
|
||||||
|
renderKept();
|
||||||
|
renderLinksMenu(s);
|
||||||
|
renderHiveRev(s.hyperhive_rev);
|
||||||
restoreOpenDetails(openDetails);
|
restoreOpenDetails(openDetails);
|
||||||
// No periodic refresh timer. Phase 6 covers every container
|
// No periodic refresh timer. Phase 6 covers every container
|
||||||
// mutation with `ContainerStateChanged` / `ContainerRemoved`
|
// mutation with `ContainerStateChanged` / `ContainerRemoved`
|
||||||
|
|
@ -181,7 +208,7 @@ window.marked = marked;
|
||||||
// lost during the disconnect window (same pattern as flow.js's
|
// lost during the disconnect window (same pattern as flow.js's
|
||||||
// onStreamOpen).
|
// onStreamOpen).
|
||||||
//
|
//
|
||||||
// Both /dashboard.html and /flow.html subscribe to `/api/dashboard/stream`
|
// Both the dashboard and /flow.html subscribe to `/api/dashboard/stream`
|
||||||
// and filter client-side — the dashboard ignores broker traffic
|
// and filter client-side — the dashboard ignores broker traffic
|
||||||
// and FL0W ignores mutation events.
|
// and FL0W ignores mutation events.
|
||||||
const MUTATION_HANDLERS = {
|
const MUTATION_HANDLERS = {
|
||||||
|
|
@ -198,6 +225,7 @@ window.marked = marked;
|
||||||
},
|
},
|
||||||
meta_inputs_changed: onMetaInputsChanged,
|
meta_inputs_changed: onMetaInputsChanged,
|
||||||
meta_update_running: onMetaUpdateRunning,
|
meta_update_running: onMetaUpdateRunning,
|
||||||
|
tombstones_changed: applyTombstonesChanged,
|
||||||
};
|
};
|
||||||
(function bindDashboardStream() {
|
(function bindDashboardStream() {
|
||||||
// Route through the SharedWorker so all open hyperhive tabs on the
|
// Route through the SharedWorker so all open hyperhive tabs on the
|
||||||
|
|
@ -208,12 +236,12 @@ window.marked = marked;
|
||||||
// fallback when SharedWorker isn't supported.
|
// fallback when SharedWorker isn't supported.
|
||||||
//
|
//
|
||||||
// kinds= matches MUTATION_HANDLERS verbatim, so the stream carries
|
// kinds= matches MUTATION_HANDLERS verbatim, so the stream carries
|
||||||
// only the 7 wire kinds this page acts on.
|
// only the 8 wire kinds this page acts on.
|
||||||
const es = openStream(
|
const es = openStream(
|
||||||
"/api/dashboard/stream?kinds=" +
|
"/api/dashboard/stream?kinds=" +
|
||||||
"transient_set,transient_cleared," +
|
"transient_set,transient_cleared," +
|
||||||
"container_state_changed,container_removed,rebuild_queue_changed," +
|
"container_state_changed,container_removed,rebuild_queue_changed," +
|
||||||
"meta_inputs_changed,meta_update_running",
|
"meta_inputs_changed,meta_update_running,tombstones_changed",
|
||||||
);
|
);
|
||||||
es.onmessage = (e) => {
|
es.onmessage = (e) => {
|
||||||
let ev;
|
let ev;
|
||||||
|
|
@ -245,11 +273,11 @@ window.marked = marked;
|
||||||
})();
|
})();
|
||||||
|
|
||||||
// ─── tab routing ───────────────────────────────────────────────────────
|
// ─── tab routing ───────────────────────────────────────────────────────
|
||||||
// Hash-based: `#agents` / `#rebuild` / `#meta` / `#buildlogs`
|
// Hash-based: `#agents` / `#rebuild` / `#meta` / `#buildlogs` / `#kept`
|
||||||
// activate the matching pane on the dashboard. Empty
|
// activate the matching pane on the dashboard. Empty
|
||||||
// hash defaults to 4G3NTS. FL0W and ST4TS are NOT tabs —
|
// hash defaults to 4G3NTS. FL0W and ST4TS are NOT tabs —
|
||||||
// they're separate pages (`/flow.html`, `/stats.html`) reached from
|
// they're separate pages (`/flow.html`, `/stats.html`) reached from
|
||||||
// the H0M3 hub. Tab routing only
|
// the pages menu (links-menu.js). Tab routing only
|
||||||
// applies when the tab DOM is present (e.g. not on the flow page
|
// applies when the tab DOM is present (e.g. not on the flow page
|
||||||
// itself, where these elements don't exist and the loop no-ops).
|
// itself, where these elements don't exist and the loop no-ops).
|
||||||
// The shared hash-routed tab strip (@hive/shared/tabs.js) owns the
|
// The shared hash-routed tab strip (@hive/shared/tabs.js) owns the
|
||||||
|
|
@ -299,6 +327,8 @@ window.marked = marked;
|
||||||
setTabCount("agents", stale);
|
setTabCount("agents", stale);
|
||||||
// R3BU1LD QU3U3 — rebuilds in flight.
|
// R3BU1LD QU3U3 — rebuilds in flight.
|
||||||
setTabCount("rebuild", rebuildCount());
|
setTabCount("rebuild", rebuildCount());
|
||||||
|
// K3PT ST4T3 — entries of kept state.
|
||||||
|
setTabCount("kept", keptCount());
|
||||||
}
|
}
|
||||||
// Poll the state stores on a 1s tick to keep the pill counts in
|
// Poll the state stores on a 1s tick to keep the pill counts in
|
||||||
// sync. The state stores are mutated synchronously by every SSE
|
// sync. The state stores are mutated synchronously by every SSE
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
/* ─── page chrome: back-link header ────────────────────────────────
|
/* ─── page chrome: back-link header ────────────────────────────────
|
||||||
The minimal sticky "frosted bar" header used by every standalone page
|
The minimal sticky "frosted bar" header used by every standalone page
|
||||||
reached from the H0M3 hub — /flow, /stats, /core. A back
|
reached from the dashboard's pages menu — /flow, /stats. A back
|
||||||
link to H0M3 on the left, then either a page title or (on /core) a
|
link to the dashboard on the left, then a page title and, on /stats, a
|
||||||
sub-tab nav. The operator dashboard uses its own richer
|
sub-tab nav. The operator dashboard uses its own richer
|
||||||
sticky chrome (`.dashboard-chrome`); these classes cover the simple
|
sticky chrome (`.dashboard-chrome`); these classes cover the simple
|
||||||
pages.
|
pages.
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,8 @@
|
||||||
// The list is entirely server-driven (see swarm-controller's
|
// The list is entirely server-driven (see swarm-controller's
|
||||||
// `services.hyperhive.swarm.controller.links` option) — 0 entries
|
// `services.hyperhive.swarm.controller.links` option) — 0 entries
|
||||||
// hides the button rather than showing an empty popover, same "don't
|
// hides the button rather than showing an empty popover, same "don't
|
||||||
// render a dead affordance" rule the old dashboard's H0M3 tiles follow
|
// render a dead affordance" rule the dashboard's pages menu follows for
|
||||||
// for Forge/Matrix.
|
// its swarm entry.
|
||||||
import { useEffect, useRef, useState } from "preact/hooks";
|
import { useEffect, useRef, useState } from "preact/hooks";
|
||||||
import { LinkIcon } from "@hive/shared/icons.js";
|
import { LinkIcon } from "@hive/shared/icons.js";
|
||||||
import "./LinksMenu.css";
|
import "./LinksMenu.css";
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,7 @@
|
||||||
// Stepped on an interval (the steppy cadence is the look, and it costs
|
// Stepped on an interval (the steppy cadence is the look, and it costs
|
||||||
// little), paused while the tab is hidden, and stopped (and cleared) when
|
// little), paused while the tab is hidden, and stopped (and cleared) when
|
||||||
// motion is reduced: the settings menu's `data-motion` override wins over
|
// motion is reduced: the settings menu's `data-motion` override wins over
|
||||||
// `prefers-reduced-motion`, and is followed live. The same effect as the hive dashboard's home
|
// `prefers-reduced-motion`, and is followed live.
|
||||||
// page, which is plain JS over a fixed canvas.
|
|
||||||
import { useEffect, useRef } from "preact/hooks";
|
import { useEffect, useRef } from "preact/hooks";
|
||||||
import {
|
import {
|
||||||
CELL,
|
CELL,
|
||||||
|
|
|
||||||
|
|
@ -87,6 +87,11 @@ pub(super) struct StateSnapshot {
|
||||||
/// which is only right by accident on deployments that aren't
|
/// which is only right by accident on deployments that aren't
|
||||||
/// plain localhost.
|
/// plain localhost.
|
||||||
forge_public_url: Option<String>,
|
forge_public_url: Option<String>,
|
||||||
|
/// Browser URL of the swarm UI (the swarm apex domain). Sourced from
|
||||||
|
/// `HIVE_SWARM_PUBLIC_URL`, which the c0re NixOS module sets from
|
||||||
|
/// `services.hyperhive.swarm.domain`. `None` when the domain is unset —
|
||||||
|
/// the dashboard's pages menu then omits the swarm entry.
|
||||||
|
swarm_public_url: Option<String>,
|
||||||
/// Human name of this single-host hive instance (e.g. `"pr1ma"`).
|
/// Human name of this single-host hive instance (e.g. `"pr1ma"`).
|
||||||
/// Sourced from `HYPERHIVE_HIVE_NAME` env var, set by the c0re
|
/// Sourced from `HYPERHIVE_HIVE_NAME` env var, set by the c0re
|
||||||
/// NixOS module from `services.hyperhive.hiveName`. `None` when
|
/// NixOS module from `services.hyperhive.hiveName`. `None` when
|
||||||
|
|
@ -219,6 +224,9 @@ pub(super) async fn api_state(
|
||||||
forge_public_url: std::env::var("HIVE_FORGE_PUBLIC_URL")
|
forge_public_url: std::env::var("HIVE_FORGE_PUBLIC_URL")
|
||||||
.ok()
|
.ok()
|
||||||
.filter(|s| !s.is_empty()),
|
.filter(|s| !s.is_empty()),
|
||||||
|
swarm_public_url: std::env::var("HIVE_SWARM_PUBLIC_URL")
|
||||||
|
.ok()
|
||||||
|
.filter(|s| !s.is_empty()),
|
||||||
hive_name: std::env::var("HYPERHIVE_HIVE_NAME")
|
hive_name: std::env::var("HYPERHIVE_HIVE_NAME")
|
||||||
.ok()
|
.ok()
|
||||||
.filter(|s| !s.is_empty()),
|
.filter(|s| !s.is_empty()),
|
||||||
|
|
|
||||||
|
|
@ -208,6 +208,13 @@ in
|
||||||
# forge links rather than emitting one it can't justify.
|
# forge links rather than emitting one it can't justify.
|
||||||
HIVE_FORGE_PUBLIC_URL = config.services.hyperhive.swarm.forge.publicUrl;
|
HIVE_FORGE_PUBLIC_URL = config.services.hyperhive.swarm.forge.publicUrl;
|
||||||
}
|
}
|
||||||
|
// lib.optionalAttrs (config.services.hyperhive.swarm.domain != null) {
|
||||||
|
# Browser URL of the swarm UI, which answers on the swarm apex domain
|
||||||
|
# (see ../swarm-ui.nix). The dashboard links to it from its pages menu.
|
||||||
|
# Absent when the domain is unset, so the link is hidden rather than
|
||||||
|
# guessed.
|
||||||
|
HIVE_SWARM_PUBLIC_URL = "https://${config.services.hyperhive.swarm.domain}/";
|
||||||
|
}
|
||||||
// lib.optionalAttrs (config.services.hyperhive.swarm.matrix.gatewayHost != null) {
|
// lib.optionalAttrs (config.services.hyperhive.swarm.matrix.gatewayHost != null) {
|
||||||
# Browser-facing matrix GUI (fluffychat) URL — the gateway
|
# Browser-facing matrix GUI (fluffychat) URL — the gateway
|
||||||
# vhost (`gatewayHost`, `chat.<swarm-domain>` by default). Surfaced via the daemon's `Urls`
|
# vhost (`gatewayHost`, `chat.<swarm-domain>` by default). Surfaced via the daemon's `Urls`
|
||||||
|
|
|
||||||
|
|
@ -116,13 +116,13 @@ in
|
||||||
example = "https://forge.example.com";
|
example = "https://forge.example.com";
|
||||||
description = ''
|
description = ''
|
||||||
Browser-facing forge URL the dashboard uses to build clickable
|
Browser-facing forge URL the dashboard uses to build clickable
|
||||||
forge links (the H0M3 Forge tile, per-agent-row forge links) —
|
forge links (the per-agent-row forge links) —
|
||||||
sourced into every agent container + hive-c0re as
|
sourced into every agent container + hive-c0re as
|
||||||
`HIVE_FORGE_PUBLIC_URL`.
|
`HIVE_FORGE_PUBLIC_URL`.
|
||||||
|
|
||||||
Defaults to `https://''${cfg.domain}`, the gateway vhost. When
|
Defaults to `https://''${cfg.domain}`, the gateway vhost. When
|
||||||
`null`, the dashboard **hides** forge links rather than
|
`null`, the dashboard **hides** forge links rather than
|
||||||
guessing one — see `docs/web-ui/dashboard.md::H0M3 page` for
|
guessing one — see `docs/web-ui/dashboard.md::Container row` for
|
||||||
the rationale (a link built from the operator's own browser
|
the rationale (a link built from the operator's own browser
|
||||||
hostname + a container port is only an accident away from
|
hostname + a container port is only an accident away from
|
||||||
wrong on any deployment that isn't plain localhost).
|
wrong on any deployment that isn't plain localhost).
|
||||||
|
|
|
||||||
|
|
@ -192,7 +192,7 @@ let
|
||||||
# files — hive-c0re serves none of this as a fallback.
|
# files — hive-c0re serves none of this as a fallback.
|
||||||
# `= /api/docs` (no trailing slash) redirects to `/api/docs/`: nginx
|
# `= /api/docs` (no trailing slash) redirects to `/api/docs/`: nginx
|
||||||
# owns that redirect because hive-c0re has no route for it, and the
|
# owns that redirect because hive-c0re has no route for it, and the
|
||||||
# H0M3 hub's own `/api/docs` link (no trailing slash) would 404
|
# the dashboard's own `/api/docs` links (no trailing slash) would 404
|
||||||
# without it.
|
# without it.
|
||||||
swaggerUiLocations = {
|
swaggerUiLocations = {
|
||||||
"= /api/docs" = {
|
"= /api/docs" = {
|
||||||
|
|
|
||||||
|
|
@ -15,11 +15,10 @@
|
||||||
#
|
#
|
||||||
# $out/dashboard/ the hive-c0re dashboard assets (full layout in
|
# $out/dashboard/ the hive-c0re dashboard assets (full layout in
|
||||||
# frontend/packages/dashboard/build.mjs):
|
# frontend/packages/dashboard/build.mjs):
|
||||||
# index.html (H0M3 hub, served at /) dashboard.html (operator SPA,
|
# index.html (operator SPA, served at /) flow.html
|
||||||
# served at /dashboard.html) flow.html
|
|
||||||
# stats.html favicon.svg
|
# stats.html favicon.svg
|
||||||
# static/{home,tabs,flow,stats,stream-worker}.js{,.map}
|
# static/{tabs,flow,stats,stream-worker}.js{,.map}
|
||||||
# static/{colors,theme,common,home,dashboard,flow,stats}.css
|
# static/{colors,theme,common,dashboard,flow,stats}.css
|
||||||
# $out/agent/ the per-agent default UI (layered with
|
# $out/agent/ the per-agent default UI (layered with
|
||||||
# hyperhive.frontend.extraFiles at activation time)
|
# hyperhive.frontend.extraFiles at activation time)
|
||||||
# index.html stats.html screen.html
|
# index.html stats.html screen.html
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue