Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e455964cb3 | ||
|
|
ee5c057d8f |
29 changed files with 448 additions and 517 deletions
|
|
@ -127,7 +127,7 @@ Two things live in the `agent-configs` Forgejo organization:
|
||||||
[How a hive reads the forge](#how-a-hive-reads-the-forge)).
|
[How a hive reads the forge](#how-a-hive-reads-the-forge)).
|
||||||
- The dashboard links each container's deployed config commit to this
|
- The dashboard links each container's deployed config commit to this
|
||||||
config repo on the swarm's forge (`swarm.forge.publicUrl`; `null` means no
|
config repo on the swarm's forge (`swarm.forge.publicUrl`; `null` means no
|
||||||
link), so operators can go straight from the SW4RM tab into
|
link), so operators can go straight from the 4G3NTS tab into
|
||||||
the rendered repo without an extra `git` step.
|
the rendered repo without an extra `git` step.
|
||||||
|
|
||||||
The `hive-forge` CLI (separate workspace crate, see
|
The `hive-forge` CLI (separate workspace crate, see
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ swarm controller and hive-c0re each build their own graph on it.
|
||||||
|
|
||||||
Two views, one per graph, drawn by the same component: the swarm UI's
|
Two views, one per graph, drawn by the same component: the swarm UI's
|
||||||
`/jobs` page shows the swarm controller's graph, and the hive dashboard's
|
`/jobs` page shows the swarm controller's graph, and the hive dashboard's
|
||||||
**BU1LDS** page (R3BU1LD QU3U3 — see
|
**R3BU1LD QU3U3** tab (see
|
||||||
[`web-ui/dashboard.md`](../web-ui/dashboard.md)) shows that hive's. Each
|
[`web-ui/dashboard.md`](../web-ui/dashboard.md)) shows that hive's. Each
|
||||||
**row** is one job; the rows nested under it are that job's steps, in
|
**row** is one job; the rows nested under it are that job's steps, in
|
||||||
order (occasionally a couple run side by side). A step shows one of:
|
order (occasionally a couple run side by side). A step shows one of:
|
||||||
|
|
|
||||||
|
|
@ -11,21 +11,23 @@ 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 **H0M3 hub**, served at `/` — a grid of tiles
|
||||||
linking to every surface (Dashboard, Flow, Logs, Builds, Stats,
|
linking to every surface (Dashboard, Flow, Logs, Stats,
|
||||||
Settings, Core). Every page links back to H0M3, so you're
|
Settings, Core). Every page links back to H0M3, so you're
|
||||||
never more than one step from the hub.
|
never more than one step from the hub.
|
||||||
|
|
||||||
The **dashboard** itself (`/dashboard.html`) is where you'll spend most
|
The **dashboard** itself (`/dashboard.html`) is where you'll spend most
|
||||||
of your time. It's a single page with exactly two tabs:
|
of your time. It's a single page with five tabs:
|
||||||
|
|
||||||
- **SW4RM** — 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
|
||||||
|
lifecycle: the rebuild queue with a live log of whatever is building,
|
||||||
|
the meta flake inputs you can bump, and the build log history.
|
||||||
- **P3RM1SS10NS** — what tools and system-level access each agent has.
|
- **P3RM1SS10NS** — what tools and system-level access each agent has.
|
||||||
|
|
||||||
Everything else lives on its own page instead, all reachable from the
|
Everything else lives on its own page instead, all reachable from the
|
||||||
H0M3 hub: **Flow** (`/flow.html`, the raw live message stream across
|
H0M3 hub: **Flow** (`/flow.html`, the raw live message stream across
|
||||||
the whole swarm), **Stats** (`/stats.html`, swarm-wide usage stats),
|
the whole swarm), **Stats** (`/stats.html`, swarm-wide usage stats), and
|
||||||
**Builds** (`/builds.html`, the rebuild queue and build history), and
|
|
||||||
**Core** (`/core.html`, tombstones and container resource use).
|
**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
|
||||||
|
|
@ -35,7 +37,7 @@ if the gateway isn't in front).
|
||||||
|
|
||||||
## The things you'll actually do
|
## The things you'll actually do
|
||||||
|
|
||||||
**Check on an agent.** SW4RM shows every container as a row: name,
|
**Check on an agent.** 4G3NTS shows every container as a row: name,
|
||||||
whether it's running, what it's currently doing (a live status pill —
|
whether it's running, what it's currently doing (a live status pill —
|
||||||
`rebuilding…`, `starting…`, or similar — while something's in flight),
|
`rebuilding…`, `starting…`, or similar — while something's in flight),
|
||||||
and quick links (stats, screen, forge profile). Select the name to open
|
and quick links (stats, screen, forge profile). Select the name to open
|
||||||
|
|
@ -47,12 +49,12 @@ repo. Review and merge them on the forge; the merge deploys the change.
|
||||||
See [`config-changes.md`](../agent-lifecycle/config-changes.md#config-changes).
|
See [`config-changes.md`](../agent-lifecycle/config-changes.md#config-changes).
|
||||||
|
|
||||||
**Start, stop, restart, or rebuild an agent.** Select one or more
|
**Start, stop, restart, or rebuild an agent.** Select one or more
|
||||||
agents on SW4RM (select the icon) and use the selection bar, or use the
|
agents on 4G3NTS (select the icon) and use the selection bar, or use the
|
||||||
per-agent `⋮` menu on a single row. Rebuilding re-applies that agent's
|
per-agent `⋮` menu on a single row. Rebuilding re-applies that agent's
|
||||||
current config; use it whenever an agent
|
current config; use it whenever an agent
|
||||||
shows as "needs update."
|
shows as "needs update."
|
||||||
|
|
||||||
**Watch a build.** BU1LDS shows the rebuild queue live, plus a
|
**Watch a build.** The dashboard's R3BU1LD QU3U3 tab shows the rebuild queue live, plus a
|
||||||
streaming log of whatever's currently building. Useful right after
|
streaming log of whatever's currently building. Useful right after
|
||||||
merging a config change or bumping a flake input.
|
merging a config change or bumping a flake input.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,8 @@ matrix accounts, the controller's job graph — are on the
|
||||||
|
|
||||||
hive-c0re serves the dashboard at `/dashboard.html` (with the home page at `/`).
|
hive-c0re serves the dashboard at `/dashboard.html` (with the home page 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 (`#swarm`,
|
one tab pane at a time. The URL hash (`#agents`, `#rebuild`, `#meta`,
|
||||||
`#permissions`) drives which pane is
|
`#buildlogs`, `#permissions`) 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 the other
|
||||||
standalone pages are reachable from the H0M3 hub at `/`, not
|
standalone pages are reachable from the H0M3 hub at `/`, not
|
||||||
from the dashboard tab strip.
|
from the dashboard tab strip.
|
||||||
|
|
@ -19,17 +19,18 @@ from the 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
|
- **← home back-link**: top-left of the chrome, links to the H0M3 hub
|
||||||
at `/`. Every surface links back to the hub rather than to each other.
|
at `/`. Every surface links back to the hub rather than to each other.
|
||||||
- **Tab strip**: `◆ SW4RM ◆`, `◆ P3RM1SS10NS ◆`. In-page tabs only —
|
- **Tab strip**: `◆ 4G3NTS ◆`, `◆ R3BU1LD QU3U3 ◆`, `◆ M3T4 1NPUTS ◆`,
|
||||||
|
`◆ BUILD L0GS ◆`, `◆ P3RM1SS10NS ◆`. In-page tabs only —
|
||||||
the SYST3M panels live on the
|
the SYST3M panels live on the
|
||||||
standalone **C0R3** page (`/core.html`), and FL0W / ST4TS
|
standalone **C0R3** page (`/core.html`), and FL0W / ST4TS
|
||||||
live on their own pages too, all reachable from the
|
live on their own pages too, all reachable from the
|
||||||
H0M3 hub (not the tab strip). Count pill on SW4RM
|
H0M3 hub (not the tab strip). Count pills on 4G3NTS
|
||||||
(container count).
|
(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 (dashboard + every stand-alone page — FL0W,
|
||||||
H0M3, C0R3, BU1LDS, ST4TS), injected at the top
|
H0M3, C0R3, ST4TS), 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
|
||||||
|
|
@ -55,7 +56,7 @@ The FL0W page uses a slim header (a `← home` 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.
|
||||||
|
|
||||||
## SW4RM tab
|
## 4G3NTS tab
|
||||||
|
|
||||||
**C0NTAINERS** — live containers as a flat list, one row per
|
**C0NTAINERS** — live containers as a flat list, one row per
|
||||||
container, sorted alphabetically by agent name; every container the
|
container, sorted alphabetically by agent name; every container the
|
||||||
|
|
@ -73,13 +74,13 @@ agent is stale.
|
||||||
|
|
||||||
Passive / rare-interaction state, reached from the **Core** tile on the
|
Passive / rare-interaction state, reached from the **Core** tile on the
|
||||||
H0M3 hub (served at
|
H0M3 hub (served at
|
||||||
`/core.html`), with the same minimal chrome as `/builds.html`: a `← home`
|
`/core.html`), with minimal chrome: a `← home` back-link and a single pane,
|
||||||
back-link and a single pane, **K3PT ST4T3**. The page is its own esbuild
|
**K3PT ST4T3**. The page is its own esbuild bundle (`core.js`) that
|
||||||
bundle (`core.js`) that cold-loads `/api/state` and subscribes to
|
cold-loads `/api/state` and subscribes to `/api/dashboard/stream` for
|
||||||
`/api/dashboard/stream` for `tombstones_changed`, `capabilities_changed`,
|
`tombstones_changed`, `capabilities_changed`, and `tool_groups_changed`
|
||||||
and `tool_groups_changed` (the latter two re-render the stale-perms
|
(the latter two re-render the stale-perms sub-section when permission
|
||||||
sub-section when permission data changes). (Rebuild queue and meta inputs
|
data changes). (Rebuild queue and meta inputs are dashboard tabs — see
|
||||||
live on the BU1LDS page — see below.)
|
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
|
||||||
|
|
@ -111,21 +112,23 @@ autorefreshes on `capabilities_changed` + `tool_groups_changed` SSE
|
||||||
events. Each ghost agent gets a `✕ clear perms` button
|
events. Each ghost agent gets a `✕ clear perms` button
|
||||||
(`DELETE /api/permissions/{name}`).
|
(`DELETE /api/permissions/{name}`).
|
||||||
|
|
||||||
## BU1LDS page (`/builds.html`)
|
## Build tabs (`#rebuild`, `#meta`, `#buildlogs`)
|
||||||
|
|
||||||
The build lifecycle hub — rebuild queue, live build log, meta inputs,
|
The build lifecycle — rebuild queue, live build log, meta inputs, and
|
||||||
and build log history on one page. Standalone page reached from the
|
build log history — is three tabs of the dashboard: **R3BU1LD QU3U3**
|
||||||
**Builds** tile on the H0M3 hub, same minimal chrome as `/core.html`.
|
(`#rebuild`), **M3T4 1NPUTS** (`#meta`) and **BUILD L0GS**
|
||||||
Three sub-tabs: **R3BU1LD QU3U3** (default), **M3T4 1NPUTS**,
|
(`#buildlogs`). They live in `builds.js`, which `tabs.js` imports: the
|
||||||
**BUILD L0GS**. Its own esbuild bundle (`builds.js`); cold-loads
|
dashboard's one tab strip, `/api/state` snapshot and
|
||||||
`/api/state` and subscribes to `/api/dashboard/stream` for
|
`/api/dashboard/stream` subscription serve them, with
|
||||||
`rebuild_queue_changed`, `meta_inputs_changed`, `meta_update_running`.
|
`rebuild_queue_changed`, `meta_inputs_changed` and `meta_update_running`
|
||||||
The SW4RM tab (`/dashboard.html`) does **not** read this endpoint at
|
on the stream alongside the dashboard's other kinds. The count pill on
|
||||||
all, by design — the swarm view stays independent of job-queue
|
R3BU1LD QU3U3 is the number of rebuilds in flight.
|
||||||
internals. Its per-agent pending badges are transient-only, and
|
The 4G3NTS tab does **not** read the graph endpoint, by design — the swarm
|
||||||
its queue-summary banner reads the much narrower `GET /api/jobq/rollup`
|
view stays independent of job-queue internals. Its per-agent pending
|
||||||
instead (see Container row, below) — a handful of pre-tallied counts,
|
badges are transient-only, and its queue-summary banner reads the much
|
||||||
not the graph.
|
narrower `GET /api/jobq/rollup` instead (see Container row, below) — a
|
||||||
|
handful of pre-tallied counts, not the graph. The banner's "view queue →"
|
||||||
|
link goes to the `#rebuild` tab.
|
||||||
|
|
||||||
**R3BU1LD QU3U3** — pending, in-flight, and recently settled container
|
**R3BU1LD QU3U3** — pending, in-flight, and recently settled container
|
||||||
operations: rebuilds, meta-update cascades, and first-spawns. One
|
operations: rebuilds, meta-update cascades, and first-spawns. One
|
||||||
|
|
@ -394,7 +397,7 @@ re-renders the terminal row. The operator addresses the root agent as `@root`.
|
||||||
## H0M3 page (`/`)
|
## H0M3 page (`/`)
|
||||||
|
|
||||||
The H0M3 hub is the primary landing page (served at `/` by default). A
|
The H0M3 hub is the primary landing page (served at `/` by default). A
|
||||||
responsive grid of link tiles — Dashboard, Flow, Builds, Stats,
|
responsive grid of link tiles — Dashboard, Flow, Stats,
|
||||||
Core, API — each pointing to their respective
|
Core, API — each pointing to their respective
|
||||||
surfaces, all unconditionally shown (no gating). The page is a pure
|
surfaces, all unconditionally shown (no gating). The page is a pure
|
||||||
portal with no tab-bar or SSE subscriptions. Typography + colours inherit
|
portal with no tab-bar or SSE subscriptions. Typography + colours inherit
|
||||||
|
|
@ -411,7 +414,7 @@ the tooltip says "select … for bulk actions" or "deselect … (or press
|
||||||
Esc to clear all)." The `<img>` points at `<url>/icon`; load failure
|
Esc to clear all)." The `<img>` points at `<url>/icon`; load failure
|
||||||
falls back to the dimmed hyperhive mark (`/favicon.svg`). The card
|
falls back to the dimmed hyperhive mark (`/favicon.svg`). The card
|
||||||
body sits to the right with three stacked lines
|
body sits to the right with three stacked lines
|
||||||
(`assets/swarm.js::renderContainers`).
|
(`assets/agents.js::renderContainers`).
|
||||||
|
|
||||||
<!-- vale write-good.Passive = NO -->
|
<!-- vale write-good.Passive = NO -->
|
||||||
**Icon layout + load strategy:** the `<img>` is absolutely
|
**Icon layout + load strategy:** the `<img>` is absolutely
|
||||||
|
|
@ -580,10 +583,10 @@ agent is stale. Banner pulses on each broker SSE event
|
||||||
**Build-queue summary banner** — when the job queue has any active
|
**Build-queue summary banner** — when the job queue has any active
|
||||||
work, a compact amber banner sits above the container list: `◐ build
|
work, a compact amber banner sits above the container list: `◐ build
|
||||||
queue — N running · M queued — view queue →` (the link goes to the
|
queue — N running · M queued — view queue →` (the link goes to the
|
||||||
BU1LDS page's R3BU1LD QU3U3). The shared `JobqRollup` Preact component
|
R3BU1LD QU3U3 tab). The shared `JobqRollup` Preact component
|
||||||
(`@hive/shared/jobq-rollup.js` — the same one swarm-ui's `/jobs` page
|
(`@hive/shared/jobq-rollup.js` — the same one swarm-ui's `/jobs` page
|
||||||
mounts, pointed at swarm-controller's own rollup endpoint instead),
|
mounts, pointed at swarm-controller's own rollup endpoint instead),
|
||||||
mounted once into `#jobq-rollup-section` by `swarm.js::initJobqRollup`
|
mounted once into `#jobq-rollup-section` by `agents.js::initJobqRollup`
|
||||||
and refreshed via its own handle rather than being re-rendered by
|
and refreshed via its own handle rather than being re-rendered by
|
||||||
`renderContainers`. Reads `GET /api/jobq/rollup`
|
`renderContainers`. Reads `GET /api/jobq/rollup`
|
||||||
(`hive-jobq-wire::state_rollup`) — `Vec<{ state, nodes, roots }>`,
|
(`hive-jobq-wire::state_rollup`) — `Vec<{ state, nodes, roots }>`,
|
||||||
|
|
@ -645,7 +648,7 @@ Bulk actions (`R3ST4RT` / `ST0P` / `ST4RT` / `P4US3` / `R3SUM3` /
|
||||||
buttons — see **Container row** above. Selecting an agent's icon
|
buttons — see **Container row** above. Selecting an agent's icon
|
||||||
toggles its selection (an in-memory `Set<name>`); `Esc` or the bar's
|
toggles its selection (an in-memory `Set<name>`); `Esc` or the bar's
|
||||||
`✕ clear` button drops everything. The selection persists across tab
|
`✕ clear` button drops everything. The selection persists across tab
|
||||||
switches in-memory — the bar just hides on non-SW4RM tabs since other
|
switches in-memory — the bar just hides on non-4G3NTS tabs since other
|
||||||
tabs don't show the agent cards needed to cross-reference.
|
tabs don't show the agent cards needed to cross-reference.
|
||||||
|
|
||||||
When the operator selects one or more agents (via the icon), a sticky
|
When the operator selects one or more agents (via the icon), a sticky
|
||||||
|
|
@ -711,11 +714,11 @@ below — some endpoints aren't in it yet.
|
||||||
404 when the row is missing (vacuum-reaped or stale id).
|
404 when the row is missing (vacuum-reaped or stale id).
|
||||||
- `GET /api/build-logs/id/{id}/stream` — live SSE stream of
|
- `GET /api/build-logs/id/{id}/stream` — live SSE stream of
|
||||||
stdout+stderr for an in-progress build (newline-delimited JSON
|
stdout+stderr for an in-progress build (newline-delimited JSON
|
||||||
frames). Used by the expanding live-row view in BU1LDS. The
|
frames). Used by the expanding live-row view in the BUILD L0GS tab. The
|
||||||
stream closes when the build finishes or errors.
|
stream closes when the build finishes or errors.
|
||||||
- `GET /api/build-logs/id/{id}/raw` — raw concatenated
|
- `GET /api/build-logs/id/{id}/raw` — raw concatenated
|
||||||
stdout+stderr as `text/plain`. Used by the `↓ raw` / `↓ download
|
stdout+stderr as `text/plain`. Used by the `↓ raw` / `↓ download
|
||||||
raw` links in BU1LDS. Works on both finished and in-progress
|
raw` links in the build tabs. Works on both finished and in-progress
|
||||||
build logs.
|
build logs.
|
||||||
- `GET /static/marked.js` serves the vendored `marked` bundle used
|
- `GET /static/marked.js` serves the vendored `marked` bundle used
|
||||||
for markdown previews.
|
for markdown previews.
|
||||||
|
|
@ -854,10 +857,10 @@ payload):
|
||||||
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. C0R3 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. BU1LDS 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`.
|
||||||
- `meta_update_running` (running: bool) — emitted when a
|
- `meta_update_running` (running: bool) — emitted when a
|
||||||
`nix flake update` ripple starts or completes. BU1LDS M3T4 1NPUTS
|
`nix flake update` ripple starts or completes. The M3T4 1NPUTS
|
||||||
tab uses this to show/hide the "⏳ meta-update running" banner.
|
tab uses this to show/hide the "⏳ meta-update running" banner.
|
||||||
|
|
||||||
<!-- vale write-good.Passive = NO -->
|
<!-- vale write-good.Passive = NO -->
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@
|
||||||
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` / `home.css` /
|
||||||
`stats.css` / `core.css` / `builds.css`);
|
`stats.css` / `core.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.
|
||||||
|
|
@ -208,7 +208,7 @@ re-render), call `refreshState()`. State shapes live in
|
||||||
`hive-c0re/src/dashboard/state_snapshot.rs::StateSnapshot` and
|
`hive-c0re/src/dashboard/state_snapshot.rs::StateSnapshot` and
|
||||||
`web_ui/state.rs::StateSnapshot` — when
|
`web_ui/state.rs::StateSnapshot` — when
|
||||||
adding state fields, plumb through the snapshot struct and the
|
adding state fields, plumb through the snapshot struct and the
|
||||||
relevant domain module (`swarm.js`, `permissions.js`, etc.) render function.
|
relevant domain module (`agents.js`, `permissions.js`, etc.) render function.
|
||||||
|
|
||||||
**Focus preservation:** `refreshState` checks whether
|
**Focus preservation:** `refreshState` checks whether
|
||||||
`document.activeElement` sits inside one of the managed sections
|
`document.activeElement` sits inside one of the managed sections
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ layers per-agent additions on top of the default agent dist.
|
||||||
output is reproducible without network access at build time.
|
output is reproducible without network access at build time.
|
||||||
- **esbuild**: `agent` and `swarm-ui` are Preact apps; `dashboard` is a
|
- **esbuild**: `agent` and `swarm-ui` are Preact apps; `dashboard` is a
|
||||||
vanilla-JS + custom-element MPA with a few Preact-rendered pages
|
vanilla-JS + custom-element MPA with a few Preact-rendered pages
|
||||||
(`builds.js`, `swarm.js`); `shared` holds both kinds of module. No
|
(`builds.js`, `agents.js`, imported by the dashboard entry); `shared` holds both kinds of module. No
|
||||||
framework runtime beyond Preact where it's used. Each workspace's
|
framework runtime beyond Preact where it's used. Each workspace's
|
||||||
`build.mjs` is a short esbuild script.
|
`build.mjs` is a short esbuild script.
|
||||||
- **Single-PR migration**: see the tracked design proposal and
|
- **Single-PR migration**: see the tracked design proposal and
|
||||||
|
|
|
||||||
|
|
@ -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 (H0M3 at
|
||||||
// `/`, dashboard/flow/core/builds at their own
|
// `/`, dashboard/flow/core/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.
|
||||||
|
|
@ -38,7 +38,6 @@ await build({
|
||||||
src("home.js"),
|
src("home.js"),
|
||||||
src("stats.js"),
|
src("stats.js"),
|
||||||
src("core.js"),
|
src("core.js"),
|
||||||
src("builds.js"),
|
|
||||||
],
|
],
|
||||||
outdir: staticDir(""),
|
outdir: staticDir(""),
|
||||||
bundle: true,
|
bundle: true,
|
||||||
|
|
@ -57,7 +56,7 @@ await build({
|
||||||
// runs as its own esbuild invocation over different entry points.
|
// runs as its own esbuild invocation over different entry points.
|
||||||
loader: { ".css": "text" },
|
loader: { ".css": "text" },
|
||||||
// `@hive/shared/jobq-graph.js` resolves to a real `.jsx` file
|
// `@hive/shared/jobq-graph.js` resolves to a real `.jsx` file
|
||||||
// (`JobqGraph.jsx`), pulled in transitively by `builds.js` — esbuild
|
// (`JobqGraph.jsx`), pulled in transitively by `tabs.js` via `builds.js` — esbuild
|
||||||
// already picks the `jsx` loader for `.jsx` by extension, this just
|
// already picks the `jsx` loader for `.jsx` by extension, this just
|
||||||
// sets the transform mode to match swarm-ui's (which also authors
|
// sets the transform mode to match swarm-ui's (which also authors
|
||||||
// this file). No other entry here uses JSX today; this doesn't turn
|
// this file). No other entry here uses JSX today; this doesn't turn
|
||||||
|
|
@ -103,7 +102,6 @@ for (const entry of [
|
||||||
"home.css",
|
"home.css",
|
||||||
"stats.css",
|
"stats.css",
|
||||||
"core.css",
|
"core.css",
|
||||||
"builds.css",
|
|
||||||
]) {
|
]) {
|
||||||
await build({
|
await build({
|
||||||
entryPoints: [src(entry)],
|
entryPoints: [src(entry)],
|
||||||
|
|
@ -120,7 +118,6 @@ for (const html of [
|
||||||
"flow.html",
|
"flow.html",
|
||||||
"stats.html",
|
"stats.html",
|
||||||
"core.html",
|
"core.html",
|
||||||
"builds.html",
|
|
||||||
]) {
|
]) {
|
||||||
copyFileSync(src(html), dist(html));
|
copyFileSync(src(html), dist(html));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
// hive-agent-menu.js — <hive-agent-menu>, the per-agent "⋮" context-menu
|
// hive-agent-menu.js — <hive-agent-menu>, the per-agent "⋮" context-menu
|
||||||
// custom element. One instance per agent card, appended after
|
// custom element. One instance per agent card, appended after
|
||||||
// .card-body in the container row's flex layout (swarm.js's
|
// .card-body in the container row's flex layout (agents.js's
|
||||||
// buildContainerLi via buildAgentMenu).
|
// buildContainerLi via buildAgentMenu).
|
||||||
//
|
//
|
||||||
// Deliberately a *light-DOM* element with no shadow root of its own: all
|
// Deliberately a *light-DOM* element with no shadow root of its own: all
|
||||||
|
|
@ -66,7 +66,7 @@ async function agentMenuPost(actionPath, name, body, flags) {
|
||||||
class HiveAgentMenu extends HTMLElement {
|
class HiveAgentMenu extends HTMLElement {
|
||||||
connectedCallback() {
|
connectedCallback() {
|
||||||
// Same reconnect-without-detach hazard as <hive-menu> (see its
|
// Same reconnect-without-detach hazard as <hive-menu> (see its
|
||||||
// connectedCallback comment) — swarm.js's row cache can move an
|
// connectedCallback comment) — agents.js's row cache can move an
|
||||||
// already-built <li> subtree without a real detach. Without this
|
// already-built <li> subtree without a real detach. Without this
|
||||||
// guard a reconnect would append a second <hive-menu> on top of the
|
// guard a reconnect would append a second <hive-menu> on top of the
|
||||||
// first, doubling the dropdown.
|
// first, doubling the dropdown.
|
||||||
|
|
|
||||||
987
frontend/packages/dashboard/src/agents.js
Normal file
987
frontend/packages/dashboard/src/agents.js
Normal file
|
|
@ -0,0 +1,987 @@
|
||||||
|
// 4G3NTS (containers) domain — extracted from tabs.js.
|
||||||
|
// Agent topology, container-row rendering, selection bar, and all
|
||||||
|
// live-update apply handlers for container-state, transient, and
|
||||||
|
// job-queue-rollup ops. See docs/web-ui/dashboard.md::Container row for the
|
||||||
|
// rendering contract.
|
||||||
|
|
||||||
|
import { $, form, fmtAgeSecs } from "./common.js";
|
||||||
|
import { el } from "@hive/shared/dom.js";
|
||||||
|
import { themedConfirm, themedToast } from "@hive/shared/modal.js";
|
||||||
|
import { h, render } from "preact";
|
||||||
|
import { JobqRollup } from "@hive/shared/jobq-rollup.js";
|
||||||
|
import { containersState } from "./state.js";
|
||||||
|
import { closeAllMenus } from "@hive/shared/hive-menu.js";
|
||||||
|
import "./agent-menu/hive-agent-menu.js"; // registers <hive-agent-menu> — side-effect import
|
||||||
|
|
||||||
|
// Context-window badge thresholds. Preferred source is each container's
|
||||||
|
// `context_window_tokens` from /api/state (the real window for the model
|
||||||
|
// it last ran on) — thresholds are then 75% / 50% of it, matching the
|
||||||
|
// harness compaction watermarks (compact at 75%, auto-reset at 50%). The
|
||||||
|
// fixed token constants are the fallback for when that field is absent
|
||||||
|
// (agent has no turns yet, or no per-model config matched the model).
|
||||||
|
const CTX_WARN_FRACTION = 0.75; // >= this share of the window -> red
|
||||||
|
const CTX_CAUTION_FRACTION = 0.5; // >= this share of the window -> yellow
|
||||||
|
const CTX_WARN_TOKENS = 150_000; // fallback red threshold (~= 75% of 200k)
|
||||||
|
const CTX_CAUTION_TOKENS = 100_000; // fallback yellow threshold (~= 50% of 200k)
|
||||||
|
|
||||||
|
// ─── module-level state ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
// Keyed container row cache. Maps agent name -> { el: <li>, fingerprint }.
|
||||||
|
// Allows renderContainers to skip rebuilding rows whose displayed state
|
||||||
|
// hasn't changed — prevents full-wipe flicker + avoids redundant async
|
||||||
|
// dashboard-state fetches on every SSE event.
|
||||||
|
const containerRowCache = new Map();
|
||||||
|
|
||||||
|
// Derived transient state — cold-loaded from /api/state.transients,
|
||||||
|
// then mutated live by `transient_set` / `transient_cleared`. Keyed
|
||||||
|
// by agent name -> Map<transient_kind, since_unix>: an agent can hold
|
||||||
|
// several pills at once (every *running* node naming the agent lights
|
||||||
|
// one now, not just a curated "worth surfacing" subset — see
|
||||||
|
// docs/web-ui/dashboard.md::Container row), keyed the same way the backend's
|
||||||
|
// own tombstone map is, `(agent, label)`, so a clear only removes the
|
||||||
|
// specific pill it names rather than guessing. `since_unix` is
|
||||||
|
// wall-clock so the elapsed-seconds badge ticks without polling.
|
||||||
|
const transientsState = new Map();
|
||||||
|
|
||||||
|
// In-memory set of selected agent logical names backing the sticky
|
||||||
|
// #selection-bar. See docs/web-ui/dashboard.md::Selection bar for the
|
||||||
|
// interaction model (icon-click toggle, Esc/clear button drop,
|
||||||
|
// tab-gated visibility).
|
||||||
|
const selectionState = new Set();
|
||||||
|
|
||||||
|
// ─── job queue rollup (queue-summary banner only) ──────────────────────────
|
||||||
|
//
|
||||||
|
// The ONE piece of job-queue-derived state on this page, and deliberately
|
||||||
|
// narrow — mara: "agents.js should not need to pull in the jobq to do its
|
||||||
|
// job." Rendering is owned by the shared `JobqRollup` Preact component
|
||||||
|
// (same one swarm-ui's /jobs page mounts, self-fetching
|
||||||
|
// GET /api/jobq/rollup); this file just calls `render(h(...))` and bumps
|
||||||
|
// a refresh token to force a refetch.
|
||||||
|
//
|
||||||
|
// Rendered into #jobq-rollup-section, a sibling of #containers-section
|
||||||
|
// kept OUTSIDE that section's per-render `replaceChildren()` wipe (see
|
||||||
|
// dashboard.html) — a section that gets wiped on every container-state
|
||||||
|
// tick would defeat the component owning its own fetch lifecycle.
|
||||||
|
let jobqRollupToken = 0;
|
||||||
|
|
||||||
|
export function initJobqRollup() {
|
||||||
|
const root = $("jobq-rollup-section");
|
||||||
|
if (!root) return;
|
||||||
|
render(
|
||||||
|
h(JobqRollup, {
|
||||||
|
endpoint: "/api/jobq/rollup",
|
||||||
|
queueHref: "#rebuild",
|
||||||
|
refreshToken: jobqRollupToken,
|
||||||
|
}),
|
||||||
|
root,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Refetches on cold load (tabs.js's refreshState) and whenever
|
||||||
|
// `rebuild_queue_changed` fires — a payload-less push trigger by
|
||||||
|
// design, confirmed with atlas on the jobq-deletion tracker: the event
|
||||||
|
// carries no `queue` field this page reads, same "something changed,
|
||||||
|
// go refetch" treatment builds.js gives the R3BU1LD QU3U3 tab's JobqGraph.
|
||||||
|
export function applyRebuildQueueChanged() {
|
||||||
|
jobqRollupToken += 1;
|
||||||
|
initJobqRollup();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── transients ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export function syncTransientsFromSnapshot(s) {
|
||||||
|
transientsState.clear();
|
||||||
|
const nowUnix = Math.floor(Date.now() / 1000);
|
||||||
|
for (const t of s.transients || []) {
|
||||||
|
// Snapshot is a flat list — multiple rows CAN share the same
|
||||||
|
// `name` (one row per running node) — so group into this agent's
|
||||||
|
// kind map rather than overwrite.
|
||||||
|
let byKind = transientsState.get(t.name);
|
||||||
|
if (!byKind) {
|
||||||
|
byKind = new Map();
|
||||||
|
transientsState.set(t.name, byKind);
|
||||||
|
}
|
||||||
|
// Snapshot ships `secs` (server-computed); reconstruct an
|
||||||
|
// approximate since_unix so the live ticker keeps progressing
|
||||||
|
// without surprising jumps when the next snapshot lands.
|
||||||
|
byKind.set(t.kind, t.since_unix ?? nowUnix - (t.secs || 0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export function applyTransientSet(ev) {
|
||||||
|
let byKind = transientsState.get(ev.name);
|
||||||
|
if (!byKind) {
|
||||||
|
byKind = new Map();
|
||||||
|
transientsState.set(ev.name, byKind);
|
||||||
|
}
|
||||||
|
byKind.set(ev.transient_kind, ev.since_unix);
|
||||||
|
renderContainersFromState();
|
||||||
|
}
|
||||||
|
export function applyTransientCleared(ev) {
|
||||||
|
const byKind = transientsState.get(ev.name);
|
||||||
|
if (!byKind || !byKind.delete(ev.transient_kind)) return;
|
||||||
|
// Drop the now-empty outer entry too — keeps `transientsState.size`
|
||||||
|
// meaning "agents with at least one pill" (checked at the
|
||||||
|
// no-containers-and-no-transients empty-state gate below).
|
||||||
|
if (byKind.size === 0) transientsState.delete(ev.name);
|
||||||
|
renderContainersFromState();
|
||||||
|
}
|
||||||
|
// Re-render using the last cached snapshot (containers come from
|
||||||
|
// /api/state, transients overlay from the derived map). The snapshot
|
||||||
|
// is stashed on window.__hyperhive_state by refreshState; on cold
|
||||||
|
// load before the first snapshot we just skip.
|
||||||
|
export function renderContainersFromState() {
|
||||||
|
const s = window.__hyperhive_state;
|
||||||
|
if (s) renderContainers(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── container-state apply ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export function applyContainerStateChanged(ev) {
|
||||||
|
if (!ev.container || !ev.container.name) return;
|
||||||
|
containersState.set(ev.container.name, ev.container);
|
||||||
|
renderContainersFromState();
|
||||||
|
}
|
||||||
|
export function applyContainerRemoved(ev) {
|
||||||
|
if (containersState.delete(ev.name)) renderContainersFromState();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── selection ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function toggleSelection(name) {
|
||||||
|
if (selectionState.has(name)) selectionState.delete(name);
|
||||||
|
else selectionState.add(name);
|
||||||
|
renderContainersFromState();
|
||||||
|
}
|
||||||
|
function clearSelection() {
|
||||||
|
if (selectionState.size === 0) return;
|
||||||
|
selectionState.clear();
|
||||||
|
renderContainersFromState();
|
||||||
|
}
|
||||||
|
// Esc clears the current selection (operator escape hatch — mirrors
|
||||||
|
// the side-panel close pattern). Ignored when an editable element
|
||||||
|
// has focus so typing in compose / answer isn't intercepted.
|
||||||
|
document.addEventListener("keydown", (e) => {
|
||||||
|
if (e.key !== "Escape") return;
|
||||||
|
if (!selectionState.size) return;
|
||||||
|
const a = document.activeElement;
|
||||||
|
if (
|
||||||
|
a &&
|
||||||
|
(a.isContentEditable ||
|
||||||
|
a.tagName === "INPUT" ||
|
||||||
|
a.tagName === "TEXTAREA" ||
|
||||||
|
a.tagName === "SELECT")
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
e.preventDefault();
|
||||||
|
clearSelection();
|
||||||
|
});
|
||||||
|
document.addEventListener("click", (e) => {
|
||||||
|
if (e.target && e.target.closest("#selection-clear")) {
|
||||||
|
clearSelection();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── per-agent context menu ──────────────────────────────────────────────────
|
||||||
|
// Three-dot (⋮) button on each agent card for quick single-agent
|
||||||
|
// lifecycle actions without needing to select first. State-aware:
|
||||||
|
// restart/stop only shown when running, start only shown when
|
||||||
|
// stopped; rebuild + destroy/purge always shown.
|
||||||
|
// The button is CSS-invisible until the row is hovered (or menu is
|
||||||
|
// open) so it doesn't clutter quiet rows.
|
||||||
|
// Rendering + agent-specific interaction lives in the <hive-agent-menu>
|
||||||
|
// custom element (./agent-menu/hive-agent-menu.js, imported above for its
|
||||||
|
// customElements.define side effect); the generic dropdown mechanics
|
||||||
|
// (open/close, positioning, singleton coordination — closed via
|
||||||
|
// `closeAllMenus` below) live in the shared `<hive-menu>` component it
|
||||||
|
// composes internally (@hive/shared/hive-menu.js). This is a thin
|
||||||
|
// wrapper matching <hive-dialog>'s `._opts`-before-append convention,
|
||||||
|
// since a custom element created via `document.createElement` can't
|
||||||
|
// take constructor args.
|
||||||
|
function buildAgentMenu(c, forgeBase) {
|
||||||
|
const menu = document.createElement("hive-agent-menu");
|
||||||
|
menu._opts = { c, forgeBase };
|
||||||
|
return menu;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── port conflicts ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
// Re-derive port conflicts from the live containers map. Mirrors the
|
||||||
|
// server-side `build_port_conflicts` so the banner reacts to event
|
||||||
|
// updates instead of waiting for a /api/state refetch.
|
||||||
|
function derivePortConflicts(containers) {
|
||||||
|
const byPort = new Map();
|
||||||
|
for (const c of containers) {
|
||||||
|
if (!byPort.has(c.port)) byPort.set(c.port, []);
|
||||||
|
byPort.get(c.port).push(c.name);
|
||||||
|
}
|
||||||
|
const out = [];
|
||||||
|
for (const [port, agents] of byPort) {
|
||||||
|
if (agents.length > 1) {
|
||||||
|
agents.sort();
|
||||||
|
out.push({ port, agents });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out.sort((a, b) => a.port - b.port);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── container ordering ──────────────────────────────────────────────────────
|
||||||
|
// Flat alphabetical sort — see docs/web-ui/dashboard.md::C0NTAINERS.
|
||||||
|
|
||||||
|
function sortedContainerRows(containers) {
|
||||||
|
// Close any open context menu before replacing the DOM tree — the
|
||||||
|
// previous dropdown element would otherwise be a stale reference.
|
||||||
|
closeAllMenus();
|
||||||
|
return [...containers].sort((a, b) => a.name.localeCompare(b.name));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── container row ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
// Serialise the visible state of a container row into a stable string
|
||||||
|
// for change-detection. Includes everything that affects what the row
|
||||||
|
// renders — container fields, derived pending/selection state, and
|
||||||
|
// link-base context. The async dashboard-state (nav strip, ctx badge,
|
||||||
|
// status text) is intentionally excluded: it populates in-place and is
|
||||||
|
// preserved when a row is reused.
|
||||||
|
function containerRowFingerprint(
|
||||||
|
c,
|
||||||
|
pending,
|
||||||
|
opRunning,
|
||||||
|
selected,
|
||||||
|
gatewayLinks,
|
||||||
|
hostname,
|
||||||
|
) {
|
||||||
|
return JSON.stringify({
|
||||||
|
running: c.running,
|
||||||
|
failed: c.failed,
|
||||||
|
paused: c.paused,
|
||||||
|
needs_login: c.needs_login,
|
||||||
|
needs_update: c.needs_update,
|
||||||
|
active_model: c.active_model,
|
||||||
|
port: c.port,
|
||||||
|
pending,
|
||||||
|
opRunning,
|
||||||
|
selected,
|
||||||
|
gatewayLinks,
|
||||||
|
hostname,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build a single container-row <li> from scratch. Extracted so
|
||||||
|
// renderContainers can call this only for rows whose fingerprint
|
||||||
|
// changed (keyed cache), skipping the build + async dashboard-state
|
||||||
|
// fetch for stable rows.
|
||||||
|
function buildContainerLi(c, opts) {
|
||||||
|
const { pending, opRunning, selected, url, containerBase, forgeBase, s } =
|
||||||
|
opts;
|
||||||
|
// A single `pending-running` class covers any active transient
|
||||||
|
// badge state (see the pending-badge derivation comment in
|
||||||
|
// renderContainers).
|
||||||
|
const li = el("li", {
|
||||||
|
class:
|
||||||
|
"container-row" +
|
||||||
|
(opRunning ? " pending-running" : "") +
|
||||||
|
(selected ? " selected" : ""),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Agent icon: 5em square wrapper with an absolutely-positioned
|
||||||
|
// <img> + fire-and-forget load with /favicon.svg fallback. The
|
||||||
|
// wrapper doubles as the selection toggle (click / keyboard).
|
||||||
|
// See docs/web-ui/dashboard.md::Container row for the layout + load-strategy
|
||||||
|
// rationale.
|
||||||
|
const iconImg = el("img", { class: "container-icon-img", alt: "" });
|
||||||
|
const icon = el(
|
||||||
|
"div",
|
||||||
|
{
|
||||||
|
class: "container-icon",
|
||||||
|
role: "button",
|
||||||
|
tabindex: "0",
|
||||||
|
"aria-pressed": selected ? "true" : "false",
|
||||||
|
title: selected
|
||||||
|
? `deselect ${c.name} (or press Esc to clear all)`
|
||||||
|
: `select ${c.name} for bulk actions`,
|
||||||
|
},
|
||||||
|
iconImg,
|
||||||
|
);
|
||||||
|
icon.addEventListener("click", (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
toggleSelection(c.name);
|
||||||
|
});
|
||||||
|
icon.addEventListener("keydown", (e) => {
|
||||||
|
if (e.key === "Enter" || e.key === " ") {
|
||||||
|
e.preventDefault();
|
||||||
|
toggleSelection(c.name);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (c.running) {
|
||||||
|
iconImg.src = `${url}icon`;
|
||||||
|
iconImg.addEventListener("error", () => {
|
||||||
|
if (iconImg.dataset.fallback) return; // guard: don't loop if the favicon itself 404s
|
||||||
|
iconImg.dataset.fallback = "1";
|
||||||
|
icon.classList.add("icon-unreachable");
|
||||||
|
iconImg.src = "/favicon.svg";
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Container stopped — skip the doomed `${url}icon` fetch and
|
||||||
|
// go straight to the dimmed hyperhive mark. Avoids a noisy
|
||||||
|
// failed request in the console + the brief broken-image flash.
|
||||||
|
icon.classList.add("icon-unreachable");
|
||||||
|
iconImg.src = "/favicon.svg";
|
||||||
|
}
|
||||||
|
// Card body: the three stacked content lines, right of the icon.
|
||||||
|
const body = el("div", { class: "card-body" });
|
||||||
|
|
||||||
|
// -- identity -------------------------------------------------------
|
||||||
|
const head = el("div", { class: "head" });
|
||||||
|
head.append(
|
||||||
|
el(
|
||||||
|
"a",
|
||||||
|
{ class: "name", href: url, target: "_blank", rel: "noopener" },
|
||||||
|
c.name,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
// Icon-only nav strip — populated async from the agent's own
|
||||||
|
// `GET /api/dashboard-state` (via gateway when enabled, direct
|
||||||
|
// TCP otherwise). The agent is the single source of truth for its
|
||||||
|
// link list: stats / screen (GUI agents only — c0re's disk-based
|
||||||
|
// fallback cannot detect this) / forge profile / agent-configs /
|
||||||
|
// extras. DOM-built — link strings come from the agent's process
|
||||||
|
// and must never reach the HTML parser.
|
||||||
|
const navStrip = el("span", { class: "nav-strip" });
|
||||||
|
head.append(navStrip);
|
||||||
|
if (c.running) {
|
||||||
|
// Fetch the lean dashboard-state snapshot from the agent directly.
|
||||||
|
// Populates: nav strip links (including the screen link that
|
||||||
|
// c0re's disk-based build cannot detect), rate_limited badge,
|
||||||
|
// ctx-window badge, and self-reported status text.
|
||||||
|
// Fails gracefully when the agent is starting up or the gateway
|
||||||
|
// is not yet routing to it — badges simply don't appear.
|
||||||
|
// Only runs when the row is first built (fingerprint changed) —
|
||||||
|
// reused rows keep their previously-fetched nav strip + badges.
|
||||||
|
fetch(`${containerBase}/api/dashboard-state`)
|
||||||
|
.then((r) => (r.ok ? r.json() : null))
|
||||||
|
.then((ds) => {
|
||||||
|
if (!ds) return;
|
||||||
|
// -- nav strip -----------------------------------------------
|
||||||
|
if (Array.isArray(ds.links)) {
|
||||||
|
for (const lnk of ds.links) {
|
||||||
|
// No public forge URL to link to — hide rather than build
|
||||||
|
// a broken/guessed href (see forgeBase above).
|
||||||
|
if (lnk.kind === "forge" && !forgeBase) continue;
|
||||||
|
const href =
|
||||||
|
lnk.kind === "forge"
|
||||||
|
? forgeBase + (lnk.url || "")
|
||||||
|
: lnk.kind === "external"
|
||||||
|
? lnk.url || ""
|
||||||
|
: /* container */ containerBase + "/" + (lnk.url || "");
|
||||||
|
const a = el("a", {
|
||||||
|
class: "nav-link",
|
||||||
|
href,
|
||||||
|
target: "_blank",
|
||||||
|
rel: "noopener",
|
||||||
|
title: lnk.label || "",
|
||||||
|
});
|
||||||
|
// Plain text — agent-controlled strings stay out of innerHTML.
|
||||||
|
a.textContent = lnk.icon || lnk.label || "";
|
||||||
|
navStrip.append(a);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// -- agent-owned status badges --------------------------------
|
||||||
|
// rate_limited: only show when no other critical badge is
|
||||||
|
// already shown (pending / not-running already handled sync).
|
||||||
|
if (ds.rate_limited) {
|
||||||
|
head.append(
|
||||||
|
el(
|
||||||
|
"span",
|
||||||
|
{
|
||||||
|
class: "hive-pill-sm badge-rate-limited",
|
||||||
|
title:
|
||||||
|
"API rate-limited — harness is parked, will retry automatically",
|
||||||
|
},
|
||||||
|
"⊘ rate limited",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// ctx-window badge
|
||||||
|
if (ds.ctx_tokens != null) {
|
||||||
|
const k = Math.round(ds.ctx_tokens / 1000);
|
||||||
|
const win = ds.context_window_tokens;
|
||||||
|
const warn = win != null ? win * CTX_WARN_FRACTION : CTX_WARN_TOKENS;
|
||||||
|
const caution =
|
||||||
|
win != null ? win * CTX_CAUTION_FRACTION : CTX_CAUTION_TOKENS;
|
||||||
|
const ctxClass =
|
||||||
|
ds.ctx_tokens >= warn
|
||||||
|
? "badge-ctx-warn"
|
||||||
|
: ds.ctx_tokens >= caution
|
||||||
|
? "badge-ctx-caution"
|
||||||
|
: "badge-ctx-ok";
|
||||||
|
const title =
|
||||||
|
win != null
|
||||||
|
? `last turn context: ${ds.ctx_tokens.toLocaleString()} / ${win.toLocaleString()} ` +
|
||||||
|
`tokens (${Math.round((ds.ctx_tokens / win) * 100)}% of the window)`
|
||||||
|
: `last turn context size: ${ds.ctx_tokens.toLocaleString()} tokens`;
|
||||||
|
head.append(
|
||||||
|
el(
|
||||||
|
"span",
|
||||||
|
{ class: `hive-pill-sm ${ctxClass}`, title },
|
||||||
|
`ctx·${k}k`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// -- agent status text (self-reported via set_status) ---------
|
||||||
|
if (ds.status_text) {
|
||||||
|
const nowUnix = Math.floor(Date.now() / 1000);
|
||||||
|
const ageStr =
|
||||||
|
ds.status_set_at != null
|
||||||
|
? ` (set ${fmtAgeSecs(nowUnix - ds.status_set_at)} ago)`
|
||||||
|
: "";
|
||||||
|
// Stamp data-set-at so the 30s ticker below keeps the age
|
||||||
|
// label current even when the row is reused from the cache.
|
||||||
|
const ageAttrs =
|
||||||
|
ds.status_set_at != null
|
||||||
|
? { class: "status-age", "data-set-at": String(ds.status_set_at) }
|
||||||
|
: { class: "status-age" };
|
||||||
|
// Icon + message are wrapped together in `.status-msg` so the
|
||||||
|
// CSS can clamp *just the message* to two lines; the age span is
|
||||||
|
// a separate flex sibling that is never clipped, so the "(set N
|
||||||
|
// ago)" stamp stays visible even when the status text is long.
|
||||||
|
body.append(
|
||||||
|
el(
|
||||||
|
"div",
|
||||||
|
{
|
||||||
|
class: "agent-status",
|
||||||
|
title: `agent self-reported status${ageStr}`,
|
||||||
|
},
|
||||||
|
el(
|
||||||
|
"span",
|
||||||
|
{ class: "status-msg" },
|
||||||
|
el("span", { class: "status-icon" }, "◈ "),
|
||||||
|
ds.status_text,
|
||||||
|
),
|
||||||
|
el("span", ageAttrs, ageStr),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
/* graceful: agent starting / gateway miss -> no data */
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Status / runtime badges. Pending transients always win
|
||||||
|
// (start / stop / restart / rebuild is in progress). Otherwise,
|
||||||
|
// when the container is stopped, surface a single badge — `✖ gave
|
||||||
|
// up` (red) when systemd's restart bound was hit (`c.failed`), else
|
||||||
|
// the plain muted `■ not running`. Both read `running: false`;
|
||||||
|
// `failed` is what tells "gave up" apart from "stopped on purpose"
|
||||||
|
// (an absent `failed` — an older backend — is `undefined`, falsy,
|
||||||
|
// so this degrades to the old single badge). `needs_login` is still
|
||||||
|
// c0re-owned (reads auth sentinel files on the host). rate_limited /
|
||||||
|
// ctx / status_text are agent-owned and rendered by the async
|
||||||
|
// dashboard-state fetch above.
|
||||||
|
if (pending.length) {
|
||||||
|
// One badge per pill — an agent can carry several transients at
|
||||||
|
// once now (see docs/web-ui/dashboard.md::Container row), each rendered
|
||||||
|
// independently rather than collapsed into one label.
|
||||||
|
for (const label of pending) {
|
||||||
|
head.append(
|
||||||
|
el(
|
||||||
|
"span",
|
||||||
|
{ class: "pending-state" },
|
||||||
|
el("span", { class: "spinner" }, "◐"),
|
||||||
|
" ",
|
||||||
|
label + "…",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else if (!c.running && c.failed) {
|
||||||
|
head.append(
|
||||||
|
el(
|
||||||
|
"span",
|
||||||
|
{
|
||||||
|
class: "hive-pill-sm badge-fail",
|
||||||
|
title:
|
||||||
|
"container gave up — its unit hit systemd's bounded restart limit and stopped " +
|
||||||
|
"on its own, not on operator request; start it to bring the harness back up",
|
||||||
|
},
|
||||||
|
"✖ gave up",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else if (!c.running) {
|
||||||
|
head.append(
|
||||||
|
el(
|
||||||
|
"span",
|
||||||
|
{
|
||||||
|
class: "hive-pill-sm badge-muted",
|
||||||
|
title:
|
||||||
|
"container is shut down — start it to bring the harness back up",
|
||||||
|
},
|
||||||
|
"■ not running",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else if (c.needs_login) {
|
||||||
|
head.append(
|
||||||
|
el(
|
||||||
|
"a",
|
||||||
|
{
|
||||||
|
class: "hive-pill-sm badge-warn",
|
||||||
|
href: url,
|
||||||
|
target: "_blank",
|
||||||
|
rel: "noopener",
|
||||||
|
},
|
||||||
|
"needs login →",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (c.paused) {
|
||||||
|
// Paused badge is also a resume button: clicking POSTs /api/resume/{name}
|
||||||
|
// which removes the marker and flips the badge off via the SSE rescan.
|
||||||
|
head.append(
|
||||||
|
form(
|
||||||
|
"/api/resume/" + c.name,
|
||||||
|
"hive-pill-sm badge-paused btn-inline",
|
||||||
|
"⏸ paused",
|
||||||
|
`resume ${c.name}? the turn loop restarts and drains queued messages.`,
|
||||||
|
{},
|
||||||
|
{ noRefresh: true },
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (c.needs_update) {
|
||||||
|
head.append(
|
||||||
|
form(
|
||||||
|
"/api/rebuild/" + c.name,
|
||||||
|
"hive-pill-sm badge-warn btn-inline",
|
||||||
|
"needs update ↻",
|
||||||
|
"rebuild " + c.name + "? hot-reloads the container.",
|
||||||
|
{},
|
||||||
|
{ noRefresh: true },
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (c.active_model) {
|
||||||
|
head.append(
|
||||||
|
el(
|
||||||
|
"span",
|
||||||
|
{
|
||||||
|
class: "hive-pill-sm badge-model",
|
||||||
|
title: `active claude model: ${c.active_model}`,
|
||||||
|
},
|
||||||
|
c.active_model,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
body.append(head);
|
||||||
|
|
||||||
|
// Per-card action buttons (R3ST4RT / ST0P / ST4RT / R3BU1LD /
|
||||||
|
// DESTR0Y / PURG3) live on the selection bar, not here — see
|
||||||
|
// docs/web-ui/dashboard.md::Selection bar. The contextual `needs update ↻`
|
||||||
|
// chip in the head row stays — it's a state-hint, not an
|
||||||
|
// action button.
|
||||||
|
|
||||||
|
li.append(icon, body, buildAgentMenu(c, forgeBase));
|
||||||
|
return li;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── containers render ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export function renderContainers(s) {
|
||||||
|
const root = $("containers-section");
|
||||||
|
// #containers-section only exists on /dashboard.html. tabs.js is the
|
||||||
|
// bundle for that page only (flow.html loads flow.js instead).
|
||||||
|
// Belt-and-suspenders for any future page that adds tabs.js
|
||||||
|
// without a #containers-section — matches the
|
||||||
|
// no-op-when-target-absent convention the other renderers
|
||||||
|
// follow.
|
||||||
|
if (!root) return;
|
||||||
|
|
||||||
|
// Containers come from the derived map (event-driven) rather than
|
||||||
|
// `s.containers`; `s` still supplies hostname (for the web-ui
|
||||||
|
// link) and tombstones/meta_inputs (not event-derived yet). The
|
||||||
|
// tree builder handles the ordering — we don't pre-sort here.
|
||||||
|
const containers = Array.from(containersState.values());
|
||||||
|
const portConflicts = derivePortConflicts(containers);
|
||||||
|
const anyStale = containers.some((c) => c.needs_update);
|
||||||
|
|
||||||
|
// Preserve the keyed container list across the header-section
|
||||||
|
// rebuild. We wipe the banners/buttons above the list on every
|
||||||
|
// render (simple), but recycle <li> elements for unchanged rows
|
||||||
|
// (keyed) to avoid full-DOM thrash and redundant async fetches.
|
||||||
|
const existingUl = root.querySelector("ul.containers");
|
||||||
|
root.replaceChildren();
|
||||||
|
|
||||||
|
// Port-hash collisions: rename one of the listed agents and
|
||||||
|
// rebuild. The banner sits above the agent list so it's the
|
||||||
|
// first thing the operator sees when something's wedged.
|
||||||
|
if (portConflicts.length) {
|
||||||
|
const banner = el(
|
||||||
|
"hive-warn",
|
||||||
|
{ level: "error" },
|
||||||
|
el("strong", {}, "⚠ port collision"),
|
||||||
|
" — ",
|
||||||
|
);
|
||||||
|
const groups = portConflicts
|
||||||
|
.map((c) => `:${c.port} (${c.agents.join(" + ")})`)
|
||||||
|
.join("; ");
|
||||||
|
banner.append(groups + ". rename one of each and ↻ R3BU1LD.");
|
||||||
|
root.append(banner);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (anyStale) {
|
||||||
|
root.append(
|
||||||
|
form(
|
||||||
|
"/api/update-all",
|
||||||
|
"btn-rebuild",
|
||||||
|
"↻ UPD4TE 4LL",
|
||||||
|
"rebuild every stale container?",
|
||||||
|
{},
|
||||||
|
{ noRefresh: true },
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Queue-summary banner: see initJobqRollup, above.
|
||||||
|
|
||||||
|
if (!containers.length && !transientsState.size) {
|
||||||
|
root.append(el("p", { class: "empty" }, "no managed containers"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Drop stale selections (agent destroyed while selected). Defensive —
|
||||||
|
// the action bar would otherwise loop POST against a gone agent.
|
||||||
|
const liveNames = new Set(containers.map((c) => c.name));
|
||||||
|
for (const n of Array.from(selectionState)) {
|
||||||
|
if (!liveNames.has(n)) selectionState.delete(n);
|
||||||
|
}
|
||||||
|
|
||||||
|
const hostname = (s && s.hostname) || window.location.hostname;
|
||||||
|
// When hive-gateway is in front of the dashboard, build same-origin
|
||||||
|
// `/agent/<name>/` URLs instead of the direct `http://<host>:<port>/`
|
||||||
|
// TCP fallback — the gateway proxies the prefix to the per-agent
|
||||||
|
// harness (unix-domain via `agent-sockets.json`, or a computed TCP
|
||||||
|
// loopback port while the socket marker is absent). See
|
||||||
|
// `docs/web-ui/dashboard.md::Container row` + `docs/networking/gateway.md::Vhost map`.
|
||||||
|
const gatewayLinks = !!(s && s.gateway_enabled);
|
||||||
|
// Forge public URL: state.forge_public_url (set by the NixOS module
|
||||||
|
// from services.hyperhive.forge.publicUrl) or null — never guessed
|
||||||
|
// from "<hostname>:3000". `null` propagates down to
|
||||||
|
// buildContainerLi/buildAgentMenu, which already hide forge links
|
||||||
|
// rather than emit a possibly-wrong one.
|
||||||
|
const forgeBase = (s && s.forge_public_url) || null;
|
||||||
|
const ul = existingUl ?? el("ul", { class: "containers" });
|
||||||
|
const rows = sortedContainerRows(containers);
|
||||||
|
|
||||||
|
// Build the ordered list of <li> elements, reusing cached rows
|
||||||
|
// whose displayed state hasn't changed.
|
||||||
|
const orderedLis = [];
|
||||||
|
for (const c of rows) {
|
||||||
|
const url = gatewayLinks
|
||||||
|
? `/agent/${encodeURIComponent(c.name)}/`
|
||||||
|
: `http://${hostname}:${c.port}/`;
|
||||||
|
// Container nav-strip base: gateway prefix or direct TCP.
|
||||||
|
const containerBase = gatewayLinks
|
||||||
|
? `/agent/${encodeURIComponent(c.name)}`
|
||||||
|
: `http://${hostname}:${c.port}`;
|
||||||
|
// Pending-badge derivation: transients only — "what is running,"
|
||||||
|
// full stop (mara, on review: "remove the per agent pending stuff -
|
||||||
|
// only show what is running"; rendered one badge per pill — mara,
|
||||||
|
// earlier: "show all running nodes that name the agent"). See
|
||||||
|
// docs/web-ui/dashboard.md::Container row.
|
||||||
|
const transientKindsMap = transientsState.get(c.name);
|
||||||
|
// Sorted for stable badge order across renders — Map iteration
|
||||||
|
// order is insertion order, which shifts as pills clear/re-add.
|
||||||
|
const pending = transientKindsMap
|
||||||
|
? Array.from(transientKindsMap.keys()).sort()
|
||||||
|
: [];
|
||||||
|
const opRunning = pending.length > 0;
|
||||||
|
const selected = selectionState.has(c.name);
|
||||||
|
|
||||||
|
const fp = containerRowFingerprint(
|
||||||
|
c,
|
||||||
|
pending,
|
||||||
|
opRunning,
|
||||||
|
selected,
|
||||||
|
gatewayLinks,
|
||||||
|
hostname,
|
||||||
|
);
|
||||||
|
const cached = containerRowCache.get(c.name);
|
||||||
|
|
||||||
|
let li;
|
||||||
|
if (cached && cached.fingerprint === fp) {
|
||||||
|
// Row unchanged — reuse the existing DOM node. The async
|
||||||
|
// dashboard-state (nav strip, ctx badge, status text) stays
|
||||||
|
// intact from the previous build, avoiding a redundant fetch.
|
||||||
|
li = cached.el;
|
||||||
|
} else {
|
||||||
|
li = buildContainerLi(c, {
|
||||||
|
pending,
|
||||||
|
opRunning,
|
||||||
|
selected,
|
||||||
|
url,
|
||||||
|
containerBase,
|
||||||
|
forgeBase,
|
||||||
|
s,
|
||||||
|
});
|
||||||
|
containerRowCache.set(c.name, { el: li, fingerprint: fp });
|
||||||
|
}
|
||||||
|
orderedLis.push(li);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove cache entries for agents that no longer exist.
|
||||||
|
for (const [name, entry] of containerRowCache) {
|
||||||
|
if (!liveNames.has(name)) {
|
||||||
|
entry.el.remove();
|
||||||
|
containerRowCache.delete(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply correct DOM order without a full wipe. insertBefore is a
|
||||||
|
// no-op when the node is already at position i, so stable sections
|
||||||
|
// of the list cause zero layout work.
|
||||||
|
for (let i = 0; i < orderedLis.length; i++) {
|
||||||
|
if (ul.children[i] !== orderedLis[i]) {
|
||||||
|
ul.insertBefore(orderedLis[i], ul.children[i] ?? null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Trim any excess children (defensive — shouldn't happen after the
|
||||||
|
// cache-removal pass above, but keeps the ul length exact).
|
||||||
|
while (ul.children.length > orderedLis.length) ul.lastChild.remove();
|
||||||
|
|
||||||
|
root.append(ul);
|
||||||
|
renderSelectionBar(containers);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── selection bar ───────────────────────────────────────────────────────────
|
||||||
|
// Sticky-bottom strip; visible when >=1 agent selected on the 4G3NTS
|
||||||
|
// tab. See docs/web-ui/dashboard.md::Selection bar for the interaction model
|
||||||
|
// and the per-action availability rules (disabled-with-tooltip for
|
||||||
|
// actions that don't apply to the full selection). Actions POST per
|
||||||
|
// agent in a loop (endpoints are individually idempotent /
|
||||||
|
// event-covered, so no new bulk backend wire is needed).
|
||||||
|
export function renderSelectionBar(containers) {
|
||||||
|
const bar = $("selection-bar");
|
||||||
|
if (!bar) return;
|
||||||
|
const countSpan = $("selection-count");
|
||||||
|
const namesSpan = $("selection-names");
|
||||||
|
const actions = $("selection-actions");
|
||||||
|
if (!countSpan || !namesSpan || !actions) return;
|
||||||
|
|
||||||
|
const selected = containers.filter((c) => selectionState.has(c.name));
|
||||||
|
// Tab-gate: bar only renders on 4G3NTS (the only tab with agent
|
||||||
|
// cards to cross-reference). Selection state lives in-memory and
|
||||||
|
// the bar reappears on return to 4G3NTS if still non-empty.
|
||||||
|
const onAgentsTab =
|
||||||
|
(document.body.dataset.activeTab || "agents") === "agents";
|
||||||
|
if (!selected.length || !onAgentsTab) {
|
||||||
|
bar.hidden = true;
|
||||||
|
document.body.classList.remove("has-selection");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
bar.hidden = false;
|
||||||
|
document.body.classList.add("has-selection");
|
||||||
|
countSpan.textContent =
|
||||||
|
selected.length === 1
|
||||||
|
? "1 agent selected"
|
||||||
|
: selected.length + " agents selected";
|
||||||
|
namesSpan.textContent = "· " + selected.map((c) => c.name).join(", ");
|
||||||
|
|
||||||
|
// Recompute action availability + tooltips per render. Each
|
||||||
|
// action declares which agents it CAN'T run on; the bar disables
|
||||||
|
// the button and surfaces the offending names in the tooltip.
|
||||||
|
actions.replaceChildren();
|
||||||
|
const allRunning = selected.every((c) => c.running);
|
||||||
|
const allStopped = selected.every((c) => !c.running);
|
||||||
|
// Pause/resume is orthogonal to running (see hive-agent-menu.js's own
|
||||||
|
// comment on the per-agent version of these two actions) — a paused
|
||||||
|
// stopped agent boots paused, a paused running agent keeps its
|
||||||
|
// container but drives no turns. Same all-or-nothing enablement rule
|
||||||
|
// as every other bulk action here: mixed pause state disables the
|
||||||
|
// button rather than silently no-op'ing on part of the selection.
|
||||||
|
const allUnpaused = selected.every((c) => !c.paused);
|
||||||
|
const allPaused = selected.every((c) => c.paused);
|
||||||
|
|
||||||
|
const stoppedNames = selected.filter((c) => !c.running).map((c) => c.name);
|
||||||
|
const runningNames = selected.filter((c) => c.running).map((c) => c.name);
|
||||||
|
const pausedNames = selected.filter((c) => c.paused).map((c) => c.name);
|
||||||
|
const unpausedNames = selected.filter((c) => !c.paused).map((c) => c.name);
|
||||||
|
|
||||||
|
function why(label, blockers) {
|
||||||
|
if (!blockers.length) return null;
|
||||||
|
return `${label} not available — ${blockers.join(", ")} ${blockers.length === 1 ? "is" : "are"} blocking it`;
|
||||||
|
}
|
||||||
|
|
||||||
|
addBulkButton(actions, "btn-restart", "↺ R3ST4RT", allRunning, selected, {
|
||||||
|
action: "/api/restart/",
|
||||||
|
confirm: (names) =>
|
||||||
|
`restart ${names.length} agent${names.length === 1 ? "" : "s"} (${names.join(", ")})?`,
|
||||||
|
graceful: true,
|
||||||
|
gracefulLabel:
|
||||||
|
"restart gracefully — let each agent finish its turn and flush state before the container restarts",
|
||||||
|
disabledTitle: why(
|
||||||
|
"↺ R3ST4RT",
|
||||||
|
stoppedNames.map((n) => `\`${n}\` is stopped`),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
addBulkButton(actions, "btn-stop", "■ ST0P", allRunning, selected, {
|
||||||
|
action: "/api/kill/",
|
||||||
|
confirm: (names) =>
|
||||||
|
`stop ${names.length} agent${names.length === 1 ? "" : "s"} (${names.join(", ")})?`,
|
||||||
|
confirmLabel: "■ stop",
|
||||||
|
graceful: true,
|
||||||
|
disabledTitle: why(
|
||||||
|
"■ ST0P",
|
||||||
|
stoppedNames.map((n) => `\`${n}\` is already stopped`),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
addBulkButton(actions, "btn-start", "▶ ST4RT", allStopped, selected, {
|
||||||
|
action: "/api/start/",
|
||||||
|
confirm: (names) =>
|
||||||
|
`start ${names.length} agent${names.length === 1 ? "" : "s"} (${names.join(", ")})?`,
|
||||||
|
disabledTitle: why(
|
||||||
|
"▶ ST4RT",
|
||||||
|
runningNames.map((n) => `\`${n}\` is already running`),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
addBulkButton(actions, "btn-pause", "⏸ P4US3", allUnpaused, selected, {
|
||||||
|
action: "/api/pause/",
|
||||||
|
confirm: (names) =>
|
||||||
|
`pause ${names.length} agent${names.length === 1 ? "" : "s"} (${names.join(", ")})? parks the turn loop — inbox messages queue unacked.`,
|
||||||
|
disabledTitle: why(
|
||||||
|
"⏸ P4US3",
|
||||||
|
pausedNames.map((n) => `\`${n}\` is already paused`),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
addBulkButton(actions, "btn-resume", "▶ R3SUM3", allPaused, selected, {
|
||||||
|
action: "/api/resume/",
|
||||||
|
confirm: (names) =>
|
||||||
|
`resume ${names.length} agent${names.length === 1 ? "" : "s"} (${names.join(", ")})? the turn loop restarts and drains queued messages.`,
|
||||||
|
disabledTitle: why(
|
||||||
|
"▶ R3SUM3",
|
||||||
|
unpausedNames.map((n) => `\`${n}\` is not paused`),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
addBulkButton(actions, "btn-rebuild", "↻ R3BU1LD", true, selected, {
|
||||||
|
action: "/api/rebuild/",
|
||||||
|
confirm: (names) =>
|
||||||
|
`rebuild ${names.length} agent${names.length === 1 ? "" : "s"} (${names.join(", ")})? hot-reloads each container.`,
|
||||||
|
});
|
||||||
|
addBulkButton(actions, "btn-destroy", "DESTR0Y", true, selected, {
|
||||||
|
action: "/api/destroy/",
|
||||||
|
confirm: (names) =>
|
||||||
|
`destroy ${names.length} agent${names.length === 1 ? "" : "s"} (${names.join(", ")})? containers are removed; state + creds kept.`,
|
||||||
|
});
|
||||||
|
addBulkButton(actions, "btn-destroy", "PURG3", true, selected, {
|
||||||
|
action: "/api/destroy/",
|
||||||
|
body: { purge: "on" },
|
||||||
|
confirm: (names) =>
|
||||||
|
`PURGE ${names.length} agent${names.length === 1 ? "" : "s"} (${names.join(", ")})? containers, config history, claude creds, and notes are all WIPED. no undo.`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function addBulkButton(parent, btnClass, label, enabled, selected, opts) {
|
||||||
|
const names = selected.map((c) => c.name);
|
||||||
|
const btn = el(
|
||||||
|
"button",
|
||||||
|
{
|
||||||
|
type: "button",
|
||||||
|
class: "btn " + btnClass,
|
||||||
|
},
|
||||||
|
label,
|
||||||
|
);
|
||||||
|
if (!enabled) {
|
||||||
|
btn.disabled = true;
|
||||||
|
if (opts.disabledTitle) btn.title = opts.disabledTitle;
|
||||||
|
}
|
||||||
|
btn.addEventListener("click", async () => {
|
||||||
|
if (btn.disabled) return;
|
||||||
|
const msg = opts.confirm(names);
|
||||||
|
let graceful = false;
|
||||||
|
if (msg) {
|
||||||
|
const r = await themedConfirm({
|
||||||
|
message: msg,
|
||||||
|
danger: true,
|
||||||
|
confirmLabel: opts.confirmLabel || "confirm",
|
||||||
|
checkboxes: opts.graceful
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
name: "graceful",
|
||||||
|
label:
|
||||||
|
opts.gracefulLabel ||
|
||||||
|
"stop gracefully — let each agent finish its turn and flush state before the container stops",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: [],
|
||||||
|
});
|
||||||
|
if (!r) return;
|
||||||
|
graceful = !!r.graceful;
|
||||||
|
}
|
||||||
|
btn.disabled = true;
|
||||||
|
const original = btn.innerHTML;
|
||||||
|
btn.innerHTML = '<span class="spinner">◐</span> ' + label;
|
||||||
|
const failures = [];
|
||||||
|
// Sequential POSTs to keep server-side serialisation predictable
|
||||||
|
// (rebuild_queue dedups but other endpoints don't); the loop is
|
||||||
|
// short — bulk selections are typically a handful of agents.
|
||||||
|
//
|
||||||
|
// `opts.action` is a path prefix and the agent name gets appended
|
||||||
|
// (lifecycle endpoints: /start/<name>, /rebuild/<name>). `opts.body`
|
||||||
|
// is a static object applied to every POST.
|
||||||
|
for (const name of names) {
|
||||||
|
const body = new URLSearchParams(opts.body || {});
|
||||||
|
const url =
|
||||||
|
opts.action +
|
||||||
|
encodeURIComponent(name) +
|
||||||
|
(graceful ? "?graceful=true" : "");
|
||||||
|
try {
|
||||||
|
const resp = await fetch(url, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||||
|
body,
|
||||||
|
redirect: "manual",
|
||||||
|
});
|
||||||
|
const ok =
|
||||||
|
resp.ok ||
|
||||||
|
resp.type === "opaqueredirect" ||
|
||||||
|
(resp.status >= 200 && resp.status < 400);
|
||||||
|
if (!ok) {
|
||||||
|
const text = await resp.text().catch(() => "");
|
||||||
|
failures.push(
|
||||||
|
`${name}: http ${resp.status}${text ? " — " + text.slice(0, 200) : ""}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
failures.push(`${name}: ${err}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.innerHTML = original;
|
||||||
|
if (failures.length) {
|
||||||
|
themedToast(
|
||||||
|
`${label} completed with ${failures.length} failure${failures.length === 1 ? "" : "s"}:\n\n` +
|
||||||
|
failures.join("\n"),
|
||||||
|
{ type: "error", duration: 0 },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Container-lifecycle events (ContainerStateChanged /
|
||||||
|
// ContainerRemoved / RebuildQueueChanged) flow over the existing
|
||||||
|
// SSE channel and update the derived stores live — no manual
|
||||||
|
// refresh needed.
|
||||||
|
});
|
||||||
|
parent.append(btn);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── tickers ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
// 30s ticker for agent status-age chips. Renderers stamp `data-set-at`
|
||||||
|
// (unix seconds) on the `.status-age` span. Keyed container rows persist
|
||||||
|
// across re-renders, so without this ticker the "(set N ago)" label would
|
||||||
|
// become stale as time passes. 30s granularity matches fmtAgeSecs precision
|
||||||
|
// (sub-minute values round to seconds, coarser above that).
|
||||||
|
setInterval(() => {
|
||||||
|
const now = Math.floor(Date.now() / 1000);
|
||||||
|
document.querySelectorAll(".status-age[data-set-at]").forEach((node) => {
|
||||||
|
const setAt = Number(node.dataset.setAt);
|
||||||
|
if (!Number.isFinite(setAt) || setAt === 0) return;
|
||||||
|
node.textContent = ` (set ${fmtAgeSecs(now - setAt)} ago)`;
|
||||||
|
});
|
||||||
|
}, 30_000);
|
||||||
|
|
@ -1,139 +0,0 @@
|
||||||
/* ─── /builds.html — BU1LDS page ────────────────────────────────────
|
|
||||||
The build lifecycle hub: rebuild queue + live log, meta inputs, and
|
|
||||||
build log history on one page.
|
|
||||||
|
|
||||||
Imports system-sections.css for rebuild queue + meta inputs + live-log
|
|
||||||
component styles; the build-logs-* rules for the BUILD L0GS list are
|
|
||||||
below. */
|
|
||||||
@import "./system-sections.css";
|
|
||||||
@import "@hive/shared/jobq-graph.css";
|
|
||||||
|
|
||||||
body.builds-shell {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.builds-tabbar {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.builds-main {
|
|
||||||
padding: 1.2em 1.5em 2em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.builds-pane[hidden] {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logs-toolbar {
|
|
||||||
display: flex;
|
|
||||||
gap: 0.6em;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 0.9em;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ─── build-log list ──────────────────────────────────────────────
|
|
||||||
Scrollable list of log header rows; clicking a row expands full
|
|
||||||
stdout+stderr inline. */
|
|
||||||
.build-logs-list {
|
|
||||||
list-style: none;
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
overflow-y: auto;
|
|
||||||
flex: 1 1 0;
|
|
||||||
min-height: 0;
|
|
||||||
}
|
|
||||||
.build-logs-item {
|
|
||||||
border-bottom: 1px solid var(--border);
|
|
||||||
padding: 0.15em 0;
|
|
||||||
}
|
|
||||||
.build-logs-row-btn {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 0.4em;
|
|
||||||
align-items: baseline;
|
|
||||||
width: 100%;
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
color: var(--fg);
|
|
||||||
font-family: inherit;
|
|
||||||
font-size: 0.85em;
|
|
||||||
text-align: left;
|
|
||||||
padding: 0.25em 0.4em;
|
|
||||||
cursor: pointer;
|
|
||||||
border-radius: 3px;
|
|
||||||
}
|
|
||||||
.build-logs-row-btn:hover {
|
|
||||||
background: var(--bg-elev);
|
|
||||||
}
|
|
||||||
.build-logs-kind {
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
.build-logs-age {
|
|
||||||
font-size: 0.88em;
|
|
||||||
}
|
|
||||||
.build-logs-cmdline {
|
|
||||||
color: var(--muted);
|
|
||||||
font-size: 0.82em;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
max-width: 40em;
|
|
||||||
}
|
|
||||||
.build-logs-detail {
|
|
||||||
padding: 0 0.4em 0.4em;
|
|
||||||
}
|
|
||||||
.build-logs-output {
|
|
||||||
margin: 0.3em 0 0;
|
|
||||||
padding: 0.4em 0.6em;
|
|
||||||
background: var(--crust);
|
|
||||||
color: var(--fg);
|
|
||||||
border: 1px solid var(--purple-dim);
|
|
||||||
font-size: 0.82em;
|
|
||||||
line-height: 1.4;
|
|
||||||
white-space: pre-wrap;
|
|
||||||
word-break: break-word;
|
|
||||||
overflow-x: auto;
|
|
||||||
max-height: 30em;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
.build-logs-dl {
|
|
||||||
color: var(--muted);
|
|
||||||
font-size: 0.8em;
|
|
||||||
text-decoration: none;
|
|
||||||
border: 1px solid var(--purple-dim);
|
|
||||||
padding: 0.1em 0.4em;
|
|
||||||
border-radius: 2px;
|
|
||||||
margin-top: 0.3em;
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
.build-logs-dl:not([hidden]) {
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
.build-logs-dl:hover {
|
|
||||||
color: var(--fg);
|
|
||||||
border-color: var(--purple-dim);
|
|
||||||
}
|
|
||||||
|
|
||||||
.build-logs-live-badge {
|
|
||||||
margin-bottom: 0.4em;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes live-pulse {
|
|
||||||
0%,
|
|
||||||
100% {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
opacity: 0.45;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.build-logs-live-badge.badge-running {
|
|
||||||
animation: live-pulse 1.4s ease-in-out infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
.build-logs-runtime {
|
|
||||||
font-size: 0.85em;
|
|
||||||
color: var(--muted);
|
|
||||||
}
|
|
||||||
|
|
@ -1,101 +0,0 @@
|
||||||
<!doctype html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
||||||
<title>hyperhive // BU1LDS</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/builds.css" />
|
|
||||||
</head>
|
|
||||||
<body class="builds-shell">
|
|
||||||
<!-- BU1LDS: the build lifecycle hub — rebuild queue, live build log,
|
|
||||||
meta inputs, and build log history on one page. Same minimal
|
|
||||||
chrome as /core.html — a `← home` back-link + a <hive-tab-strip>
|
|
||||||
sub-tab nav. Default sub-tab is the rebuild queue. -->
|
|
||||||
<header class="page-header">
|
|
||||||
<a class="page-back" href="/">← home</a>
|
|
||||||
<hive-tab-strip
|
|
||||||
class="hive-tabbar builds-tabbar"
|
|
||||||
id="builds-tabbar"
|
|
||||||
prefix="builds"
|
|
||||||
role="tablist"
|
|
||||||
></hive-tab-strip>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<main class="builds-main">
|
|
||||||
<!-- 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. Default tab. -->
|
|
||||||
<section
|
|
||||||
class="builds-pane"
|
|
||||||
id="builds-pane-rebuild"
|
|
||||||
data-tab-pane="rebuild"
|
|
||||||
role="tabpanel"
|
|
||||||
aria-labelledby="builds-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 SSE stream. -->
|
|
||||||
<div id="rebuild-live-log" class="rebuild-live-log" hidden></div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- M3T4 1NPUTS: select inputs to nix flake update in /meta/. -->
|
|
||||||
<section
|
|
||||||
class="builds-pane"
|
|
||||||
id="builds-pane-meta"
|
|
||||||
data-tab-pane="meta"
|
|
||||||
role="tabpanel"
|
|
||||||
aria-labelledby="builds-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 on first tab activation; auto-refreshes when
|
|
||||||
rebuild_queue_changed fires. Deep-link: ?id=N#buildlogs. -->
|
|
||||||
<section
|
|
||||||
class="builds-pane"
|
|
||||||
id="builds-pane-buildlogs"
|
|
||||||
data-tab-pane="buildlogs"
|
|
||||||
role="tabpanel"
|
|
||||||
aria-labelledby="builds-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>
|
|
||||||
</main>
|
|
||||||
|
|
||||||
<script type="module" src="/static/builds.js" defer></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|
@ -1,23 +1,17 @@
|
||||||
// BU1LDS page entry (/builds.html). The build lifecycle hub — rebuild
|
// The build lifecycle tabs of /dashboard.html — rebuild queue, live build
|
||||||
// queue, live build log, meta inputs, and build log history on one page.
|
// log, meta inputs, and build log history. Not an entry point: tabs.js
|
||||||
// Three sub-tabs via <hive-tab-strip> (default = Rebuild Queue):
|
// imports it, owns the tab strip and the dashboard event stream, and calls
|
||||||
// - R3BU1LD QU3U3: pending + running rebuilds + live build log inline
|
// the exports below. Three tabs, the panes' ids matching the tab ids:
|
||||||
// - M3T4 1NPUTS: nix flake input selector + meta-update trigger
|
// - rebuild (R3BU1LD QU3U3): pending + running rebuilds + live build log
|
||||||
// - BUILD L0GS: all-agents build log history (lazy-loaded on tab show)
|
// - meta (M3T4 1NPUTS): nix flake input selector + meta-update
|
||||||
|
// - buildlogs (BUILD L0GS): all-agents build log history, lazy-loaded
|
||||||
|
// the first time the tab is shown
|
||||||
|
|
||||||
import {
|
import { $, fmtAgeSecs, openBuildLogStream } from "./common.js";
|
||||||
$,
|
|
||||||
fmtAgeSecs,
|
|
||||||
openStream,
|
|
||||||
openBuildLogStream,
|
|
||||||
initServerWarnings,
|
|
||||||
} from "./common.js";
|
|
||||||
import { el } from "@hive/shared/dom.js";
|
import { el } from "@hive/shared/dom.js";
|
||||||
import { bindAsyncForms } from "@hive/shared/forms.js";
|
|
||||||
import { themedConfirm } from "@hive/shared/modal.js";
|
import { themedConfirm } from "@hive/shared/modal.js";
|
||||||
import { h, render } from "preact";
|
import { h, render } from "preact";
|
||||||
import { fmtAgo, fmtDuration, truncate } from "./util.js";
|
import { fmtAgo, fmtDuration, truncate } from "./util.js";
|
||||||
import "@hive/shared/hive-tab-strip.js";
|
|
||||||
import { JobqGraph } from "@hive/shared/jobq-graph.js";
|
import { JobqGraph } from "@hive/shared/jobq-graph.js";
|
||||||
|
|
||||||
// ─── derived state ───────────────────────────────────────────────────────────
|
// ─── derived state ───────────────────────────────────────────────────────────
|
||||||
|
|
@ -174,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 `builds.html` —
|
// placeholder `#rebuild-queue-section` starts with in `dashboard.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
|
||||||
|
|
@ -196,7 +190,6 @@ function renderRebuildQueue() {
|
||||||
onUpdate: (nodes) => {
|
onUpdate: (nodes) => {
|
||||||
jobqNodes = nodes || [];
|
jobqNodes = nodes || [];
|
||||||
renderRebuildLiveLog();
|
renderRebuildLiveLog();
|
||||||
updateRebuildCount();
|
|
||||||
},
|
},
|
||||||
onCancel: async (id) => {
|
onCancel: async (id) => {
|
||||||
const node = jobqNodes.find((n) => n.id === id);
|
const node = jobqNodes.find((n) => n.id === id);
|
||||||
|
|
@ -384,33 +377,18 @@ function renderRebuildLiveLog() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── rebuild-queue count pill ─────────────────────────────────────────────────
|
// ─── rebuild-queue count pill ─────────────────────────────────────────────────
|
||||||
function updateRebuildCount() {
|
// The queue's in-flight count, for the R3BU1LD QU3U3 tab's pill (tabs.js
|
||||||
const pill = $("builds-tab-count-rebuild");
|
// reads it on its refresh tick). Pending/Running/Finishing = in flight
|
||||||
if (!pill) return;
|
// (Finishing = own work done, a sub-node still running — still counts).
|
||||||
// Pending/Running/Finishing = in flight (Finishing = own work done, a
|
// Root nodes only: each is one queue entry.
|
||||||
// sub-node still running — still counts). Root nodes only: each is one
|
export function rebuildCount() {
|
||||||
// queue entry.
|
return jobqNodes.filter(
|
||||||
const n = jobqNodes.filter(
|
|
||||||
(n) =>
|
(n) =>
|
||||||
n.parent == null &&
|
n.parent == null &&
|
||||||
(n.state === "Pending" ||
|
(n.state === "Pending" ||
|
||||||
n.state === "Running" ||
|
n.state === "Running" ||
|
||||||
n.state === "Finishing"),
|
n.state === "Finishing"),
|
||||||
).length;
|
).length;
|
||||||
if (n > 0) {
|
|
||||||
pill.textContent = String(n);
|
|
||||||
pill.hidden = false;
|
|
||||||
} else {
|
|
||||||
pill.hidden = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── render-all (cold load + any full re-render) ──────────────────────────────
|
|
||||||
function renderAll() {
|
|
||||||
renderMetaInputs({ meta_inputs: metaInputsState });
|
|
||||||
// Rebuild-queue rendering + the count-pill/live-log it drives all happen
|
|
||||||
// off JobqGraph's own onUpdate callback (see mountRebuildQueue) —
|
|
||||||
// nothing to render here directly.
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── BUILD L0GS tab ───────────────────────────────────────────────────────────
|
// ─── BUILD L0GS tab ───────────────────────────────────────────────────────────
|
||||||
|
|
@ -419,10 +397,7 @@ function renderAll() {
|
||||||
// 2s) when rebuild_queue_changed fires while the BUILD L0GS tab is active.
|
// 2s) when rebuild_queue_changed fires while the BUILD L0GS tab is active.
|
||||||
// Deep-link: ?id=N#buildlogs auto-expands the target row.
|
// Deep-link: ?id=N#buildlogs auto-expands the target row.
|
||||||
|
|
||||||
const buildList = $("build-list");
|
|
||||||
const buildRefresh = $("build-refresh");
|
|
||||||
let buildLogsLoaded = false;
|
let buildLogsLoaded = false;
|
||||||
let buildTabs; // set in init
|
|
||||||
|
|
||||||
function fmtTs(unixSecs) {
|
function fmtTs(unixSecs) {
|
||||||
if (!unixSecs) return "";
|
if (!unixSecs) return "";
|
||||||
|
|
@ -431,6 +406,7 @@ function fmtTs(unixSecs) {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchBuild() {
|
async function fetchBuild() {
|
||||||
|
const buildList = $("build-list");
|
||||||
if (!buildList) return;
|
if (!buildList) return;
|
||||||
buildList.replaceChildren();
|
buildList.replaceChildren();
|
||||||
buildList.append(el("p", { class: "meta" }, "loading…"));
|
buildList.append(el("p", { class: "meta" }, "loading…"));
|
||||||
|
|
@ -612,93 +588,54 @@ async function fetchBuild() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (buildRefresh) buildRefresh.addEventListener("click", fetchBuild);
|
// ─── entry points for tabs.js ─────────────────────────────────────────────────
|
||||||
|
// Everything below is called by tabs.js, which owns the tab strip, the
|
||||||
|
// `/api/state` snapshot and the dashboard event stream.
|
||||||
|
|
||||||
// ─── SSE handlers ─────────────────────────────────────────────────────────────
|
/** Mount the rebuild queue and wire the build-log refresh button. Called
|
||||||
let buildRefreshTimer = null;
|
* once, after the tab strip exists. The queue self-fetches on mount
|
||||||
const SSE_HANDLERS = {
|
* (JobqGraph's own effect), unlike the meta inputs, which wait for the
|
||||||
rebuild_queue_changed() {
|
* snapshot. */
|
||||||
// No `ev.queue` payload read anymore — this event still carries its own
|
export function initBuilds() {
|
||||||
// queue snapshot on the wire (tabs.js/SW4RM still consumes it for the
|
const refresh = $("build-refresh");
|
||||||
// swarm badges, untouched by this), this page just treats it as a
|
if (refresh) refresh.addEventListener("click", fetchBuild);
|
||||||
// refetch trigger. `JobqGraph` owns the actual fetch.
|
mountRebuildQueue();
|
||||||
jobqGraphToken += 1;
|
|
||||||
renderRebuildQueue();
|
|
||||||
// Auto-refresh build log list when the queue changes and BUILD L0GS is active.
|
|
||||||
if (buildTabs && buildTabs.active() === "buildlogs") {
|
|
||||||
if (buildRefreshTimer) clearTimeout(buildRefreshTimer);
|
|
||||||
buildRefreshTimer = setTimeout(fetchBuild, 2000);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
meta_inputs_changed(ev) {
|
|
||||||
metaInputsState = (ev.inputs || []).slice();
|
|
||||||
renderMetaInputs({ meta_inputs: metaInputsState });
|
|
||||||
},
|
|
||||||
meta_update_running(ev) {
|
|
||||||
metaUpdateRunning = !!ev.running;
|
|
||||||
renderMetaInputs({ meta_inputs: metaInputsState });
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// ─── boot ─────────────────────────────────────────────────────────────────────
|
|
||||||
async function refreshState() {
|
|
||||||
try {
|
|
||||||
const resp = await fetch("/api/state");
|
|
||||||
if (resp.ok) syncFromSnapshot(await resp.json());
|
|
||||||
} catch {
|
|
||||||
// best-effort
|
|
||||||
}
|
|
||||||
renderAll();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function init() {
|
/** The meta inputs, from an `/api/state` snapshot. */
|
||||||
initServerWarnings();
|
export function syncBuildsFromSnapshot(s) {
|
||||||
bindAsyncForms(() => refreshState());
|
syncFromSnapshot(s);
|
||||||
// Self-fetches on mount (JobqGraph's own effect) — no explicit initial
|
renderMetaInputs({ meta_inputs: metaInputsState });
|
||||||
// fetch needed here, unlike meta inputs below.
|
}
|
||||||
mountRebuildQueue();
|
|
||||||
|
|
||||||
buildTabs = document.getElementById("builds-tabbar").configure({
|
/** A tab was shown: BUILD L0GS loads its list the first time. */
|
||||||
tabs: [
|
export function onBuildsTabShown(tab) {
|
||||||
{
|
if (tab === "buildlogs" && !buildLogsLoaded) {
|
||||||
id: "rebuild",
|
|
||||||
label: "R3BU1LD QU3U3",
|
|
||||||
badgeId: "builds-tab-count-rebuild",
|
|
||||||
},
|
|
||||||
{ id: "meta", label: "M3T4 1NPUTS" },
|
|
||||||
{ id: "buildlogs", label: "BUILD L0GS" },
|
|
||||||
],
|
|
||||||
defaultId: "rebuild",
|
|
||||||
onShow: (id) => {
|
|
||||||
if (id === "buildlogs" && !buildLogsLoaded) {
|
|
||||||
buildLogsLoaded = true;
|
buildLogsLoaded = true;
|
||||||
fetchBuild();
|
fetchBuild();
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
});
|
|
||||||
|
|
||||||
await refreshState();
|
let buildRefreshTimer = null;
|
||||||
|
|
||||||
// kinds= matches SSE_HANDLERS below verbatim — narrows this from all
|
/** `rebuild_queue_changed`: a payload-less refetch trigger — `JobqGraph`
|
||||||
// 13 wire kinds down to the 3 this page acts on. This page was one
|
* owns the actual fetch. */
|
||||||
// of 4 unfiltered `/api/dashboard/stream` subscribers before this
|
export function onRebuildQueueChanged() {
|
||||||
// (subscription discipline, part 1 of the dashboard-event-stream-
|
jobqGraphToken += 1;
|
||||||
// split issue).
|
renderRebuildQueue();
|
||||||
const es = openStream(
|
// Auto-refresh the build log list while BUILD L0GS is the active tab.
|
||||||
"/api/dashboard/stream?kinds=rebuild_queue_changed,meta_inputs_changed,meta_update_running",
|
if (document.body.dataset.activeTab === "buildlogs") {
|
||||||
);
|
if (buildRefreshTimer) clearTimeout(buildRefreshTimer);
|
||||||
if (es) {
|
buildRefreshTimer = setTimeout(fetchBuild, 2000);
|
||||||
es.onmessage = (e) => {
|
|
||||||
let ev;
|
|
||||||
try {
|
|
||||||
ev = JSON.parse(e.data);
|
|
||||||
} catch {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const h = SSE_HANDLERS[ev.kind];
|
|
||||||
if (h) h(ev);
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
init();
|
export function onMetaInputsChanged(ev) {
|
||||||
|
metaInputsState = (ev.inputs || []).slice();
|
||||||
|
renderMetaInputs({ meta_inputs: metaInputsState });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function onMetaUpdateRunning(ev) {
|
||||||
|
metaUpdateRunning = !!ev.running;
|
||||||
|
renderMetaInputs({ meta_inputs: metaInputsState });
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
(swarm.js/core.js/builds.js). Used on container rows and build-log
|
(agents.js/core.js/builds.js). Used on container rows and build-log
|
||||||
rows. */
|
rows. */
|
||||||
.badge-warn {
|
.badge-warn {
|
||||||
color: var(--amber);
|
color: var(--amber);
|
||||||
|
|
|
||||||
|
|
@ -292,7 +292,7 @@ export function openStream(url) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stream a build log into a <pre>, returning the EventSource. Used by the
|
// Stream a build log into a <pre>, returning the EventSource. Used by the
|
||||||
// BU1LDS page's BUILD L0GS tab. Appends `stdout_append` then
|
// BUILD L0GS tab. Appends `stdout_append` then
|
||||||
// `stderr_append` (one `--- stderr ---` separator) frames from
|
// `stderr_append` (one `--- stderr ---` separator) frames from
|
||||||
// `GET /api/build-logs/id/{id}/stream`; auto-scrolls to the bottom unless the operator scrolled up; on the terminal `done` frame
|
// `GET /api/build-logs/id/{id}/stream`; auto-scrolls to the bottom unless the operator scrolled up; on the terminal `done` frame
|
||||||
// closes the stream and calls `onDone(status)`; on a non-transient error
|
// closes the stream and calls `onDone(status)`; on a non-transient error
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
/* ─── /core.html — C0R3 page ───────────────────────────────────────
|
/* ─── /core.html — C0R3 page ───────────────────────────────────────
|
||||||
Kept state (tombstones) and stale permission entries. Section rules
|
Kept state (tombstones) and stale permission entries. Section rules
|
||||||
imported from system-sections.css (shared with dashboard.css +
|
imported from system-sections.css (shared with dashboard.css). */
|
||||||
builds.css). */
|
|
||||||
@import "./system-sections.css";
|
@import "./system-sections.css";
|
||||||
|
|
||||||
body.core-shell {
|
body.core-shell {
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,8 @@
|
||||||
<!-- C0R3: the host/coordinator surface. Standalone page (served at
|
<!-- C0R3: the host/coordinator surface. Standalone page (served at
|
||||||
/core.html) carved out of the dashboard's old SYST3M tab so the
|
/core.html) carved out of the dashboard's old SYST3M tab so the
|
||||||
dashboard tab strip stays lean: kept state (tombstones) and the
|
dashboard tab strip stays lean: kept state (tombstones) and the
|
||||||
stale permission entries. The section <div> ids (tombstones-section,
|
stale permission entries. Rebuild queue + meta inputs are tabs of
|
||||||
|
/dashboard.html. The section <div> ids (tombstones-section,
|
||||||
tombstones-stale-perms) match what core.js's renderers target. Hive
|
tombstones-stale-perms) match what core.js's renderers target. Hive
|
||||||
infra containers (`hive-forge`, `hive-gateway`, `hive-matrix`) have
|
infra containers (`hive-forge`, `hive-gateway`, `hive-matrix`) have
|
||||||
no dashboard panel — `hivectl stop`/`start`/`restart` is the only
|
no dashboard panel — `hivectl stop`/`start`/`restart` is the only
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,8 @@
|
||||||
// standalone page so the dashboard tab strip stays lean: kept state
|
// standalone page so the dashboard tab strip stays lean: kept state
|
||||||
// (tombstones) and the stale permission entries.
|
// (tombstones) and the stale permission entries.
|
||||||
//
|
//
|
||||||
// This page is its own esbuild bundle with its own runtime: it cold-loads
|
// 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
|
// /api/state and subscribes to /dashboard/stream (the same broker event
|
||||||
// channel the dashboard uses), maintaining its own copy of the tombstones
|
// channel the dashboard uses), maintaining its own copy of the tombstones
|
||||||
// state.
|
// state.
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@
|
||||||
pulls them in here. */
|
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";
|
||||||
|
|
||||||
/* ─── tabbed dashboard chrome ──────────────────────────────────────
|
/* ─── tabbed dashboard chrome ──────────────────────────────────────
|
||||||
Top-of-page sticky header with banner + tab strip. SSE stays
|
Top-of-page sticky header with banner + tab strip. SSE stays
|
||||||
|
|
@ -125,10 +126,15 @@ body.dashboard-shell {
|
||||||
font-variant-numeric: tabular-nums;
|
font-variant-numeric: tabular-nums;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
/* `display` above beats the UA's `[hidden]` rule, so an empty count (the
|
||||||
|
4G3NTS and R3BU1LD QU3U3 pills at zero) would otherwise draw a "0". */
|
||||||
|
.tab-count[hidden] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
/* Tab pane visibility — createTabStrip (@hive/shared/tabs.js) sets the
|
/* Tab pane visibility — createTabStrip (@hive/shared/tabs.js) sets the
|
||||||
`hidden` attribute on every inactive pane (resolved from the URL hash;
|
`hidden` attribute on every inactive pane (resolved from the URL hash;
|
||||||
default — no hash — is SW4RM). Only the active pane stays visible. */
|
default — no hash — is 4G3NTS). Only the active pane stays visible. */
|
||||||
.tab-pane[hidden] {
|
.tab-pane[hidden] {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
@ -316,7 +322,7 @@ hive-agent-menu {
|
||||||
--menu-btn-opacity: 1;
|
--menu-btn-opacity: 1;
|
||||||
}
|
}
|
||||||
/* Card actions dim while a row has any pending-state badge (transient-
|
/* Card actions dim while a row has any pending-state badge (transient-
|
||||||
driven only now — see swarm.js's pending-badge derivation comment;
|
driven only now — see agents.js's pending-badge derivation comment;
|
||||||
there is no separate queued-but-not-running row state to tell apart
|
there is no separate queued-but-not-running row state to tell apart
|
||||||
from this one anymore). */
|
from this one anymore). */
|
||||||
.container-row.pending-running .actions {
|
.container-row.pending-running .actions {
|
||||||
|
|
@ -437,12 +443,12 @@ hive-agent-menu {
|
||||||
shared <hive-warn> component — amber/no-pulse for
|
shared <hive-warn> component — amber/no-pulse for
|
||||||
the standing caveat (a pulsing banner that's always present just
|
the standing caveat (a pulsing banner that's always present just
|
||||||
teaches you to stop seeing it), `severity="red" pulse` for the port
|
teaches you to stop seeing it), `severity="red" pulse` for the port
|
||||||
collision (an incident, not a caveat). See core.js/swarm.js and
|
collision (an incident, not a caveat). See core.js/agents.js and
|
||||||
hive-warn.css for the actual styling now. */
|
hive-warn.css for the actual styling now. */
|
||||||
|
|
||||||
/* Build-queue summary banner on the SW4RM tab moved to the shared
|
/* Build-queue summary banner on the 4G3NTS tab moved to the shared
|
||||||
`JobqRollup` Preact component (`.jqr-*` classes, imported above) —
|
`JobqRollup` Preact component (`.jqr-*` classes, imported above) —
|
||||||
mounted by swarm.js::initJobqRollup into #jobq-rollup-section. */
|
mounted by agents.js::initJobqRollup into #jobq-rollup-section. */
|
||||||
|
|
||||||
.pending-state {
|
.pending-state {
|
||||||
color: var(--amber);
|
color: var(--amber);
|
||||||
|
|
@ -772,3 +778,120 @@ body.dashboard-shell.has-selection {
|
||||||
/* ST4TS moved to its own page (`/stats.html`): the
|
/* ST4TS moved to its own page (`/stats.html`): the
|
||||||
window selector / summary chips / bars live in stats.css, and the
|
window selector / summary chips / bars live in stats.css, and the
|
||||||
shared `.hive-stats-table` moved to common.css. */
|
shared `.hive-stats-table` moved to common.css. */
|
||||||
|
|
||||||
|
/* ─── build tabs (builds.js) ────────────────────────────────────────
|
||||||
|
R3BU1LD QU3U3 and M3T4 1NPUTS are styled by system-sections.css and
|
||||||
|
jobq-graph.css; the BUILD L0GS list rules are below. */
|
||||||
|
|
||||||
|
.logs-toolbar {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.6em;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 0.9em;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── build-log list ──────────────────────────────────────────────
|
||||||
|
Scrollable list of log header rows; clicking a row expands full
|
||||||
|
stdout+stderr inline. */
|
||||||
|
.build-logs-list {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
overflow-y: auto;
|
||||||
|
flex: 1 1 0;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
.build-logs-item {
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
padding: 0.15em 0;
|
||||||
|
}
|
||||||
|
.build-logs-row-btn {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.4em;
|
||||||
|
align-items: baseline;
|
||||||
|
width: 100%;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: var(--fg);
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 0.85em;
|
||||||
|
text-align: left;
|
||||||
|
padding: 0.25em 0.4em;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
.build-logs-row-btn:hover {
|
||||||
|
background: var(--bg-elev);
|
||||||
|
}
|
||||||
|
.build-logs-kind {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.build-logs-age {
|
||||||
|
font-size: 0.88em;
|
||||||
|
}
|
||||||
|
.build-logs-cmdline {
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 0.82em;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
max-width: 40em;
|
||||||
|
}
|
||||||
|
.build-logs-detail {
|
||||||
|
padding: 0 0.4em 0.4em;
|
||||||
|
}
|
||||||
|
.build-logs-output {
|
||||||
|
margin: 0.3em 0 0;
|
||||||
|
padding: 0.4em 0.6em;
|
||||||
|
background: var(--crust);
|
||||||
|
color: var(--fg);
|
||||||
|
border: 1px solid var(--purple-dim);
|
||||||
|
font-size: 0.82em;
|
||||||
|
line-height: 1.4;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-word;
|
||||||
|
overflow-x: auto;
|
||||||
|
max-height: 30em;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
.build-logs-dl {
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 0.8em;
|
||||||
|
text-decoration: none;
|
||||||
|
border: 1px solid var(--purple-dim);
|
||||||
|
padding: 0.1em 0.4em;
|
||||||
|
border-radius: 2px;
|
||||||
|
margin-top: 0.3em;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.build-logs-dl:not([hidden]) {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
.build-logs-dl:hover {
|
||||||
|
color: var(--fg);
|
||||||
|
border-color: var(--purple-dim);
|
||||||
|
}
|
||||||
|
|
||||||
|
.build-logs-live-badge {
|
||||||
|
margin-bottom: 0.4em;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes live-pulse {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 0.45;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.build-logs-live-badge.badge-running {
|
||||||
|
animation: live-pulse 1.4s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.build-logs-runtime {
|
||||||
|
font-size: 0.85em;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,20 +26,51 @@
|
||||||
<nav class="tabbar" id="tabbar" role="tablist">
|
<nav class="tabbar" id="tabbar" role="tablist">
|
||||||
<a
|
<a
|
||||||
class="tab"
|
class="tab"
|
||||||
id="tab-swarm"
|
id="tab-agents"
|
||||||
href="#swarm"
|
href="#agents"
|
||||||
role="tab"
|
role="tab"
|
||||||
aria-controls="tab-pane-swarm"
|
aria-controls="tab-pane-agents"
|
||||||
data-tab="swarm"
|
data-tab="agents"
|
||||||
>
|
>
|
||||||
<span class="tab-label">◆ SW4RM ◆</span>
|
<span class="tab-label">◆ 4G3NTS ◆</span>
|
||||||
<span class="tab-count" id="tab-count-swarm" hidden></span>
|
<span class="tab-count" id="tab-count-agents" hidden></span>
|
||||||
</a>
|
</a>
|
||||||
<!-- SYST3M moved to its own standalone page (/core.html, "C0R3"),
|
<!-- SYST3M moved to its own standalone page (/core.html, "C0R3"),
|
||||||
reached from the H0M3 hub — keeps the dashboard tab strip lean.
|
reached from the H0M3 hub — keeps the dashboard tab strip lean.
|
||||||
The rebuild-queue STATE still lives in tabs.js (it drives the
|
The build lifecycle is three tabs here (builds.js): the rebuild
|
||||||
"building…" badges on SW4RM agent cards); only the SYST3M tab +
|
queue with its live log, the meta flake inputs, and the build log
|
||||||
its section renderers moved out. -->
|
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>
|
||||||
<!-- P3RM1SS10NS: per-agent capability grants and tool-group
|
<!-- P3RM1SS10NS: per-agent capability grants and tool-group
|
||||||
assignments. Both tables are fetched on tab activation. -->
|
assignments. Both tables are fetched on tab activation. -->
|
||||||
<a
|
<a
|
||||||
|
|
@ -58,7 +89,7 @@
|
||||||
view, not part of the operational tab strip. -->
|
view, not part of the operational tab strip. -->
|
||||||
|
|
||||||
<!-- (Peer hives are no longer a tab — they render as a headline
|
<!-- (Peer hives are no longer a tab — they render as a headline
|
||||||
under SW4RM. See tab-pane-swarm.) -->
|
under 4G3NTS. See tab-pane-agents.) -->
|
||||||
|
|
||||||
<!-- FL0W and the other standalone pages are reachable from the
|
<!-- FL0W and the other standalone pages are reachable from the
|
||||||
H0M3 hub (served at /), not from the dashboard tab strip —
|
H0M3 hub (served at /), not from the dashboard tab strip —
|
||||||
|
|
@ -68,33 +99,33 @@
|
||||||
|
|
||||||
<!-- Tab panes. createTabStrip (@hive/shared/tabs.js) keeps exactly one
|
<!-- Tab panes. createTabStrip (@hive/shared/tabs.js) keeps exactly one
|
||||||
visible by toggling the `hidden` attribute, resolved from the URL
|
visible by toggling the `hidden` attribute, resolved from the URL
|
||||||
hash (default SW4RM). Panes start `hidden` to avoid a flash before
|
hash (default 4G3NTS). Panes start `hidden` to avoid a flash before
|
||||||
the script runs. -->
|
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
|
||||||
sticky chrome above + the footer below span the full width. -->
|
sticky chrome above + the footer below span the full width. -->
|
||||||
<div class="page-content">
|
<div class="page-content">
|
||||||
<main class="dashboard-main">
|
<main class="dashboard-main">
|
||||||
<!-- SW4RM: the swarm itself. Container cards (the central thing
|
<!-- 4G3NTS: the agents. Container cards (the central thing
|
||||||
the operator looks at) and rebuild queue / cascade visualisation
|
the operator looks at) and rebuild queue / cascade visualisation
|
||||||
that drives them. No inline `C0NTAINERS` h2 heading + divider
|
that drives them. No inline `C0NTAINERS` h2 heading + divider
|
||||||
— the tab label SW4RM already says it. -->
|
— the tab label 4G3NTS already says it. -->
|
||||||
<section
|
<section
|
||||||
class="tab-pane"
|
class="tab-pane"
|
||||||
id="tab-pane-swarm"
|
id="tab-pane-agents"
|
||||||
data-tab-pane="swarm"
|
data-tab-pane="agents"
|
||||||
hidden
|
hidden
|
||||||
role="tabpanel"
|
role="tabpanel"
|
||||||
aria-labelledby="tab-swarm"
|
aria-labelledby="tab-agents"
|
||||||
>
|
>
|
||||||
<!-- Swarm / hive identity headline. Populated by refreshState from
|
<!-- Swarm / hive identity headline. Populated by refreshState from
|
||||||
hive_name + swarm_name; stays hidden when neither is set. -->
|
hive_name + swarm_name; stays hidden when neither is set. -->
|
||||||
<h2 id="swarm-identity" hidden></h2>
|
<h2 id="swarm-identity" hidden></h2>
|
||||||
<!-- JobqRollup mount point, kept outside #containers-section
|
<!-- JobqRollup mount point, kept outside #containers-section
|
||||||
deliberately — that section is wiped + rebuilt on every
|
deliberately — that section is wiped + rebuilt on every
|
||||||
container-list render (see swarm.js::renderContainers), which
|
container-list render (see agents.js::renderContainers), which
|
||||||
would tear down and remount the Preact tree on every
|
would tear down and remount the Preact tree on every
|
||||||
container-state tick. Mounted once by swarm.js::initJobqRollup,
|
container-state tick. Mounted once by agents.js::initJobqRollup,
|
||||||
refreshed via its own handle rather than by re-rendering. -->
|
refreshed via its own handle rather than by re-rendering. -->
|
||||||
<div id="jobq-rollup-section"></div>
|
<div id="jobq-rollup-section"></div>
|
||||||
<div id="containers-section">
|
<div id="containers-section">
|
||||||
|
|
@ -102,9 +133,77 @@
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- SYST3M pane moved to /core.html (the standalone "C0R3" page):
|
<!-- R3BU1LD QU3U3: pending + running rebuilds, meta-updates, and
|
||||||
kept state and stale permissions. Reached
|
first-spawns. Rendered from GET /api/jobq/graph by the
|
||||||
from the H0M3 hub. -->
|
`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>
|
||||||
|
|
||||||
<!-- P3RM1SS10NS: per-agent capability grants + tool-group
|
<!-- P3RM1SS10NS: per-agent capability grants + tool-group
|
||||||
assignments. Both tables are column-driven from the backend
|
assignments. Both tables are column-driven from the backend
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,7 @@
|
||||||
<span class="home-tile-label">Dashboard</span>
|
<span class="home-tile-label">Dashboard</span>
|
||||||
</span>
|
</span>
|
||||||
<span class="home-tile-desc"
|
<span class="home-tile-desc"
|
||||||
>containers · permissions · system</span
|
>containers · builds · permissions</span
|
||||||
>
|
>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
|
@ -54,16 +54,6 @@
|
||||||
<span class="home-tile-desc">live all-agents message firehose</span>
|
<span class="home-tile-desc">live all-agents message firehose</span>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a class="home-tile" href="/builds.html">
|
|
||||||
<span class="home-tile-head">
|
|
||||||
<span class="home-tile-icon" aria-hidden="true">🔨</span>
|
|
||||||
<span class="home-tile-label">Builds</span>
|
|
||||||
</span>
|
|
||||||
<span class="home-tile-desc"
|
|
||||||
>rebuild queue · meta inputs · build logs</span
|
|
||||||
>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<a class="home-tile" href="/stats.html">
|
<a class="home-tile" href="/stats.html">
|
||||||
<span class="home-tile-head">
|
<span class="home-tile-head">
|
||||||
<span class="home-tile-icon" aria-hidden="true">📊</span>
|
<span class="home-tile-icon" aria-hidden="true">📊</span>
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue