Watch
0
0
Fork
You've already forked hyperhive
0

dashboard: remove the L0GS page and its journal backend

Logs live at swarm level now (VictoriaLogs, read through Grafana
Explore). The hive dashboard's own journal viewer goes, with
everything that existed only to serve it:

- frontend: /logs.html, logs.js, logs.css, the H0M3 "Logs" tile and
  the per-agent menu's "journal logs ->" link. The build-log list
  rules BU1LDS used from logs.css move into builds.css.
- hive-c0re: GET /api/journal/{name} and GET /api/journal-host, and
  priv_client::read_container_journal.
- hive-priv / hive-priv-sock: the ReadContainerJournal request, its
  JournalQuery/JournalOutput wire types and the journalctl -M executor.
- nix: hive-c0re's systemd-journal supplementary group, whose only
  reader was the journal-host handler.
- docs: the L0GS section, the API entry and the security table row.

Closes #5113
Closes #3819
This commit is contained in:
atlas 2026-10-12 00:17:23 +02:00 • committed by mara
commit 884fb3e08b
31 changed files with 192 additions and 1231 deletions

View file

@ -247,7 +247,7 @@ bind-mount compatibility with user namespace UID mapping first.
`hive-c0re` runs as the unprivileged system user `hive-core` `hive-c0re` runs as the unprivileged system user `hive-core`
(`/var/lib/hyperhive` owned by `hive-core:hive-core`). It can't (`/var/lib/hyperhive` owned by `hive-core:hive-core`). It can't
directly invoke `nixos-container`, `journalctl -M`, or act on a system directly invoke `nixos-container` or act on a system
unit (`systemctl reload nginx`) — those require root. `hive-priv` fills unit (`systemctl reload nginx`) — those require root. `hive-priv` fills
this gap. this gap.
@ -275,7 +275,6 @@ known operations; there is no arbitrary command pass-through:
| `CreateContainer` / `UpdateContainer` | `nixos-container create/update <name> --flake <ref>` | | `CreateContainer` / `UpdateContainer` | `nixos-container create/update <name> --flake <ref>` |
| `DestroyContainer` | `nixos-container destroy <name>` | | `DestroyContainer` | `nixos-container destroy <name>` |
| `ListContainers` | `nixos-container list` | | `ListContainers` | `nixos-container list` |
| `ReadContainerJournal` | `journalctl -M <container> -n <n> [filters...]` |
| `ReloadGatewayNginx` | `systemctl reload/start/reset-failed nginx` (host unit; the unit name is hard-coded, not a parameter) | | `ReloadGatewayNginx` | `systemctl reload/start/reset-failed nginx` (host unit; the unit name is hard-coded, not a parameter) |
| `WriteNspawnFlags` | write `/etc/nixos-containers/<container>.conf` (bind-mount list + network isolation vars) | | `WriteNspawnFlags` | write `/etc/nixos-containers/<container>.conf` (bind-mount list + network isolation vars) |
| `EnsureAgentSocketDir` | `mkdirat` `/run/hive-agent/<name>` `0751 root:root` under an `O_NOFOLLOW` parent fd; leaves an existing directory alone, refuses anything else | | `EnsureAgentSocketDir` | `mkdirat` `/run/hive-agent/<name>` `0751 root:root` under an `O_NOFOLLOW` parent fd; leaves an existing directory alone, refuses anything else |
@ -313,8 +312,8 @@ writes to only the paths `hive-priv` legitimately needs.
### Privilege boundary summary ### Privilege boundary summary
| Component | Runs as | Privilege needed for | | Component | Runs as | Privilege needed for |
| ---------------------------- | -------------- | ------------------------------------------------------------ | | ---------------------------- | -------------- | --------------------------------------------- |
| `hive-c0re` | `hive-core` | broker, HTTP dashboard | | `hive-c0re` | `hive-core` | broker, HTTP dashboard |
| `hive-priv` | `root` | container lifecycle, journal reads, bind mounts, cred writes | | `hive-priv` | `root` | container lifecycle, bind mounts, cred writes |
| `hive-ag3nt` (per-container) | per-agent user | turn execution, MCP serving | | `hive-ag3nt` (per-container) | per-agent user | turn execution, MCP serving |

View file

@ -28,8 +28,7 @@ of your time. It's a single page with exactly four tabs:
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), **Logs** (`/logs.html`, per-agent and host the whole swarm), **Stats** (`/stats.html`, swarm-wide usage stats),
journals), **Stats** (`/stats.html`, swarm-wide usage stats),
**Builds** (`/builds.html`, the rebuild queue and build history), and **Builds** (`/builds.html`, the rebuild queue and build history), and
**Core** (`/core.html`, tombstones and container resource use). Your local **Core** (`/core.html`, tombstones and container resource use). Your local
browser preferences (notifications) live in the dashboard's Y3R C4LL browser preferences (notifications) live in the dashboard's Y3R C4LL
@ -72,9 +71,10 @@ merging a config change or bumping a flake input.
takes effect until you hit **save all** at the bottom of the tab; a takes effect until you hit **save all** at the bottom of the tab; a
save queues a rebuild for whichever agents actually changed. save queues a rebuild for whichever agents actually changed.
**Read an agent's logs.** The Logs page's AGENT tab pulls a live **Read an agent's logs.** Logs aren't in the hive dashboard. Every
journald view for any agent + service; the per-agent `⋮` menu's hive ships its agents', daemons' and infra containers' journals to the
**journal logs →** link jumps straight there, pre-filtered. swarm's VictoriaLogs; read them in Grafana's **Explore** — see
[`swarm/services.md`](../swarm/services.md#logs-victorialogs).
**Set up a schedule.** Scheduled prompts are swarm-level: set them with **Set up a schedule.** Scheduled prompts are swarm-level: set them with
`swarmctl schedule` ([`swarm/schedules.md`](../swarm/schedules.md)). `swarmctl schedule` ([`swarm/schedules.md`](../swarm/schedules.md)).

View file

@ -12,7 +12,7 @@ swap touches only the first:
what the app references and it **never changes on a swap**. what the app references and it **never changes on a swap**.
Both files live in `frontend/packages/shared/src/`. Per-page stylesheets Both files live in `frontend/packages/shared/src/`. Per-page stylesheets
(`common.css`, `dashboard.css`, `flow.css`, `logs.css`, `agent.css`) (`common.css`, `dashboard.css`, `flow.css`, `agent.css`)
reference the **semantic** names and must **not** redeclare them or reach reference the **semantic** names and must **not** redeclare them or reach
for the raw `--baseNN` slots directly. (`base.css` holds only the shared for the raw `--baseNN` slots directly. (`base.css` holds only the shared
`body` typography — it references the palette but no longer defines it.) `body` typography — it references the palette but no longer defines it.)

View file

@ -12,7 +12,7 @@ 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`, `#call`, one tab pane at a time. The URL hash (`#swarm`, `#call`,
`#permissions`, `#schedules`) drives which pane is `#permissions`, `#schedules`) drives which pane is
active; hash changes don't reload the page. FL0W, L0GS, 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.
@ -21,7 +21,7 @@ from the dashboard tab strip.
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 ◆`, `◆ Y3R C4LL ◆`, `◆ P3RM1SS10NS ◆`, - **Tab strip**: `◆ SW4RM ◆`, `◆ Y3R C4LL ◆`, `◆ P3RM1SS10NS ◆`,
`◆ SCH3DUL3S ◆`. In-page tabs only — the SYST3M panels live on the `◆ SCH3DUL3S ◆`. In-page tabs only — the SYST3M panels live on the
standalone **C0R3** page (`/core.html`), and FL0W / L0GS / 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 pills on SW4RM H0M3 hub (not the tab strip). Count pills on SW4RM
(container count), Y3R C4LL (unread (container count), Y3R C4LL (unread
@ -29,7 +29,7 @@ from the dashboard tab strip.
- **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, L0GS, on **every** page (dashboard + every stand-alone page — FL0W,
H0M3, C0R3, BU1LDS, ST4TS), injected at the top H0M3, C0R3, BU1LDS, 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
@ -53,9 +53,9 @@ from the dashboard tab strip.
cold-load and updates it live as broker `sent` events arrive; it's cold-load and updates it live as broker `sent` events arrive; it's
preserved when `hive_name` / `swarm_name` later replace the raw title. preserved when `hive_name` / `swarm_name` later replace the raw title.
The FL0W and L0GS pages use a slim header (a `← home` back-link + the The FL0W page uses a slim header (a `← home` back-link + the page
page title) rather than the dashboard tab strip — they're standalone title) rather than the dashboard tab strip — it's a standalone surface,
surfaces, not tab panes. not a tab pane.
## SW4RM tab ## SW4RM tab
@ -110,7 +110,7 @@ instead — it's agent-terminal-only, unrelated to this section.
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 `/logs.html`: a `← home` `/core.html`), with the same minimal chrome as `/builds.html`: a `← home`
back-link + a `createTabStrip` sub-tab nav (**K3PT ST4T3** default, back-link + a `createTabStrip` sub-tab nav (**K3PT ST4T3** default,
then **C0NT41N3R L04D**). The page is its own esbuild bundle (`core.js`) then **C0NT41N3R L04D**). The page is its own esbuild bundle (`core.js`)
that cold-loads `/api/state` and subscribes to `/api/dashboard/stream` for that cold-loads `/api/state` and subscribes to `/api/dashboard/stream` for
@ -400,7 +400,7 @@ tab activation and on each dashboard state load.
Hive-wide turn statistics, aggregated across every agent's Hive-wide turn statistics, aggregated across every agent's
`hyperhive-turn-stats.sqlite` for the selected window. Not a dashboard `hyperhive-turn-stats.sqlite` for the selected window. Not a dashboard
tab — a standalone page reached from the **Stats** tile on the H0M3 tab — a standalone page reached from the **Stats** tile on the H0M3
hub, same minimal chrome as `/flow.html` / `/logs.html`. Distinct from hub, same minimal chrome as `/flow.html`. Distinct from
each agent's own `/stats` page (which carries the per-agent trend each agent's own `/stats` page (which carries the per-agent trend
charts): ST4TS is the swarm-level rollup. charts): ST4TS is the swarm-level rollup.
@ -488,7 +488,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, Logs, Builds, Stats, responsive grid of link tiles — Dashboard, Flow, Builds, 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
@ -496,45 +496,6 @@ from the shared theme (Catppuccin Mocha via `common.css` + `theme.css`).
`home.js` fills the swarm/hive identity line at the top. All dashboard `home.js` fills the swarm/hive identity line at the top. All dashboard
sub-pages include a `← Home` back-link for navigation. sub-pages include a `← Home` back-link for navigation.
## L0GS page (`/logs.html`)
A dedicated log-viewer page (not a tab pane — a separate HTML page),
reachable from the Logs tile on the H0M3 hub. Minimal chrome:
a `← home` back link and a three-item sub-tab strip. Tab
routing is hash-based (`#agent`, `#infra`, `#system`); default is
`#agent`. (Build log history lives on the BU1LDS page — see above.)
**AGENT sub-tab** — per-container journald viewer. Two selects: agent
name (populated from `GET /api/state`) and unit filter
(`hive-agent.service` / `hive-mcp-http.service` /
`hive-bash-daemon.service` / `hive-matrix-daemon.service` /
`(full machine journal)`). Fetches
`GET /api/journal/{name}?unit=<unit>&lines=500` on selection change
or `↻ refresh`. Output rendered as a `<pre>` block. A `?agent=<name>`
and/or `?unit=<svc>` URL param pre-selects the agent + unit on page
load — the per-agent `⋮` menu's **journal logs →** entry uses this to
deep-link directly to a specific agent's journal. A "fetched N ago"
chip appears after the `↻ refresh` button following each successful
fetch and ticks every 30 s.
**INFRA sub-tab** — journald viewer for the three hive infrastructure
containers (`hive-forge`, `hive-gateway`, `hive-matrix`), a
fixed client-side list (`INFRA_NAMES` in `logs.js` — no dashboard API
exposes just the name list). No unit filter (infra containers don't run
the per-agent hive daemons) — always the full machine journal (or, for
the gateway, the host journal filtered to its own unit). Fetches
`GET /api/journal/{name}?lines=500`, same "fetched N ago" ticker as
AGENT. A `?agent=<name>` deep-link routes here instead of AGENT when
the name is one of the three infra containers.
**SYSTEM sub-tab** — host-side service logs. Unit selector
(`hive-c0re.service` / `hive-priv.service`). Fetches
`GET /api/journal-host?unit=<unit>&lines=500` on activation
and on `↻ refresh`. Rendered as a `<pre>` block. A "fetched N ago"
chip ticks every 30 s. Available to the operator unconditionally (not
capability-gated — the endpoint lives on the hive-c0re dashboard,
behind the gateway).
## Container row ## Container row
A full-height **square agent icon** (5em, capped) on the left. The A full-height **square agent icon** (5em, capped) on the left. The
@ -700,9 +661,6 @@ per-agent actions and navigation links. Contents:
Identical to the bulk actions on the selection bar but operate Identical to the bulk actions on the selection bar but operate
on one agent without requiring a selection. on one agent without requiring a selection.
- `↻ R3BU1LD` — always available; queues a rebuild for this agent. - `↻ R3BU1LD` — always available; queues a rebuild for this agent.
- `journal logs →` — opens `/logs.html#agent?agent=<name>` so the
operator lands directly in the AGENT log tab pre-filtered to this
container, without having to pick an agent from the dropdown.
- `DESTR0Y` / `PURG3` — destructive, each prompts for confirmation - `DESTR0Y` / `PURG3` — destructive, each prompts for confirmation
via the themed dialog (see **Themed dialogs** below). via the themed dialog (see **Themed dialogs** below).
- `deployed:<sha> ↗` — present when the agent has a `deployed_sha` - `deployed:<sha> ↗` — present when the agent has a `deployed_sha`
@ -884,8 +842,6 @@ below — some endpoints aren't in it yet.
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 BU1LDS. Works on both finished and in-progress
build logs. build logs.
- `GET /api/journal/{name}?unit=&lines=` — journalctl viewer for
a managed container; rendered in the side panel.
- `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.
- `GET /api/state-file?path=<host-or-container-path>` — bounded - `GET /api/state-file?path=<host-or-container-path>` — bounded

View file

@ -19,7 +19,7 @@
from `@hive/shared` (was `hive-fr0nt`); the dashboard ships one from `@hive/shared` (was `hive-fr0nt`); the dashboard ships one
CSS bundle per page (`colors.css` + `theme.css` + `common.css`, CSS bundle per page (`colors.css` + `theme.css` + `common.css`,
loaded by every page, plus a page-specific bundle — loaded by every page, plus a page-specific bundle —
`dashboard.css` / `flow.css` / `logs.css` / `home.css` / `dashboard.css` / `flow.css` / `home.css` /
`stats.css` / `core.css` / `builds.css`); `stats.css` / `core.css` / `builds.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` +
@ -247,7 +247,7 @@ values rather than per-row caches.
tagged with `data-restore-key="<stable-key>"` survives the tagged with `data-restore-key="<stable-key>"` survives the
refresh. `snapshotOpenDetails()` walks managed sections before refresh. `snapshotOpenDetails()` walks managed sections before
render, `restoreOpenDetails()` re-applies after. Long-content render, `restoreOpenDetails()` re-applies after. Long-content
drill-ins (file previews, diffs, journald logs) now open in the drill-ins (file previews, diffs) now open in the
**side panel** (see below) rather than expanding inline. The **side panel** (see below) rather than expanding inline. The
dashboard's own answered-questions history list was the only dashboard's own answered-questions history list was the only
`<details data-restore-key>` consumer and is gone `<details data-restore-key>` consumer and is gone
@ -261,8 +261,8 @@ that needs it rather than ripped out along with their one caller.
swipes in from the right — a singleton `#side-panel` with a swipes in from the right — a singleton `#side-panel` with a
titled header, a close button, and a scrollable body. Closes on titled header, a close button, and a scrollable body. Closes on
the button, selecting the backdrop, or `Escape`. `Panel.open(title, the button, selecting the backdrop, or `Escape`. `Panel.open(title,
node)` swaps the body; the JS builders for file previews and node)` swaps the body; the JS builder for file previews
journald logs all render into it. **The renders into it. **The
drawer width is drag-to-resize**: a thin 6px hit-strip on drawer width is drag-to-resize**: a thin 6px hit-strip on
the left edge captures pointer events, resizes the drawer in the left edge captures pointer events, resizes the drawer in
real-time (pointer capture keeps dragging even if the cursor real-time (pointer capture keeps dragging even if the cursor

View file

@ -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/logs/core/builds at their own // `/`, dashboard/flow/core/builds 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.
@ -35,7 +35,6 @@ await build({
entryPoints: [ entryPoints: [
src("tabs.js"), src("tabs.js"),
src("flow.js"), src("flow.js"),
src("logs.js"),
src("home.js"), src("home.js"),
src("stats.js"), src("stats.js"),
src("core.js"), src("core.js"),
@ -101,7 +100,6 @@ for (const entry of [
"common.css", "common.css",
"dashboard.css", "dashboard.css",
"flow.css", "flow.css",
"logs.css",
"home.css", "home.css",
"stats.css", "stats.css",
"core.css", "core.css",
@ -120,7 +118,6 @@ for (const html of [
"index.html", "index.html",
"dashboard.html", "dashboard.html",
"flow.html", "flow.html",
"logs.html",
"stats.html", "stats.html",
"core.html", "core.html",
"builds.html", "builds.html",

View file

@ -140,24 +140,6 @@ class HiveAgentMenu extends HTMLElement {
const menuSep = () => const menuSep = () =>
el("li", { class: "agent-menu-sep", role: "separator" }); el("li", { class: "agent-menu-sep", role: "separator" });
// Navigation link item (opens in same tab by default).
const menuLink = (label, href, title) => {
const li = el("li", { role: "presentation" });
const a = el(
"a",
{
class: "agent-menu-item",
href,
role: "menuitem",
title: title || "",
},
label,
);
a.addEventListener("click", close);
li.append(a);
return li;
};
// Show only actions that are applicable in the current state. // Show only actions that are applicable in the current state.
if (c.running) { if (c.running) {
dropdown.append( dropdown.append(
@ -209,15 +191,6 @@ class HiveAgentMenu extends HTMLElement {
action: "/api/rebuild/", action: "/api/rebuild/",
confirm: `rebuild ${c.name}? hot-reloads the container.`, confirm: `rebuild ${c.name}? hot-reloads the container.`,
}), }),
menuSep(),
// Deep-link to the AGENT log tab pre-filtered to this container.
// The ?agent= param is read by logs.js on load and pre-selects this
// agent's journal without extra clicks.
menuLink(
"journal logs →",
`/logs.html?agent=${encodeURIComponent(c.name)}#agent`,
`view ${c.name} journal logs`,
),
); );
dropdown.append( dropdown.append(

View file

@ -1,14 +1,11 @@
/* ─── /builds.html — BU1LDS page ──────────────────────────────────── /* ─── /builds.html — BU1LDS page ────────────────────────────────────
The build lifecycle hub: rebuild queue + live log, meta inputs, and The build lifecycle hub: rebuild queue + live log, meta inputs, and
build log history on one page. Carved out of /core.html (rebuild build log history on one page.
queue + meta inputs) and /logs.html (BUILD tab).
Imports system-sections.css for rebuild queue + meta inputs + live-log Imports system-sections.css for rebuild queue + meta inputs + live-log
component styles, and logs.css for the build-logs-* component styles. component styles; the build-logs-* rules for the BUILD L0GS list are
Body-scoped `.logs-shell` rules in logs.css don't apply here below. */
(`.builds-shell` body); only the build-logs-* component rules are used. */
@import "./system-sections.css"; @import "./system-sections.css";
@import "./logs.css";
@import "@hive/shared/jobq-graph.css"; @import "@hive/shared/jobq-graph.css";
body.builds-shell { body.builds-shell {
@ -27,3 +24,116 @@ body.builds-shell {
.builds-pane[hidden] { .builds-pane[hidden] {
display: none; 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);
}

View file

@ -12,9 +12,7 @@
</head> </head>
<body class="builds-shell"> <body class="builds-shell">
<!-- BU1LDS: the build lifecycle hub — rebuild queue, live build log, <!-- BU1LDS: the build lifecycle hub — rebuild queue, live build log,
meta inputs, and build log history on one page. Carved out of the meta inputs, and build log history on one page. Same minimal
old /core.html (rebuild queue + meta inputs) and /logs.html (BUILD
tab) so the full build lifecycle is in one place. Same minimal
chrome as /core.html — a `← home` back-link + a <hive-tab-strip> chrome as /core.html — a `← home` back-link + a <hive-tab-strip>
sub-tab nav. Default sub-tab is the rebuild queue. --> sub-tab nav. Default sub-tab is the rebuild queue. -->
<header class="page-header"> <header class="page-header">
@ -74,7 +72,7 @@
</div> </div>
</section> </section>
<!-- BUILD L0GS: all-agents build log history (moved from /logs.html). <!-- BUILD L0GS: all-agents build log history.
Click a row to expand stdout + stderr. Live builds stream in real Click a row to expand stdout + stderr. Live builds stream in real
time. Lazy-loaded on first tab activation; auto-refreshes when time. Lazy-loaded on first tab activation; auto-refreshes when
rebuild_queue_changed fires. Deep-link: ?id=N#buildlogs. --> rebuild_queue_changed fires. Deep-link: ?id=N#buildlogs. -->

View file

@ -4,11 +4,6 @@
// - R3BU1LD QU3U3: pending + running rebuilds + live build log inline // - R3BU1LD QU3U3: pending + running rebuilds + live build log inline
// - M3T4 1NPUTS: nix flake input selector + meta-update trigger // - M3T4 1NPUTS: nix flake input selector + meta-update trigger
// - BUILD L0GS: all-agents build log history (lazy-loaded on tab show) // - BUILD L0GS: all-agents build log history (lazy-loaded on tab show)
//
// Carved out of /core.html (rebuild queue + meta inputs) and /logs.html
// (BUILD tab) so the full build lifecycle is in one place. The section
// renderers here are direct copies from core.js / logs.js with only the
// deep-link URL and count-pill id adjusted.
import { import {
$, $,

View file

@ -10,8 +10,8 @@
@import "@hive/shared/pill.css"; @import "@hive/shared/pill.css";
/* ─── global typography ───────────────────────────────────────────── /* ─── global typography ─────────────────────────────────────────────
Element-level rules shared across all three pages (index, flow, Element-level rules shared across every page. These must not
logs). These must not reference page-specific chrome classes. */ reference page-specific chrome classes. */
h1, h1,
h2 { h2 {
@ -152,8 +152,7 @@ code {
} }
/* ─── buttons ─────────────────────────────────────────────────────── /* ─── buttons ───────────────────────────────────────────────────────
.btn base + semantic colour/size modifiers. Logs page uses .btn base + semantic colour/size modifiers. */
.btn-restart for the ↻ refresh buttons; dashboard uses all variants. */
.btn { .btn {
font-family: inherit; font-family: inherit;
font-weight: bold; font-weight: bold;
@ -516,7 +515,7 @@ code {
} }
/* ─── side panel ───────────────────────────────────────────────── /* ─── side panel ─────────────────────────────────────────────────
Long content (file previews, diffs, journald, applied config) opens in Long content (file previews, diffs, applied config) opens in
a drawer that swipes in from the right — used by both /flow.html and a drawer that swipes in from the right — used by both /flow.html and
the dashboard. The drawer chrome itself (backdrop, drawer, resize the dashboard. The drawer chrome itself (backdrop, drawer, resize
handle, header, title, close button) is now the shared handle, header, title, close button) is now the shared
@ -605,8 +604,8 @@ hive-side-panel .md td {
carries the horizontal gutter. Full-width chrome (the tab strip, the carries the horizontal gutter. Full-width chrome (the tab strip, the
server-warnings banner, the footer) then spans edge-to-edge for free, server-warnings banner, the footer) then spans edge-to-edge for free,
with no negative-margin breakout hacks. Only matches on pages that opt with no negative-margin breakout hacks. Only matches on pages that opt
in by adding the wrapper (dashboard.html, index.html); FL0W / L0GS / in by adding the wrapper (dashboard.html, index.html); FL0W / ST4TS
ST4TS are full-bleed with their own `.<page>-main` padding. */ are full-bleed with their own `.<page>-main` padding. */
.page-content { .page-content {
padding: 0 1.5em; padding: 0 1.5em;
} }

View file

@ -292,12 +292,10 @@ export function openStream(url) {
return target; return target;
} }
// Stream a build log into a <pre>, returning the EventSource. Shared by the // Stream a build log into a <pre>, returning the EventSource. Used by the
// L0GS page BUILD tab and the C0R3 rebuild-queue live-log panel so the // BU1LDS page's BUILD L0GS tab. Appends `stdout_append` then
// append / sticky-scroll / stderr-separator / reconnect-replay logic lives in // `stderr_append` (one `--- stderr ---` separator) frames from
// one place. Appends `stdout_append` then `stderr_append` (one `--- stderr ---` // `GET /api/build-logs/id/{id}/stream`; auto-scrolls to the bottom unless the operator scrolled up; on the terminal `done` frame
// 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
// closes the stream and calls `onDone(status)`; on a non-transient error // closes the stream and calls `onDone(status)`; on a non-transient error
// closes and calls `onError()`. The backend replays accumulated output on each // closes and calls `onError()`. The backend replays accumulated output on each
// (re)connect, so a CONNECTING reconnect resets the <pre> to avoid doubling. // (re)connect, so a CONNECTING reconnect resets the <pre> to avoid doubling.
@ -348,7 +346,7 @@ export function openBuildLogStream(id, pre, { onDone, onError } = {}) {
// ─── side panel ───────────────────────────────────────────────────────── // ─── side panel ─────────────────────────────────────────────────────────
// Singleton drawer that swipes in from the right. Long content // Singleton drawer that swipes in from the right. Long content
// (file previews, journald logs, applied config) // (file previews, applied config)
// opens here via `sidePanel.open(title, node)` instead of expanding // opens here via `sidePanel.open(title, node)` instead of expanding
// inline. Body is swapped on each open; closing just slides out so // inline. Body is swapped on each open; closing just slides out so
// the content stays visible through the transition. // the content stays visible through the transition.
@ -654,7 +652,7 @@ export const NOTIF = (() => {
// ─── server warnings banner ────────────────────────────────────────── // ─── server warnings banner ──────────────────────────────────────────
// A generic top-of-page banner shown on every page (dashboard + the // A generic top-of-page banner shown on every page (dashboard + the
// stand-alone FL0W / L0GS / H0M3 pages). The backend decides what to // stand-alone FL0W / H0M3 pages). The backend decides what to
// warn about — `/api/state.server_warnings` is a list of // warn about — `/api/state.server_warnings` is a list of
// `{ kind, level, message }` — and this just renders it, coloured by // `{ kind, level, message }` — and this just renders it, coloured by
// `level` (`warn` amber / `crit` red). Adding a new system warning is a // `level` (`warn` amber / `crit` red). Adding a new system warning is a

View file

@ -8,8 +8,8 @@ body.core-shell {
padding: 0; padding: 0;
} }
/* Fill the header row next to the ← home back-link, same as the logs /* Fill the header row next to the ← home back-link, same as the
sub-tab strip. */ builds sub-tab strip. */
.core-tabbar { .core-tabbar {
flex: 1; flex: 1;
} }

View file

@ -14,7 +14,7 @@
<!-- 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. Same minimal chrome as dashboard tab strip stays lean. Same minimal chrome as
/logs.html — a `← home` back-link to the H0M3 hub + a /builds.html — a `← home` back-link to the H0M3 hub + a
<hive-tab-strip> sub-tab nav. Two sub-tabs: kept state (tombstones) <hive-tab-strip> sub-tab nav. Two sub-tabs: kept state (tombstones)
and container load. Rebuild queue + meta inputs have moved to and container load. Rebuild queue + meta inputs have moved to
/builds.html (the build lifecycle hub). Default tab: K3PT ST4T3. /builds.html (the build lifecycle hub). Default tab: K3PT ST4T3.

View file

@ -90,11 +90,9 @@
<!-- (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 SW4RM. See tab-pane-swarm.) -->
<!-- FL0W, L0GS, and the other standalone pages are reachable from <!-- FL0W and the other standalone pages are reachable from the
the H0M3 hub (served at /), not from the dashboard tab strip — H0M3 hub (served at /), not from the dashboard tab strip —
the strip holds only in-page tabs. Contextual deep-links into the strip holds only in-page tabs. -->
the log viewer (an agent's logs, a build entry's log) live
inside the relevant dashboard content. -->
</nav> </nav>
</header> </header>
@ -335,8 +333,8 @@
</p> </p>
</footer> </footer>
<!-- Slide-in detail panel (clicked file previews, journald logs, <!-- Slide-in detail panel (clicked file previews, applied config)
applied config) is a <hive-side-panel> element is a <hive-side-panel> element
(@hive/shared/side-panel.js) — the Panel singleton in common.js (@hive/shared/side-panel.js) — the Panel singleton in common.js
creates + appends it to <body> lazily on first use, so nothing creates + appends it to <body> lazily on first use, so nothing
needs to be pre-declared here. --> needs to be pre-declared here. -->

View file

@ -92,7 +92,7 @@ body.flow-shell .flow-main-slim {
/* Agent filter: a select in the FL0W header narrows the /* Agent filter: a select in the FL0W header narrows the
timeline to messages involving one agent. Non-matching rows get timeline to messages involving one agent. Non-matching rows get
`.flow-hidden`; the select reuses the journal-unit control look. */ `.flow-hidden`. */
.flow-filter { .flow-filter {
font-family: inherit; font-family: inherit;
font-size: 0.85em; font-size: 0.85em;

View file

@ -64,14 +64,6 @@
> >
</a> </a>
<a class="home-tile" href="/logs.html">
<span class="home-tile-head">
<span class="home-tile-icon" aria-hidden="true">📜</span>
<span class="home-tile-label">Logs</span>
</span>
<span class="home-tile-desc">agent · infra · system 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>

View file

@ -1,233 +0,0 @@
/* ─── /logs.html — log viewer ──────────────────────────────────────
Same minimal-chrome pattern as /flow.html but with a sub-tab strip
instead of a full dashboard tabbar. */
body.logs-shell {
margin: 0;
padding: 0;
/* Full-viewport flex column so the log pane can scroll internally
instead of the whole page scrolling. The sticky-top wrapper and
page-header shrink to their natural height; .logs-main takes the
rest and each visible .logs-pane fills it. */
height: 100dvh;
display: flex;
flex-direction: column;
overflow: hidden;
}
/* Base tab styling now lives in @hive/shared/tabs.css (.hive-tab*). The
logs strip only adds a layout delta: fill the header row next to the
← home back-link. `min-width: 0` lets this flex:1 nav shrink below its
intrinsic content width instead of being bumped onto its own row under
the back-link (the `.page-header` is explicitly nowrap; see chrome.css). */
.logs-tabbar {
flex: 1;
min-width: 0;
}
.logs-main {
padding: 1.2em 1.5em 0;
flex: 1 1 0;
min-height: 0;
display: flex;
flex-direction: column;
}
.logs-pane[hidden] {
display: none;
}
.logs-pane {
flex: 1 1 0;
min-height: 0;
display: flex;
flex-direction: column;
padding-bottom: 1.2em;
}
.logs-toolbar {
display: flex;
gap: 0.6em;
align-items: center;
margin-bottom: 0.9em;
flex-wrap: wrap;
}
/* ─── per-container journald + build-log viewers ──────────────────
Both open inside the logs page: a toolbar above a scrollable
output area. `.journal-*` classes power the agent and system log
tabs; `.build-logs-*` power the build tab. */
.journal-controls {
flex: 0 0 auto;
display: flex;
gap: 0.5em;
align-items: center;
}
.journal-unit {
font-family: inherit;
font-size: 0.9em;
background: var(--bg-elev);
color: var(--fg);
border: 1px solid var(--border);
padding: 0.2em 0.4em;
}
.logs-toolbar .journal-unit {
background: var(--border);
border-color: var(--purple-dim);
border-radius: 4px;
padding: 0.25em 0.5em;
font-size: 0.85em;
}
.journal-refresh {
font-size: 0.75em;
padding: 0.15em 0.5em;
}
/* "fetched N ago" chip next to the refresh button on AGENT + SYSTEM tabs. */
.logs-fetch-ts {
font-size: 0.8em;
color: var(--muted);
}
.journal-output {
margin: 0;
background: var(--crust);
color: var(--fg);
border: 1px solid var(--purple-dim);
padding: 0.5em 0.7em;
flex: 1 1 0;
min-height: 0;
overflow: auto;
font-size: 0.85em;
line-height: 1.4;
white-space: pre;
word-break: normal;
}
/* ─── build-log list ──────────────────────────────────────────────
Build tab: toolbar above a scrollable list of log header rows;
clicking a row expands full stdout+stderr inline. */
.build-logs-panel {
display: flex;
flex-direction: column;
height: 100%;
gap: 0.4em;
}
.build-logs-toolbar {
flex: 0 0 auto;
display: flex;
gap: 0.5em;
align-items: center;
}
.build-logs-refresh {
font-size: 0.75em;
padding: 0.15em 0.5em;
}
.build-logs-list {
list-style: none;
padding: 0;
margin: 0;
overflow-y: auto;
flex: 1 1 0;
min-height: 0;
}
.build-logs-loading,
.build-logs-empty,
.build-logs-error {
color: var(--muted);
font-size: 0.85em;
padding: 0.3em 0;
}
.build-logs-error {
color: var(--red);
}
.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);
}

View file

@ -1,112 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>hyperhive // LOGS</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/logs.css" />
</head>
<body class="logs-shell">
<!-- Minimal chrome: back link + sub-tab strip.
Same pattern as flow.html — no full dashboard tabbar. Back link
points to the H0M3 hub (served at /), not the dashboard.
Three sub-tabs: AGENT, INFRA, SYSTEM. Build log history has
moved to /builds.html (the build lifecycle hub). -->
<header class="page-header">
<a class="page-back" href="/">← home</a>
<hive-tab-strip
class="hive-tabbar logs-tabbar"
id="logs-tabbar"
prefix="logs"
role="tablist"
></hive-tab-strip>
</header>
<main class="logs-main">
<!-- AGENT: journald viewer for a specific agent container.
Agent selector + unit filter + line count. Backed by
GET /api/journal/{agent}?unit=<unit>&lines=N. -->
<section
class="logs-pane"
id="logs-pane-agent"
data-tab-pane="agent"
role="tabpanel"
aria-labelledby="logs-tab-agent"
>
<div class="logs-toolbar">
<select id="agent-select" class="journal-unit"></select>
<select id="agent-unit-select" class="journal-unit">
<option value="hive-agent.service">hive-agent.service</option>
<option value="hive-mcp-http.service">hive-mcp-http.service</option>
<option value="hive-bash-daemon.service">
hive-bash-daemon.service
</option>
<option value="hive-matrix-daemon.service">
hive-matrix-daemon.service
</option>
<option value="">(full machine journal)</option>
</select>
<button type="button" class="btn btn-restart" id="agent-refresh">
↻ refresh
</button>
<span id="agent-fetch-ts" class="meta logs-fetch-ts" hidden></span>
</div>
<pre id="agent-output" class="journal-output">
select an agent above</pre
>
</section>
<!-- INFRA: journald viewer for infrastructure containers (hive-forge,
hive-gateway, hive-matrix). Always fetches the full
machine journal — no unit filter (infra containers don't run the
per-agent hive daemons). Backed by GET /api/journal/{name}?lines=N.
Deep-link: ?agent=hive-forge routes here instead of the AGENT tab. -->
<section
class="logs-pane"
id="logs-pane-infra"
data-tab-pane="infra"
role="tabpanel"
aria-labelledby="logs-tab-infra"
>
<div class="logs-toolbar">
<select id="infra-select" class="journal-unit"></select>
<button type="button" class="btn btn-restart" id="infra-refresh">
↻ refresh
</button>
<span id="infra-fetch-ts" class="meta logs-fetch-ts" hidden></span>
</div>
<pre id="infra-output" class="journal-output">
select a container above</pre
>
</section>
<!-- SYSTEM: host-side service logs. Shows the hive-c0re daemon
journal via GET /api/journal-host?unit=hive-c0re.service. -->
<section
class="logs-pane"
id="logs-pane-system"
data-tab-pane="system"
role="tabpanel"
aria-labelledby="logs-tab-system"
>
<div class="logs-toolbar">
<select id="system-unit-select" class="journal-unit">
<option value="hive-c0re.service">hive-c0re.service</option>
<option value="hive-priv.service">hive-priv.service</option>
</select>
<button type="button" class="btn btn-restart" id="system-refresh">
↻ refresh
</button>
<span id="system-fetch-ts" class="meta logs-fetch-ts" hidden></span>
</div>
<pre id="system-output" class="journal-output">loading…</pre>
</section>
</main>
<script type="module" src="/static/logs.js" defer></script>
</body>
</html>

View file

@ -1,284 +0,0 @@
// /logs.html entry point: log viewer with three sub-tabs (AGENT, INFRA,
// SYSTEM). Build log history lives at /builds.html, not here.
// AGENT — per-container journald viewer, backed by GET /api/journal/{name}
// INFRA — infra-container journald viewer (hive-forge, hive-gateway, …),
// same API but full machine journal only (no unit filter)
// SYSTEM — host service logs, backed by GET /api/journal-host
//
// Tab routing via URL hash (#agent, #infra, #system). Default: #agent.
//
// URL params `?agent=name` and `?unit=svc` pre-select the agent + unit.
// When `?agent=` names an infra container the INFRA tab is activated instead.
// Last-fetched timestamp is shown next to the refresh button on AGENT,
// INFRA, and SYSTEM tabs so the operator knows how stale the output is.
import { $, fmtAgeSecs, initServerWarnings } from "./common.js";
import { el } from "@hive/shared/dom.js";
import "@hive/shared/hive-tab-strip.js";
(() => {
initServerWarnings();
// ─── tab routing ──────────────────────────────────────────────────────
// The shared hash-routed tab strip (@hive/shared/tabs.js) handles the
// active toggle + aria-selected + pane visibility + lazy fetches via
// onShow. Constructed in the init block below (after the fetch fns +
// element refs it depends on exist). `logTabs.active()` replaces the
// old `activeTab()`.
let logTabs;
// ─── helpers ──────────────────────────────────────────────────────────
// Format a last-fetched timestamp: "fetched just now" / "fetched 3m ago".
function fmtFetchTs(fetchedAt) {
const ageSecs = Math.floor((Date.now() - fetchedAt) / 1000);
return (
"fetched " + (ageSecs < 5 ? "just now" : fmtAgeSecs(ageSecs) + " ago")
);
}
// ─── AGENT tab ────────────────────────────────────────────────────────
const agentSelect = $("agent-select");
const agentUnitSelect = $("agent-unit-select");
const agentRefresh = $("agent-refresh");
const agentOutput = $("agent-output");
const agentFetchTs = $("agent-fetch-ts");
let agentFetching = false;
let agentLastFetch = 0;
async function fetchAgent() {
if (!agentSelect || !agentOutput) return;
const name = agentSelect.value;
if (!name) {
agentOutput.textContent = "select an agent above";
return;
}
if (agentFetching) return;
agentFetching = true;
agentOutput.textContent = "fetching…";
if (agentFetchTs) agentFetchTs.hidden = true;
const unit = agentUnitSelect ? agentUnitSelect.value : "";
const params = new URLSearchParams({ lines: "500" });
if (unit) params.set("unit", unit);
try {
const resp = await fetch(
"/api/journal/" + encodeURIComponent(name) + "?" + params,
);
const text = await resp.text();
agentOutput.textContent = resp.ok
? text || "(empty)"
: "error " + resp.status + "\n" + text;
agentOutput.scrollTop = agentOutput.scrollHeight;
if (resp.ok) {
agentLastFetch = Date.now();
if (agentFetchTs) {
agentFetchTs.textContent = fmtFetchTs(agentLastFetch);
agentFetchTs.hidden = false;
}
}
} catch (err) {
agentOutput.textContent = "fetch failed: " + err;
} finally {
agentFetching = false;
}
}
if (agentSelect) agentSelect.addEventListener("change", fetchAgent);
if (agentUnitSelect) agentUnitSelect.addEventListener("change", fetchAgent);
if (agentRefresh) agentRefresh.addEventListener("click", fetchAgent);
// ─── INFRA tab ────────────────────────────────────────────────────────
// Infra containers (hive-forge, hive-gateway, hive-matrix) don't
// run the per-agent hive daemons, so the unit filter is inapplicable. We
// always fetch the full machine journal for them.
const infraSelect = $("infra-select");
const infraRefresh = $("infra-refresh");
const infraOutput = $("infra-output");
const infraFetchTs = $("infra-fetch-ts");
let infraFetching = false;
let infraLastFetch = 0;
async function fetchInfra() {
if (!infraSelect || !infraOutput) return;
const name = infraSelect.value;
if (!name) {
infraOutput.textContent = "select a container above";
return;
}
if (infraFetching) return;
infraFetching = true;
infraOutput.textContent = "fetching…";
if (infraFetchTs) infraFetchTs.hidden = true;
const params = new URLSearchParams({ lines: "500" });
try {
const resp = await fetch(
"/api/journal/" + encodeURIComponent(name) + "?" + params,
);
const text = await resp.text();
infraOutput.textContent = resp.ok
? text || "(empty)"
: "error " + resp.status + "\n" + text;
infraOutput.scrollTop = infraOutput.scrollHeight;
if (resp.ok) {
infraLastFetch = Date.now();
if (infraFetchTs) {
infraFetchTs.textContent = fmtFetchTs(infraLastFetch);
infraFetchTs.hidden = false;
}
}
} catch (err) {
infraOutput.textContent = "fetch failed: " + err;
} finally {
infraFetching = false;
}
}
if (infraSelect) infraSelect.addEventListener("change", fetchInfra);
if (infraRefresh) infraRefresh.addEventListener("click", fetchInfra);
// Fixed allowlist — the three hive infra services never change at
// runtime, and there's no dashboard API exposing just the name list
// (the one that used to, `/api/state`'s `infra_containers` field, was
// start/stop-panel-only and is gone). Mirrors `hive_priv_sock::InfraContainer::ALL`.
const INFRA_NAMES = ["hive-forge", "hive-gateway", "hive-matrix"];
// ─── container list init ──────────────────────────────────────────────
// Fetch /api/state once to populate the AGENT selector (agents only);
// INFRA is a fixed list (see INFRA_NAMES). Also handles the ?agent= /
// ?unit= deep-link, routing to the INFRA tab when the named container
// is an infra container.
async function loadContainerLists() {
if (infraSelect) {
infraSelect.replaceChildren();
infraSelect.append(el("option", { value: "" }, "— select container —"));
for (const name of INFRA_NAMES) {
infraSelect.append(el("option", { value: name }, name));
}
}
try {
const resp = await fetch("/api/state");
if (!resp.ok) return;
const state = await resp.json();
// Populate AGENT selector (agents only — no infra optgroup).
if (agentSelect) {
agentSelect.replaceChildren();
agentSelect.append(el("option", { value: "" }, "— select agent —"));
for (const c of state.containers || []) {
agentSelect.append(el("option", { value: c.name }, c.name));
}
}
// Deep-link: honour ?agent= and ?unit= URL params.
const urlAgent = new URLSearchParams(location.search).get("agent");
const urlUnit = new URLSearchParams(location.search).get("unit");
if (urlAgent) {
if (INFRA_NAMES.includes(urlAgent)) {
// Route to INFRA tab.
logTabs.show("infra");
if (infraSelect) {
infraSelect.value = urlAgent;
fetchInfra();
}
} else if (agentSelect) {
// Route to AGENT tab.
const found = Array.from(agentSelect.options).some(
(o) => o.value === urlAgent,
);
if (found) {
agentSelect.value = urlAgent;
if (urlUnit && agentUnitSelect) {
const unitFound = Array.from(agentUnitSelect.options).some(
(o) => o.value === urlUnit,
);
if (unitFound) agentUnitSelect.value = urlUnit;
}
fetchAgent();
}
}
}
} catch {
/**/
}
}
// ─── SYSTEM tab ───────────────────────────────────────────────────────
const systemUnitSelect = $("system-unit-select");
const systemRefresh = $("system-refresh");
const systemOutput = $("system-output");
const systemFetchTs = $("system-fetch-ts");
let systemFetching = false;
let systemLastFetch = 0;
async function fetchSystem() {
if (!systemOutput) return;
if (systemFetching) return;
systemFetching = true;
systemOutput.textContent = "fetching…";
if (systemFetchTs) systemFetchTs.hidden = true;
const unit = systemUnitSelect
? systemUnitSelect.value
: "hive-c0re.service";
const params = new URLSearchParams({ lines: "500" });
if (unit) params.set("unit", unit);
try {
const resp = await fetch("/api/journal-host?" + params);
const text = await resp.text();
systemOutput.textContent = resp.ok
? text || "(empty)"
: "error " + resp.status + "\n" + text;
systemOutput.scrollTop = systemOutput.scrollHeight;
if (resp.ok) {
systemLastFetch = Date.now();
if (systemFetchTs) {
systemFetchTs.textContent = fmtFetchTs(systemLastFetch);
systemFetchTs.hidden = false;
}
}
} catch (err) {
systemOutput.textContent = "fetch failed: " + err;
} finally {
systemFetching = false;
}
}
if (systemUnitSelect)
systemUnitSelect.addEventListener("change", fetchSystem);
if (systemRefresh) systemRefresh.addEventListener("click", fetchSystem);
// ─── init ─────────────────────────────────────────────────────────────
// Wire the shared tab strip now that fetchSystem + the element refs it
// needs are defined. Its initial show() paints the active pane and, if
// the deep-linked tab is SYSTEM, kicks off the lazy fetch via onShow.
// Default: AGENT.
logTabs = document.getElementById("logs-tabbar").configure({
tabs: [
{ id: "agent", label: "AGENT" },
{ id: "infra", label: "INFRA" },
{ id: "system", label: "SYSTEM" },
],
defaultId: "agent",
onShow: (id) => {
if (id === "system") fetchSystem();
},
});
loadContainerLists();
// Tick the last-fetched timestamps every 30s so "fetched 1m ago" stays
// accurate without a manual refresh.
setInterval(() => {
if (agentLastFetch && agentFetchTs && !agentFetchTs.hidden) {
agentFetchTs.textContent = fmtFetchTs(agentLastFetch);
}
if (infraLastFetch && infraFetchTs && !infraFetchTs.hidden) {
infraFetchTs.textContent = fmtFetchTs(infraLastFetch);
}
if (systemLastFetch && systemFetchTs && !systemFetchTs.hidden) {
systemFetchTs.textContent = fmtFetchTs(systemLastFetch);
}
}, 30_000);
})();

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