dashboard: the hive UI no longer shows container load
The cpu, memory and disk figures are exported as OTEL metrics and graphed in Grafana, and the caps are set in the swarm UI, so the C0NT41N3R L04D tab on the C0R3 page only duplicated both. C0R3 is now the kept-state page alone, without a tab strip.
This commit is contained in:
parent
4285faf07c
commit
a2540be98b
11 changed files with 52 additions and 515 deletions
|
|
@ -74,13 +74,17 @@ 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 the same minimal chrome as `/builds.html`: a `← home`
|
||||||
back-link + a `createTabStrip` sub-tab nav (**K3PT ST4T3** default,
|
back-link and a single pane, **K3PT ST4T3**. The page is its own esbuild
|
||||||
then **C0NT41N3R L04D**). The page is its own esbuild bundle (`core.js`)
|
bundle (`core.js`) that cold-loads `/api/state` and subscribes to
|
||||||
that 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 live on the BU1LDS
|
live on the BU1LDS page — see below.)
|
||||||
page — see below.)
|
|
||||||
|
Per-container CPU, memory and disk load is not shown in the hive UI. The
|
||||||
|
same figures are exported as OTEL metrics and graphed in Grafana (see
|
||||||
|
[observability](../scheduler/observability.md)). An agent's CPU and memory
|
||||||
|
caps are set per agent in the swarm UI.
|
||||||
|
|
||||||
**K3PT ST4T3** — two sub-sections on one pane:
|
**K3PT ST4T3** — two sub-sections on one pane:
|
||||||
|
|
||||||
|
|
@ -102,32 +106,11 @@ snapshot-storage rework where the problem changes shape.
|
||||||
*Stale permission entries*: agents with explicit capability or
|
*Stale permission entries*: agents with explicit capability or
|
||||||
tool-group JSON entries but no live container — typically renamed or
|
tool-group JSON entries but no live container — typically renamed or
|
||||||
deleted agents whose entries persisted in `capabilities.json` /
|
deleted agents whose entries persisted in `capabilities.json` /
|
||||||
`tool-groups.json`. Lazy-loaded on first K3PT ST4T3 tab activation;
|
`tool-groups.json`. Loaded when the page opens;
|
||||||
autorefreshes on `capabilities_changed` + `tool_groups_changed` SSE
|
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}`).
|
||||||
|
|
||||||
**C0NT41N3R L04D** — live CPU + memory per agent container, read
|
|
||||||
straight from cgroup v2 on the host (`cpu.stat`, `memory.current`,
|
|
||||||
`memory.peak`, `memory.max` under
|
|
||||||
`/sys/fs/cgroup/machine.slice/machine-h\x2d<name>.scope/`). CPU is a
|
|
||||||
host-normalised percentage (0..100 across all cores) sampled over a
|
|
||||||
short (~200 ms) two-read interval; memory shows current + peak with a
|
|
||||||
bar against the `memory.max` quota. Backed by
|
|
||||||
`GET /api/container-resources` (`container_stats.rs`), which reads the
|
|
||||||
files read-only (world-readable; no `hive-priv`) and skips agents whose
|
|
||||||
scope dir is absent (= not running). Pull-only: `core.js` polls every
|
|
||||||
5 s **only while the C0NT41N3R L04D sub-tab is active** (CPU needs a fresh
|
|
||||||
sample each refresh), and stops on sub-tab change. Disk size (`disk_bytes`)
|
|
||||||
rides the same row, but a separate background sampler feeds it every
|
|
||||||
30 min: `hive-priv` runs one bounded `du -x` as root over the agent's
|
|
||||||
`state/` + `work/` + container writable rootfs (shared nix store excluded),
|
|
||||||
so the 5 s poll stays cheap cgroup-only reads. The row carries the last
|
|
||||||
measured value; the column shows "unknown" (`null`) before the first
|
|
||||||
measurement and whenever the last one failed, never 0. Network isn't tracked
|
|
||||||
per container — see `docs/networking/network.md` for the current
|
|
||||||
per-agent netns model.
|
|
||||||
|
|
||||||
## BU1LDS page (`/builds.html`)
|
## BU1LDS page (`/builds.html`)
|
||||||
|
|
||||||
The build lifecycle hub — rebuild queue, live build log, meta inputs,
|
The build lifecycle hub — rebuild queue, live build log, meta inputs,
|
||||||
|
|
@ -765,17 +748,6 @@ below — some endpoints aren't in it yet.
|
||||||
zero-turn dbs); returns swarm totals, a busiest-first per-agent
|
zero-turn dbs); returns swarm totals, a busiest-first per-agent
|
||||||
rollup, swarm model mix, and a labelled `est_cost_usd`. Window
|
rollup, swarm model mix, and a labelled `est_cost_usd`. Window
|
||||||
defaults to `24h`.
|
defaults to `24h`.
|
||||||
- `GET /api/container-resources` — live per-agent-container CPU +
|
|
||||||
memory from cgroup v2 (C0R3 › C0NT41N3R L04D panel). Returns one
|
|
||||||
row per running agent (`name`, `cpu_pct`, `mem_current_bytes`,
|
|
||||||
`mem_peak_bytes`, `mem_max_bytes`, `disk_bytes`); samples CPU over
|
|
||||||
~200 ms so the call briefly awaits. Skips non-running agents (no scope
|
|
||||||
dir). No network field — agents share the host netns. `disk_bytes` is
|
|
||||||
the last value from a **separate slow sampler** (not this hot path):
|
|
||||||
every 30 min `hive-priv` runs a bounded `du -sxb` as root over the agent's
|
|
||||||
`state/` + `work/` + container writable rootfs, `-x` excluding the shared
|
|
||||||
read-only nix store (`PrivRequest::MeasureAgentDisk`). `null` means
|
|
||||||
unknown: not measured yet, or the last measurement failed.
|
|
||||||
- `POST /api/cancel-reminder/{id}` — hard-delete a pending reminder.
|
- `POST /api/cancel-reminder/{id}` — hard-delete a pending reminder.
|
||||||
- `POST /api/retry-reminder/{id}` — re-arm a reminder whose delivery
|
- `POST /api/retry-reminder/{id}` — re-arm a reminder whose delivery
|
||||||
failed (clears the failure state so the scheduler retries).
|
failed (clears the failure state so the scheduler retries).
|
||||||
|
|
|
||||||
|
|
@ -619,8 +619,8 @@ hive-side-panel .md td {
|
||||||
|
|
||||||
/* ─── stats table ─────────────────────────────────────────────────
|
/* ─── stats table ─────────────────────────────────────────────────
|
||||||
Shared right-aligned numeric table. Used by /stats.html (busiest
|
Shared right-aligned numeric table. Used by /stats.html (busiest
|
||||||
agents) and the dashboard SYST3M › C0NT41N3R L04D table — moved here
|
agents) — moved here from dashboard.css when ST4TS became its own
|
||||||
from dashboard.css when ST4TS became its own page. */
|
page. */
|
||||||
.hive-stats-table {
|
.hive-stats-table {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
/* ─── /core.html — C0R3 page ───────────────────────────────────────
|
/* ─── /core.html — C0R3 page ───────────────────────────────────────
|
||||||
Kept state (tombstones) and container load. Section rules imported
|
Kept state (tombstones) and stale permission entries. Section rules
|
||||||
from system-sections.css (shared with dashboard.css + builds.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 {
|
||||||
|
|
@ -8,22 +9,10 @@ body.core-shell {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Fill the header row next to the ← home back-link, same as the
|
|
||||||
builds sub-tab strip. */
|
|
||||||
.core-tabbar {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.core-main {
|
.core-main {
|
||||||
padding: 1.2em 1.5em 2em;
|
padding: 1.2em 1.5em 2em;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* createTabStrip toggles the native `hidden` attribute on inactive
|
|
||||||
panes; ensure it wins over any inherited display. */
|
|
||||||
.core-pane[hidden] {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ─── K3PT ST4T3 container cards ────────────────────────────────────
|
/* ─── K3PT ST4T3 container cards ────────────────────────────────────
|
||||||
core.html doesn't load dashboard.css (that's the operator SPA),
|
core.html doesn't load dashboard.css (that's the operator SPA),
|
||||||
so the .container-row card styles aren't inherited. Redefine them
|
so the .container-row card styles aren't inherited. Redefine them
|
||||||
|
|
|
||||||
|
|
@ -13,70 +13,31 @@
|
||||||
<body class="core-shell">
|
<body class="core-shell">
|
||||||
<!-- 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: kept state (tombstones) and the
|
||||||
/builds.html — a `← home` back-link to the H0M3 hub + a
|
stale permission entries. The section <div> ids (tombstones-section,
|
||||||
<hive-tab-strip> sub-tab nav. Two sub-tabs: kept state (tombstones)
|
tombstones-stale-perms) match what core.js's renderers target. Hive
|
||||||
and container load. Rebuild queue + meta inputs have moved to
|
infra containers (`hive-forge`, `hive-gateway`, `hive-matrix`) have
|
||||||
/builds.html (the build lifecycle hub). Default tab: K3PT ST4T3.
|
no dashboard panel — `hivectl stop`/`start`/`restart` is the only
|
||||||
The section <div> ids (tombstones-section, container-load-section)
|
|
||||||
match what core.js's renderers target. Hive infra containers
|
|
||||||
(`hive-forge`, `hive-gateway`, `hive-matrix`) have no
|
|
||||||
dashboard panel — `hivectl stop`/`start`/`restart` is the only
|
|
||||||
control surface. -->
|
control surface. -->
|
||||||
<header class="page-header">
|
<header class="page-header">
|
||||||
<a class="page-back" href="/">← home</a>
|
<a class="page-back" href="/">← home</a>
|
||||||
<hive-tab-strip
|
<span class="page-title">K3PT ST4T3</span>
|
||||||
class="hive-tabbar core-tabbar"
|
|
||||||
id="core-tabbar"
|
|
||||||
prefix="core"
|
|
||||||
role="tablist"
|
|
||||||
></hive-tab-strip>
|
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main class="core-main">
|
<main class="core-main">
|
||||||
<!-- K3PT ST4T3: tombstoned-agent kept state + stale permission entries.
|
<!-- tombstones-section shows destroyed agents (purge button).
|
||||||
tombstones-section shows destroyed agents (purge button).
|
tombstones-stale-perms shows agents with explicit capability/
|
||||||
tombstones-stale-perms is lazy-loaded on tab activation and shows
|
tool-group entries but no live container (typically renamed/deleted
|
||||||
agents with explicit capability/tool-group entries but no live
|
agents whose JSON entries persisted). Each stale entry gets a
|
||||||
container (typically renamed/deleted agents whose JSON entries
|
"✕ clear perms" button. -->
|
||||||
persisted). Each stale entry gets a "✕ clear perms" button. -->
|
<p class="meta">
|
||||||
<section
|
kept state from previously tombstoned agents — recreating an agent with
|
||||||
class="core-pane"
|
the same name reuses it.
|
||||||
id="core-pane-kept"
|
</p>
|
||||||
data-tab-pane="kept"
|
<div id="tombstones-section">
|
||||||
role="tabpanel"
|
<p class="meta">loading…</p>
|
||||||
aria-labelledby="core-tab-kept"
|
</div>
|
||||||
>
|
<div id="tombstones-stale-perms"></div>
|
||||||
<p class="meta">
|
|
||||||
kept state from previously tombstoned agents — recreating an agent
|
|
||||||
with the same name reuses it.
|
|
||||||
</p>
|
|
||||||
<div id="tombstones-section">
|
|
||||||
<p class="meta">loading…</p>
|
|
||||||
</div>
|
|
||||||
<div id="tombstones-stale-perms"></div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- C0NT41N3R L04D: live cpu + memory per agent container, from
|
|
||||||
cgroup v2 on the host. Polled only while this sub-tab is active. -->
|
|
||||||
<section
|
|
||||||
class="core-pane"
|
|
||||||
id="core-pane-load"
|
|
||||||
data-tab-pane="load"
|
|
||||||
role="tabpanel"
|
|
||||||
aria-labelledby="core-tab-load"
|
|
||||||
>
|
|
||||||
<p class="meta">
|
|
||||||
live cpu + memory per agent container, from cgroup v2 on the host. cpu
|
|
||||||
is % of total host capacity (all cores), sampled over ~200ms each
|
|
||||||
refresh; polled every 5s while this tab is open. network is omitted on
|
|
||||||
purpose — agents share the host netns, so there is no per-container
|
|
||||||
counter (see <code>docs/networking/network.md</code>).
|
|
||||||
</p>
|
|
||||||
<div id="container-load-section">
|
|
||||||
<p class="meta">loading…</p>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<script type="module" src="/static/core.js" defer></script>
|
<script type="module" src="/static/core.js" defer></script>
|
||||||
|
|
|
||||||
|
|
@ -1,27 +1,20 @@
|
||||||
// C0R3 page entry (/core.html). The host/coordinator surface, its own
|
// C0R3 page entry (/core.html). The host/coordinator surface, its own
|
||||||
// standalone page so the dashboard tab strip stays lean. Two sub-tabs
|
// standalone page so the dashboard tab strip stays lean: kept state
|
||||||
// via <hive-tab-strip> (default = K3PT ST4T3): kept state (tombstones)
|
// (tombstones) and the stale permission entries.
|
||||||
// and container load.
|
|
||||||
//
|
//
|
||||||
// Rebuild queue + meta inputs live at /builds.html (the build lifecycle
|
// This page is its own esbuild bundle with its own runtime: it cold-loads
|
||||||
// hub), not here. This page is its own esbuild bundle with its own
|
// /api/state and subscribes to /dashboard/stream (the same broker event
|
||||||
// runtime: it cold-loads /api/state and subscribes to /dashboard/stream
|
// channel the dashboard uses), maintaining its own copy of the tombstones
|
||||||
// (the same broker event channel the dashboard uses), maintaining its
|
// state.
|
||||||
// own copy of the tombstones state.
|
|
||||||
|
|
||||||
import { $, form, openStream, initServerWarnings } from "./common.js";
|
import { $, form, openStream, initServerWarnings } from "./common.js";
|
||||||
import { el } from "@hive/shared/dom.js";
|
import { el } from "@hive/shared/dom.js";
|
||||||
import { asyncBtn, bindAsyncForms } from "@hive/shared/forms.js";
|
import { asyncBtn, bindAsyncForms } from "@hive/shared/forms.js";
|
||||||
import "@hive/shared/hive-tab-strip.js";
|
|
||||||
|
|
||||||
// ─── derived state (own copies; this bundle has its own runtime) ──────────
|
// ─── derived state (own copies; this bundle has its own runtime) ──────────
|
||||||
let tombstonesState = [];
|
let tombstonesState = [];
|
||||||
// ContainerView list from the last /api/state snapshot — used by renderContainerLoad
|
|
||||||
// to splice in the configured cpu_quota/memory_max limits alongside live cgroup data.
|
|
||||||
let containersState = [];
|
|
||||||
function syncFromSnapshot(s) {
|
function syncFromSnapshot(s) {
|
||||||
tombstonesState = (s.tombstones || []).slice();
|
tombstonesState = (s.tombstones || []).slice();
|
||||||
containersState = (s.containers || []).slice();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── kept state (tombstones) ──────────────────────────────────────────────
|
// ─── kept state (tombstones) ──────────────────────────────────────────────
|
||||||
|
|
@ -187,273 +180,6 @@ async function fetchAndRenderStalePerms() {
|
||||||
stalePermsLoaded = true;
|
stalePermsLoaded = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── container load (live cgroup poll while the LOAD sub-tab is open) ──────
|
|
||||||
let containerLoadTimer = null;
|
|
||||||
|
|
||||||
function cloadFmtBytes(n) {
|
|
||||||
if (!Number.isFinite(n) || n <= 0) return "0";
|
|
||||||
const u = ["B", "KiB", "MiB", "GiB", "TiB"];
|
|
||||||
let i = 0;
|
|
||||||
let v = n;
|
|
||||||
while (v >= 1024 && i < u.length - 1) {
|
|
||||||
v /= 1024;
|
|
||||||
i += 1;
|
|
||||||
}
|
|
||||||
return v.toFixed(v < 10 && i > 0 ? 1 : 0) + " " + u[i];
|
|
||||||
}
|
|
||||||
function cloadMeter(pct) {
|
|
||||||
const p = Math.max(0, Math.min(100, pct));
|
|
||||||
const cls = p >= 90 ? "hot" : p >= 70 ? "warn" : "";
|
|
||||||
const m = document.createElement("span");
|
|
||||||
m.className = "cload-meter";
|
|
||||||
m.title = p.toFixed(0) + "%";
|
|
||||||
const fill = document.createElement("span");
|
|
||||||
fill.className = "fill" + (cls ? " " + cls : "");
|
|
||||||
fill.style.width = p + "%";
|
|
||||||
m.append(fill);
|
|
||||||
return m;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stash the last load rows so re-renders triggered by SSE state updates can
|
|
||||||
// call renderContainerLoad(lastLoadRows) without waiting for the next poll.
|
|
||||||
let lastLoadRows = [];
|
|
||||||
|
|
||||||
function renderContainerLoad(rows) {
|
|
||||||
lastLoadRows = rows;
|
|
||||||
const root = $("container-load-section");
|
|
||||||
if (!root) return;
|
|
||||||
if (!Array.isArray(rows) || rows.length === 0) {
|
|
||||||
root.replaceChildren();
|
|
||||||
const p = document.createElement("p");
|
|
||||||
p.className = "meta";
|
|
||||||
p.textContent = "no running agent containers";
|
|
||||||
root.append(p);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build a name→ContainerView lookup so we can splice in the configured
|
|
||||||
// cpu_quota / memory_max limits (set in meta/resource-limits.json and
|
|
||||||
// resolved by the server) next to the live cgroup readings.
|
|
||||||
const cvByName = new Map(containersState.map((c) => [c.name, c]));
|
|
||||||
|
|
||||||
const table = document.createElement("table");
|
|
||||||
table.className = "hive-stats-table";
|
|
||||||
// "cpu cap" / "mem cap" show the configured ceilings from ContainerView
|
|
||||||
// (effective drop-in values, take effect on next start/restart).
|
|
||||||
// "limit" remains the live cgroup memory ceiling from /api/container-resources.
|
|
||||||
table.innerHTML =
|
|
||||||
"<thead><tr><th>agent</th><th>cpu</th><th>memory</th>" +
|
|
||||||
"<th>peak</th><th>limit</th><th>disk</th>" +
|
|
||||||
'<th class="cload-cap-th" title="configured ceiling — takes effect on next start">cpu cap</th>' +
|
|
||||||
'<th class="cload-cap-th" title="configured ceiling — takes effect on next start">mem cap</th>' +
|
|
||||||
"<th></th></tr></thead>";
|
|
||||||
const tb = document.createElement("tbody");
|
|
||||||
for (const r of rows) {
|
|
||||||
const cv = cvByName.get(r.name);
|
|
||||||
const tr = document.createElement("tr");
|
|
||||||
const name = document.createElement("td");
|
|
||||||
name.textContent = r.name;
|
|
||||||
tr.append(name);
|
|
||||||
const cpu = document.createElement("td");
|
|
||||||
cpu.className = "num";
|
|
||||||
cpu.append(
|
|
||||||
(Number(r.cpu_pct) || 0).toFixed(1) + "%",
|
|
||||||
cloadMeter(Number(r.cpu_pct) || 0),
|
|
||||||
);
|
|
||||||
tr.append(cpu);
|
|
||||||
const mem = document.createElement("td");
|
|
||||||
mem.className = "num";
|
|
||||||
const memCur = Number(r.mem_current_bytes) || 0;
|
|
||||||
if (r.mem_max_bytes) {
|
|
||||||
mem.append(
|
|
||||||
cloadFmtBytes(memCur),
|
|
||||||
cloadMeter((100 * memCur) / r.mem_max_bytes),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
mem.textContent = cloadFmtBytes(memCur);
|
|
||||||
}
|
|
||||||
tr.append(mem);
|
|
||||||
const peak = document.createElement("td");
|
|
||||||
peak.className = "num";
|
|
||||||
peak.textContent = r.mem_peak_bytes
|
|
||||||
? cloadFmtBytes(Number(r.mem_peak_bytes))
|
|
||||||
: "—";
|
|
||||||
tr.append(peak);
|
|
||||||
const lim = document.createElement("td");
|
|
||||||
lim.className = "num";
|
|
||||||
lim.textContent = r.mem_max_bytes
|
|
||||||
? cloadFmtBytes(Number(r.mem_max_bytes))
|
|
||||||
: "∞";
|
|
||||||
tr.append(lim);
|
|
||||||
// Disk: on-disk footprint (state/ + work/ + container writable rootfs, shared
|
|
||||||
// nix store excluded). Measured out-of-band every 30 min server-side;
|
|
||||||
// null means unknown (not measured yet, or the measurement failed).
|
|
||||||
const disk = document.createElement("td");
|
|
||||||
disk.className = "num";
|
|
||||||
disk.title =
|
|
||||||
"state/ + work/ + container writable rootfs (shared nix store excluded); measured every 30 min";
|
|
||||||
disk.textContent =
|
|
||||||
r.disk_bytes != null ? cloadFmtBytes(Number(r.disk_bytes)) : "unknown";
|
|
||||||
tr.append(disk);
|
|
||||||
|
|
||||||
// Configured CPU / memory caps from ContainerView (resolved effective
|
|
||||||
// values: per-agent override when set, hive-wide default otherwise).
|
|
||||||
const cpuCap = document.createElement("td");
|
|
||||||
cpuCap.className = "num cload-cap";
|
|
||||||
cpuCap.title = "configured ceiling — takes effect on next start";
|
|
||||||
cpuCap.textContent = cv?.cpu_quota || "—";
|
|
||||||
tr.append(cpuCap);
|
|
||||||
const memCap = document.createElement("td");
|
|
||||||
memCap.className = "num cload-cap";
|
|
||||||
memCap.title = "configured ceiling — takes effect on next start";
|
|
||||||
memCap.textContent = cv?.memory_max || "—";
|
|
||||||
tr.append(memCap);
|
|
||||||
|
|
||||||
// S3T button toggles the inline edit row for this agent.
|
|
||||||
const actTd = document.createElement("td");
|
|
||||||
const setBtn = document.createElement("button");
|
|
||||||
setBtn.type = "button";
|
|
||||||
setBtn.className = "btn btn-sm cload-set-btn";
|
|
||||||
setBtn.textContent = "S3T";
|
|
||||||
setBtn.title = "set CPU / memory cap for " + r.name;
|
|
||||||
tr.append(actTd);
|
|
||||||
actTd.append(setBtn);
|
|
||||||
tb.append(tr);
|
|
||||||
|
|
||||||
// Inline edit row (hidden by default, toggled by the S3T button).
|
|
||||||
const editRow = document.createElement("tr");
|
|
||||||
editRow.className = "cload-edit-row";
|
|
||||||
editRow.hidden = true;
|
|
||||||
const editTd = document.createElement("td");
|
|
||||||
editTd.colSpan = 9;
|
|
||||||
editTd.className = "cload-edit-cell";
|
|
||||||
|
|
||||||
const editForm = document.createElement("form");
|
|
||||||
editForm.className = "cload-edit-form";
|
|
||||||
editForm.addEventListener("submit", async (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
const cpuInput = editForm.querySelector(".cload-cpu-input");
|
|
||||||
const memInput = editForm.querySelector(".cload-mem-input");
|
|
||||||
const errSpan = editForm.querySelector(".cload-edit-err");
|
|
||||||
const submitBtn = editForm.querySelector('[type="submit"]');
|
|
||||||
errSpan.hidden = true;
|
|
||||||
submitBtn.disabled = true;
|
|
||||||
try {
|
|
||||||
const body = new URLSearchParams({
|
|
||||||
cpu_quota: cpuInput.value.trim(),
|
|
||||||
memory_max: memInput.value.trim(),
|
|
||||||
});
|
|
||||||
const resp = await fetch(
|
|
||||||
"/api/resource-limits/" + encodeURIComponent(r.name),
|
|
||||||
{ method: "POST", body },
|
|
||||||
);
|
|
||||||
if (!resp.ok) {
|
|
||||||
errSpan.textContent = await resp
|
|
||||||
.text()
|
|
||||||
.catch(() => "error " + resp.status);
|
|
||||||
errSpan.hidden = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Success: collapse the edit row. The SSE rescan will push updated
|
|
||||||
// ContainerView data (cpu_quota/memory_max) to containersState,
|
|
||||||
// triggering a re-render of the cap columns via refreshContainerLoad.
|
|
||||||
editRow.hidden = true;
|
|
||||||
setBtn.textContent = "S3T";
|
|
||||||
} catch (err) {
|
|
||||||
errSpan.textContent = String(err);
|
|
||||||
errSpan.hidden = false;
|
|
||||||
} finally {
|
|
||||||
submitBtn.disabled = false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const cpuLabel = document.createElement("label");
|
|
||||||
cpuLabel.className = "cload-edit-label";
|
|
||||||
cpuLabel.textContent = "cpu quota";
|
|
||||||
const cpuInput = document.createElement("input");
|
|
||||||
cpuInput.type = "text";
|
|
||||||
cpuInput.className = "cload-cpu-input";
|
|
||||||
// Pre-fill with the current *effective* value (not just a placeholder)
|
|
||||||
// so submitting after editing only the other field round-trips this one
|
|
||||||
// unchanged. The form always posts both fields, and the server treats
|
|
||||||
// an empty field as "clear this override" — a placeholder-only hint
|
|
||||||
// reads as filled to the eye but posts as empty, silently resetting
|
|
||||||
// whichever field the user didn't touch.
|
|
||||||
cpuInput.value = cv ? cv.cpu_quota : "";
|
|
||||||
cpuInput.placeholder = "e.g. 200%";
|
|
||||||
cpuInput.title =
|
|
||||||
'systemd CPUQuota= value (e.g. "400%"). empty = use hive default';
|
|
||||||
cpuLabel.append(cpuInput);
|
|
||||||
|
|
||||||
const memLabel = document.createElement("label");
|
|
||||||
memLabel.className = "cload-edit-label";
|
|
||||||
memLabel.textContent = "mem max";
|
|
||||||
const memInput = document.createElement("input");
|
|
||||||
memInput.type = "text";
|
|
||||||
memInput.className = "cload-mem-input";
|
|
||||||
// Same reasoning as cpuInput above.
|
|
||||||
memInput.value = cv ? cv.memory_max : "";
|
|
||||||
memInput.placeholder = "e.g. 8G";
|
|
||||||
memInput.title =
|
|
||||||
'systemd MemoryMax= value (e.g. "8G", "50%", "infinity"). empty = use hive default';
|
|
||||||
memLabel.append(memInput);
|
|
||||||
|
|
||||||
const submitBtn = document.createElement("button");
|
|
||||||
submitBtn.type = "submit";
|
|
||||||
submitBtn.className = "btn btn-restart cload-save-btn";
|
|
||||||
submitBtn.textContent = "S4V3";
|
|
||||||
|
|
||||||
const hintSpan = document.createElement("span");
|
|
||||||
hintSpan.className = "meta cload-edit-hint";
|
|
||||||
hintSpan.textContent = "↺ restart to apply to a running container";
|
|
||||||
|
|
||||||
const errSpan = document.createElement("span");
|
|
||||||
errSpan.className = "cload-edit-err";
|
|
||||||
errSpan.hidden = true;
|
|
||||||
|
|
||||||
editForm.append(cpuLabel, memLabel, submitBtn, hintSpan, errSpan);
|
|
||||||
editTd.append(editForm);
|
|
||||||
editRow.append(editTd);
|
|
||||||
tb.append(editRow);
|
|
||||||
|
|
||||||
setBtn.addEventListener("click", () => {
|
|
||||||
const open = !editRow.hidden;
|
|
||||||
editRow.hidden = open;
|
|
||||||
setBtn.textContent = open ? "S3T" : "✕";
|
|
||||||
});
|
|
||||||
}
|
|
||||||
table.append(tb);
|
|
||||||
root.replaceChildren(table);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function refreshContainerLoad() {
|
|
||||||
try {
|
|
||||||
const resp = await fetch("/api/container-resources");
|
|
||||||
if (!resp.ok) throw new Error("http " + resp.status);
|
|
||||||
renderContainerLoad(await resp.json());
|
|
||||||
} catch (e) {
|
|
||||||
const root = $("container-load-section");
|
|
||||||
if (root) {
|
|
||||||
root.replaceChildren();
|
|
||||||
const p = document.createElement("p");
|
|
||||||
p.className = "meta";
|
|
||||||
p.textContent = "container load fetch failed: " + e;
|
|
||||||
root.append(p);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function startContainerLoadPolling() {
|
|
||||||
refreshContainerLoad();
|
|
||||||
if (containerLoadTimer) return;
|
|
||||||
containerLoadTimer = setInterval(refreshContainerLoad, 5000);
|
|
||||||
}
|
|
||||||
function stopContainerLoadPolling() {
|
|
||||||
if (containerLoadTimer) {
|
|
||||||
clearInterval(containerLoadTimer);
|
|
||||||
containerLoadTimer = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── render-all (cold load + any full re-render) ──────────────────────────
|
// ─── render-all (cold load + any full re-render) ──────────────────────────
|
||||||
function renderAll() {
|
function renderAll() {
|
||||||
renderTombstones({ tombstones: tombstonesState });
|
renderTombstones({ tombstones: tombstonesState });
|
||||||
|
|
@ -465,19 +191,6 @@ const SSE_HANDLERS = {
|
||||||
tombstonesState = (ev.tombstones || []).slice();
|
tombstonesState = (ev.tombstones || []).slice();
|
||||||
renderTombstones({ tombstones: tombstonesState });
|
renderTombstones({ tombstones: tombstonesState });
|
||||||
},
|
},
|
||||||
// When a container's state changes (e.g. after resource-limits update
|
|
||||||
// triggers rescan_containers_and_emit), update containersState in place
|
|
||||||
// so the cap columns in the LOAD table reflect the new configured values.
|
|
||||||
container_state_changed(ev) {
|
|
||||||
if (!ev.container) return;
|
|
||||||
const idx = containersState.findIndex((c) => c.name === ev.container.name);
|
|
||||||
if (idx >= 0) {
|
|
||||||
containersState[idx] = ev.container;
|
|
||||||
} else {
|
|
||||||
containersState.push(ev.container);
|
|
||||||
}
|
|
||||||
if (lastLoadRows.length) renderContainerLoad(lastLoadRows);
|
|
||||||
},
|
|
||||||
// Refresh the stale-perms sub-section when perm data changes (a ghost
|
// Refresh the stale-perms sub-section when perm data changes (a ghost
|
||||||
// was cleared, or perms were saved for an agent whose name collides).
|
// was cleared, or perms were saved for an agent whose name collides).
|
||||||
capabilities_changed(_ev) {
|
capabilities_changed(_ev) {
|
||||||
|
|
@ -511,33 +224,16 @@ async function init() {
|
||||||
// to the bare `ok` response page.
|
// to the bare `ok` response page.
|
||||||
bindAsyncForms(() => refreshState());
|
bindAsyncForms(() => refreshState());
|
||||||
|
|
||||||
// Hash-routed sub-tab strip; default K3PT ST4T3. Container-load
|
|
||||||
// polling runs only while the LOAD sub-tab is open (cpu is a short
|
|
||||||
// two-sample read each refresh on the server).
|
|
||||||
document.getElementById("core-tabbar").configure({
|
|
||||||
tabs: [
|
|
||||||
{ id: "kept", label: "K3PT ST4T3" },
|
|
||||||
{ id: "load", label: "C0NT41N3R L04D" },
|
|
||||||
],
|
|
||||||
defaultId: "kept",
|
|
||||||
onShow: (id) => {
|
|
||||||
if (id === "load") startContainerLoadPolling();
|
|
||||||
else stopContainerLoadPolling();
|
|
||||||
// Lazy-load stale-perms on first K3PT ST4T3 activation; always
|
|
||||||
// re-fetch on subsequent visits in case perms changed.
|
|
||||||
if (id === "kept") fetchAndRenderStalePerms();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
await refreshState();
|
await refreshState();
|
||||||
|
fetchAndRenderStalePerms();
|
||||||
|
|
||||||
// kinds= matches SSE_HANDLERS below verbatim — narrows this from all
|
// kinds= matches SSE_HANDLERS below verbatim — narrows this from all
|
||||||
// 13 wire kinds down to the 4 this page acts on. This page was one
|
// 13 wire kinds down to the 3 this page acts on. This page was one
|
||||||
// of 4 unfiltered `/api/dashboard/stream` subscribers before this
|
// of 4 unfiltered `/api/dashboard/stream` subscribers before this
|
||||||
// (subscription discipline, part 1 of the dashboard-event-stream-
|
// (subscription discipline, part 1 of the dashboard-event-stream-
|
||||||
// split issue).
|
// split issue).
|
||||||
const es = openStream(
|
const es = openStream(
|
||||||
"/api/dashboard/stream?kinds=tombstones_changed,container_state_changed," +
|
"/api/dashboard/stream?kinds=tombstones_changed," +
|
||||||
"capabilities_changed,tool_groups_changed",
|
"capabilities_changed,tool_groups_changed",
|
||||||
);
|
);
|
||||||
if (es) {
|
if (es) {
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
it defines the CSS custom properties, typography, and shared
|
it defines the CSS custom properties, typography, and shared
|
||||||
component rules that this file depends on. */
|
component rules that this file depends on. */
|
||||||
|
|
||||||
/* SYST3M panel rules (rebuild queue / meta inputs / container load)
|
/* SYST3M panel rules (rebuild queue / meta inputs / kept state)
|
||||||
live in system-sections.css, shared with /core.html. A few are still
|
live in system-sections.css, shared with /core.html. A few are still
|
||||||
referenced from the dashboard (`.rebuild-queue`), so the dashboard
|
referenced from the dashboard (`.rebuild-queue`), so the dashboard
|
||||||
pulls them in here. */
|
pulls them in here. */
|
||||||
|
|
@ -771,6 +771,4 @@ 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 (the SYST3M ›
|
shared `.hive-stats-table` moved to common.css. */
|
||||||
C0NT41N3R L04D table in system-sections.css / on /core.html still
|
|
||||||
uses it). */
|
|
||||||
|
|
|
||||||
|
|
@ -103,7 +103,7 @@
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- SYST3M pane moved to /core.html (the standalone "C0R3" page):
|
<!-- SYST3M pane moved to /core.html (the standalone "C0R3" page):
|
||||||
meta inputs, rebuild queue, kept state, container load. Reached
|
kept state and stale permissions. Reached
|
||||||
from the H0M3 hub. -->
|
from the H0M3 hub. -->
|
||||||
|
|
||||||
<!-- P3RM1SS10NS: per-agent capability grants + tool-group
|
<!-- P3RM1SS10NS: per-agent capability grants + tool-group
|
||||||
|
|
|
||||||
|
|
@ -79,7 +79,7 @@
|
||||||
<span class="home-tile-icon" aria-hidden="true">⬡</span>
|
<span class="home-tile-icon" aria-hidden="true">⬡</span>
|
||||||
<span class="home-tile-label">Core</span>
|
<span class="home-tile-label">Core</span>
|
||||||
</span>
|
</span>
|
||||||
<span class="home-tile-desc">kept state · container load</span>
|
<span class="home-tile-desc">kept state · stale permissions</span>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<!-- API tile: the OpenAPI spec + Swagger UI are always served by
|
<!-- API tile: the OpenAPI spec + Swagger UI are always served by
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,7 @@
|
||||||
// dashboard SPA: the live agent roster.
|
// dashboard SPA: the live agent roster.
|
||||||
//
|
//
|
||||||
// The roster is read by nearly every tab (the container tree, the
|
// The roster is read by nearly every tab (the container tree, the
|
||||||
// capabilities + tool-group matrices, the
|
// capabilities + tool-group matrices, and the selection bar), so
|
||||||
// container-load poll, and the selection bar), so
|
|
||||||
// it lives here as a single source of truth that each domain module
|
// it lives here as a single source of truth that each domain module
|
||||||
// imports rather than threading through call signatures. Everything
|
// imports rather than threading through call signatures. Everything
|
||||||
// else in the dashboard is domain-local and lives with its own module.
|
// else in the dashboard is domain-local and lives with its own module.
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,7 @@
|
||||||
chrome pattern as /flow.html: the back-link header +
|
chrome pattern as /flow.html: the back-link header +
|
||||||
title (.page-header / .page-back / .page-title) live in
|
title (.page-header / .page-back / .page-title) live in
|
||||||
@hive/shared/chrome.css.
|
@hive/shared/chrome.css.
|
||||||
`.hive-stats-table` also lives in common.css — it's shared with the
|
`.hive-stats-table` lives in common.css. Only the ST4TS-specific
|
||||||
dashboard SYST3M › C0NT41N3R L04D table. Only the ST4TS-specific
|
|
||||||
window selector, summary chips, and bars are page-specific. */
|
window selector, summary chips, and bars are page-specific. */
|
||||||
|
|
||||||
body.stats-shell {
|
body.stats-shell {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
/* ─── SYST3M/BU1LDS section panels — shared by dashboard, /core.html,
|
/* ─── SYST3M/BU1LDS section panels — shared by dashboard, /core.html,
|
||||||
and /builds.html ──────────────────────────────────────────────────
|
and /builds.html ──────────────────────────────────────────────────
|
||||||
Rebuild queue, meta inputs, kept state, and container-load panels.
|
Rebuild queue, meta inputs and kept-state panels.
|
||||||
Imported by core.css, dashboard.css, and builds.css. */
|
Imported by core.css, dashboard.css, and builds.css. */
|
||||||
|
|
||||||
/* ─── meta inputs panel ────────────────────────────────────────────── */
|
/* ─── meta inputs panel ────────────────────────────────────────────── */
|
||||||
|
|
@ -198,83 +198,6 @@
|
||||||
color: var(--fg);
|
color: var(--fg);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ─── SYST3M › C0NT41N3R L04D: live cgroup cpu/mem ─────────────────────────────
|
|
||||||
Reuses the shared `.hive-stats-table` styling (in common.css); only
|
|
||||||
the inline meter bar is new. */
|
|
||||||
.cload-meter {
|
|
||||||
display: inline-block;
|
|
||||||
width: 6em;
|
|
||||||
height: 10px;
|
|
||||||
vertical-align: middle;
|
|
||||||
margin-left: 6px;
|
|
||||||
background: var(--bg-elev);
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
border-radius: 3px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
.cload-meter .fill {
|
|
||||||
display: block;
|
|
||||||
height: 100%;
|
|
||||||
background: var(--green);
|
|
||||||
}
|
|
||||||
.cload-meter .fill.warn {
|
|
||||||
background: var(--amber);
|
|
||||||
}
|
|
||||||
.cload-meter .fill.hot {
|
|
||||||
background: var(--red);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Configured-cap columns: muted header so they read apart from live metrics.
|
|
||||||
The S3T button sits in its own column; keep it compact. */
|
|
||||||
.cload-cap-th {
|
|
||||||
color: var(--muted);
|
|
||||||
}
|
|
||||||
.cload-cap {
|
|
||||||
color: var(--muted);
|
|
||||||
}
|
|
||||||
.cload-set-btn {
|
|
||||||
padding: 0.1em 0.5em;
|
|
||||||
font-size: 0.8em;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Inline edit row — spans all columns, contains a mini-form. */
|
|
||||||
.cload-edit-row td {
|
|
||||||
border-top: none;
|
|
||||||
padding-top: 0;
|
|
||||||
}
|
|
||||||
.cload-edit-cell {
|
|
||||||
padding: 0.2em 0.4em 0.6em 1.2em;
|
|
||||||
}
|
|
||||||
.cload-edit-form {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.5em;
|
|
||||||
}
|
|
||||||
.cload-edit-label {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.3em;
|
|
||||||
font-size: 0.85em;
|
|
||||||
color: var(--muted);
|
|
||||||
}
|
|
||||||
.cload-cpu-input,
|
|
||||||
.cload-mem-input {
|
|
||||||
width: 7em;
|
|
||||||
font-size: 0.9em;
|
|
||||||
}
|
|
||||||
.cload-save-btn {
|
|
||||||
padding: 0.15em 0.6em;
|
|
||||||
font-size: 0.85em;
|
|
||||||
}
|
|
||||||
.cload-edit-hint {
|
|
||||||
font-size: 0.8em;
|
|
||||||
}
|
|
||||||
.cload-edit-err {
|
|
||||||
color: var(--red);
|
|
||||||
font-size: 0.85em;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Remove the now-dead `.rqe-step` rule (the `step` sub-step label was
|
/* Remove the now-dead `.rqe-step` rule (the `step` sub-step label was
|
||||||
retired from the wire in `feat(job_queue): retire the now-off-wire step
|
retired from the wire in `feat(job_queue): retire the now-off-wire step
|
||||||
sub-step label` — the rule has had no JS referents since then). */
|
sub-step label` — the rule has had no JS referents since then). */
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue