build agents only from internal/meta; delete the hive's local meta flake
The hive no longer renders, locks or commits a meta flake. A rebuild's MetaSync node picks the internal/meta commit and records it with the hyperhive and config pins it holds; Prebuild, Swap and Create build from it, and a head that does not pin the agent's current config fails the build naming the agent. hive-priv's meta_rev is required. Removed: meta.rs sync/lock/render, the manual-rebuild relock, the boot sweep's hyperhive bump (MetaLock), the update-meta-inputs form and its panel, MetaWindow, the hive's hyperhive_rev setting. Kept: agent_set::refresh holds what sync_agents also did (topology.json, the gateway's agent-socket map and nginx include), called from spawn, destroy, rebuild and the migration. The boot reconcile and the stale-start upgrade compare the agent's marker with the hyperhive pin of internal/meta; the dashboard's deployed chip reads the config marker a successful build writes.
This commit is contained in:
parent
cea4827918
commit
4aa11b3a94
64 changed files with 1042 additions and 3793 deletions
|
|
@ -597,7 +597,7 @@ export function renderContainers(s) {
|
|||
|
||||
// Containers come from the derived map (event-driven) rather than
|
||||
// `s.containers`; `s` still supplies hostname (for the web-ui
|
||||
// link) and tombstones/meta_inputs (not event-derived yet). The
|
||||
// link) and tombstones (not event-derived yet). The
|
||||
// tree builder handles the ordering — we don't pre-sort here.
|
||||
const containers = Array.from(containersState.values());
|
||||
const portConflicts = derivePortConflicts(containers);
|
||||
|
|
|
|||
|
|
@ -1,9 +1,8 @@
|
|||
// The build lifecycle tabs of the dashboard — rebuild queue, live build
|
||||
// log, meta inputs, and build log history. Not an entry point: tabs.js
|
||||
// log, and build log history. Not an entry point: tabs.js
|
||||
// imports it, owns the tab strip and the dashboard event stream, and calls
|
||||
// the exports below. Three tabs, the panes' ids matching the tab ids:
|
||||
// the exports below. Two tabs, the panes' ids matching the tab ids:
|
||||
// - rebuild (R3BU1LD QU3U3): pending + running rebuilds + live build log
|
||||
// - meta (M3T4 1NPUTS): nix flake input selector + meta-update
|
||||
// - buildlogs (BUILD L0GS): all-agents build log history, lazy-loaded
|
||||
// the first time the tab is shown
|
||||
|
||||
|
|
@ -11,12 +10,11 @@ import { $, fmtAgeSecs, openBuildLogStream } from "./common.js";
|
|||
import { el } from "@hive/shared/dom.js";
|
||||
import { themedConfirm } from "@hive/shared/modal.js";
|
||||
import { h, render } from "preact";
|
||||
import { fmtAgo, fmtDuration, truncate } from "./util.js";
|
||||
import { fmtDuration } from "./util.js";
|
||||
import { JobqGraph } from "@hive/shared/jobq-graph.js";
|
||||
|
||||
// ─── derived state ───────────────────────────────────────────────────────────
|
||||
let metaInputsState = [];
|
||||
let metaUpdateRunning = false;
|
||||
|
||||
// Kept in sync from JobqGraph's `onUpdate` callback (see mountRebuildQueue
|
||||
// below) — the component owns fetching GET /api/jobq/graph and renders
|
||||
// the tree itself; this flat array exists only for the two things it
|
||||
|
|
@ -24,133 +22,6 @@ let metaUpdateRunning = false;
|
|||
let jobqNodes = [];
|
||||
let jobqGraphToken = 0;
|
||||
|
||||
function syncFromSnapshot(s) {
|
||||
metaInputsState = (s.meta_inputs || []).slice();
|
||||
metaUpdateRunning = !!s.meta_update_running;
|
||||
}
|
||||
|
||||
// ─── meta inputs ─────────────────────────────────────────────────────────────
|
||||
function renderMetaInputs(s) {
|
||||
const root = $("meta-inputs-section");
|
||||
if (!root) return;
|
||||
// Snapshot ticked checkboxes before wiping so a concurrent
|
||||
// MetaInputsChanged doesn't silently clear a pending selection.
|
||||
const checkedInputs = new Set(
|
||||
Array.from(
|
||||
root.querySelectorAll('input[type="checkbox"][data-meta-input]:checked'),
|
||||
).map((cb) => cb.dataset.metaInput),
|
||||
);
|
||||
root.replaceChildren();
|
||||
const inputs = s.meta_inputs || [];
|
||||
if (!inputs.length) {
|
||||
root.append(el("p", { class: "empty" }, "meta repo not seeded yet"));
|
||||
return;
|
||||
}
|
||||
if (metaUpdateRunning) {
|
||||
root.append(
|
||||
el(
|
||||
"p",
|
||||
{ class: "meta-update-running" },
|
||||
"⏳ meta-update running — flake lock bump + affected agents rebuilding. " +
|
||||
"watch the agent cards for per-rebuild progress.",
|
||||
),
|
||||
);
|
||||
}
|
||||
const f = el("form", {
|
||||
method: "POST",
|
||||
action: "/api/meta-update",
|
||||
class: "meta-inputs-form",
|
||||
"data-async": "",
|
||||
"data-no-refresh": "",
|
||||
"data-confirm":
|
||||
"update selected meta flake inputs + rebuild affected agents?",
|
||||
});
|
||||
const bulk = el("div", { class: "meta-inputs-bulk" });
|
||||
const selAll = el(
|
||||
"button",
|
||||
{ type: "button", class: "meta-bulk-btn" },
|
||||
"select all",
|
||||
);
|
||||
const selNone = el(
|
||||
"button",
|
||||
{ type: "button", class: "meta-bulk-btn" },
|
||||
"select none",
|
||||
);
|
||||
bulk.append("bulk: ", selAll, " ", selNone);
|
||||
f.append(bulk);
|
||||
const ul = el("ul", { class: "meta-inputs" });
|
||||
for (const inp of inputs) {
|
||||
const depth = (inp.name.match(/\//g) || []).length;
|
||||
const leaf = inp.name.slice(inp.name.lastIndexOf("/") + 1);
|
||||
const li = el("li");
|
||||
if (depth > 0) li.style.marginLeft = depth * 1.3 + "em";
|
||||
const id = "meta-input-" + inp.name.replace(/[^a-z0-9-]/gi, "_");
|
||||
const cb = el("input", {
|
||||
type: "checkbox",
|
||||
name: "meta_input_" + inp.name,
|
||||
id,
|
||||
value: inp.name,
|
||||
"data-meta-input": inp.name,
|
||||
});
|
||||
if (checkedInputs.has(inp.name)) cb.checked = true;
|
||||
const label = el("label", { for: id, title: inp.name });
|
||||
label.append(cb);
|
||||
if (depth > 0) label.append(el("span", { class: "meta-input-twig" }, "└ "));
|
||||
label.append(
|
||||
el("span", { class: "meta-input-name" }, leaf),
|
||||
" ",
|
||||
el("code", { class: "meta-input-rev" }, inp.rev.slice(0, 12)),
|
||||
" ",
|
||||
el("span", { class: "meta-input-ts" }, fmtAgo(inp.last_modified)),
|
||||
);
|
||||
if (inp.url) {
|
||||
label.append(
|
||||
" ",
|
||||
el(
|
||||
"span",
|
||||
{ class: "meta-input-url", title: inp.url },
|
||||
"· " + truncate(inp.url, 48),
|
||||
),
|
||||
);
|
||||
}
|
||||
li.append(label);
|
||||
ul.append(li);
|
||||
}
|
||||
f.append(ul);
|
||||
const hidden = el("input", { type: "hidden", name: "inputs", value: "" });
|
||||
f.append(hidden);
|
||||
const btn = el(
|
||||
"button",
|
||||
{
|
||||
type: "submit",
|
||||
class: "btn btn-meta-update",
|
||||
disabled: "",
|
||||
},
|
||||
metaUpdateRunning ? "⏳ UPD4T1NG…" : "◆ UPD4TE & R3BU1LD",
|
||||
);
|
||||
f.append(btn);
|
||||
function refreshDisabled() {
|
||||
const any = f.querySelectorAll("input[data-meta-input]:checked").length > 0;
|
||||
if (any && !metaUpdateRunning) btn.removeAttribute("disabled");
|
||||
else btn.setAttribute("disabled", "");
|
||||
}
|
||||
f.addEventListener("change", refreshDisabled);
|
||||
function setAllChecked(val) {
|
||||
for (const b of f.querySelectorAll("input[data-meta-input]"))
|
||||
b.checked = val;
|
||||
refreshDisabled();
|
||||
}
|
||||
selAll.addEventListener("click", () => setAllChecked(true));
|
||||
selNone.addEventListener("click", () => setAllChecked(false));
|
||||
f.addEventListener("submit", () => {
|
||||
const selected = Array.from(
|
||||
f.querySelectorAll("input[data-meta-input]:checked"),
|
||||
).map((b) => b.dataset.metaInput);
|
||||
hidden.value = selected.join(",");
|
||||
});
|
||||
root.append(f);
|
||||
}
|
||||
|
||||
// ─── rebuild queue ────────────────────────────────────────────────────────────
|
||||
// R3BU1LD QU3U3 is `JobqGraph` (Preact, rendered via plain `render(h(...))`
|
||||
// — no JSX pragma needed for either call, no mount wrapper: `render` is
|
||||
|
|
@ -594,20 +465,13 @@ async function fetchBuild() {
|
|||
|
||||
/** Mount the rebuild queue and wire the build-log refresh button. Called
|
||||
* once, after the tab strip exists. The queue self-fetches on mount
|
||||
* (JobqGraph's own effect), unlike the meta inputs, which wait for the
|
||||
* snapshot. */
|
||||
* (JobqGraph's own effect). */
|
||||
export function initBuilds() {
|
||||
const refresh = $("build-refresh");
|
||||
if (refresh) refresh.addEventListener("click", fetchBuild);
|
||||
mountRebuildQueue();
|
||||
}
|
||||
|
||||
/** The meta inputs, from an `/api/state` snapshot. */
|
||||
export function syncBuildsFromSnapshot(s) {
|
||||
syncFromSnapshot(s);
|
||||
renderMetaInputs({ meta_inputs: metaInputsState });
|
||||
}
|
||||
|
||||
/** A tab was shown: BUILD L0GS loads its list the first time. */
|
||||
export function onBuildsTabShown(tab) {
|
||||
if (tab === "buildlogs" && !buildLogsLoaded) {
|
||||
|
|
@ -629,13 +493,3 @@ export function onRebuildQueueChanged() {
|
|||
buildRefreshTimer = setTimeout(fetchBuild, 2000);
|
||||
}
|
||||
}
|
||||
|
||||
export function onMetaInputsChanged(ev) {
|
||||
metaInputsState = (ev.inputs || []).slice();
|
||||
renderMetaInputs({ meta_inputs: metaInputsState });
|
||||
}
|
||||
|
||||
export function onMetaUpdateRunning(ev) {
|
||||
metaUpdateRunning = !!ev.running;
|
||||
renderMetaInputs({ meta_inputs: metaInputsState });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
it defines the CSS custom properties, typography, and shared
|
||||
component rules that this file depends on. */
|
||||
|
||||
/* SYST3M panel rules (rebuild queue / meta inputs / kept state)
|
||||
/* SYST3M panel rules (rebuild queue / kept state)
|
||||
live in system-sections.css. */
|
||||
@import "./system-sections.css";
|
||||
@import "@hive/shared/jobq-rollup.css";
|
||||
|
|
@ -674,7 +674,7 @@ body.dashboard-shell.has-selection {
|
|||
shared `.hive-stats-table` moved to common.css. */
|
||||
|
||||
/* ─── build tabs (builds.js) ────────────────────────────────────────
|
||||
R3BU1LD QU3U3 and M3T4 1NPUTS are styled by system-sections.css and
|
||||
R3BU1LD QU3U3 is styled by system-sections.css and
|
||||
jobq-graph.css; the BUILD L0GS list rules are below. */
|
||||
|
||||
.logs-toolbar {
|
||||
|
|
|
|||
|
|
@ -33,9 +33,9 @@
|
|||
<span class="tab-label">◆ 4G3NTS ◆</span>
|
||||
<span class="tab-count" id="tab-count-agents" hidden></span>
|
||||
</a>
|
||||
<!-- The build lifecycle is three tabs here (builds.js): the rebuild
|
||||
queue with its live log, the meta flake inputs, and the build log
|
||||
history. Kept state (kept.js) is the fourth. -->
|
||||
<!-- The build lifecycle is two tabs here (builds.js): the rebuild
|
||||
queue with its live log, and the build log history. Kept state
|
||||
(kept.js) is the third. -->
|
||||
<a
|
||||
class="tab"
|
||||
id="tab-rebuild"
|
||||
|
|
@ -47,16 +47,7 @@
|
|||
<span class="tab-label">◆ R3BU1LD QU3U3 ◆</span>
|
||||
<span class="tab-count" id="tab-count-rebuild" hidden></span>
|
||||
</a>
|
||||
<a
|
||||
class="tab"
|
||||
id="tab-meta"
|
||||
href="#meta"
|
||||
role="tab"
|
||||
aria-controls="tab-pane-meta"
|
||||
data-tab="meta"
|
||||
>
|
||||
<span class="tab-label">◆ M3T4 1NPUTS ◆</span>
|
||||
</a>
|
||||
|
||||
<a
|
||||
class="tab"
|
||||
id="tab-buildlogs"
|
||||
|
|
@ -126,8 +117,7 @@
|
|||
</div>
|
||||
</section>
|
||||
|
||||
<!-- R3BU1LD QU3U3: pending + running rebuilds, meta-updates, and
|
||||
first-spawns. Rendered from GET /api/jobq/graph by the
|
||||
<!-- R3BU1LD QU3U3: pending + running rebuilds and first-spawns. Rendered from GET /api/jobq/graph by the
|
||||
`JobqGraph` Preact component (@hive/shared/jobq-graph.js);
|
||||
`rebuild_queue_changed` over /api/dashboard/stream is the
|
||||
refresh trigger and carries no payload of its own. -->
|
||||
|
|
@ -140,9 +130,9 @@
|
|||
aria-labelledby="tab-rebuild"
|
||||
>
|
||||
<p class="meta">
|
||||
pending + running rebuilds, meta-updates, and first-spawns. one runs
|
||||
at a time; meta-update cascades nest under their parent. dedup:
|
||||
re-enqueueing a still-queued op collapses into the existing entry.
|
||||
pending + running rebuilds and first-spawns. one runs at a time.
|
||||
dedup: re-enqueueing a still-queued op collapses into the existing
|
||||
entry.
|
||||
</p>
|
||||
<div id="rebuild-queue-section">
|
||||
<p class="meta">loading…</p>
|
||||
|
|
@ -154,26 +144,6 @@
|
|||
<div id="rebuild-live-log" class="rebuild-live-log" hidden></div>
|
||||
</section>
|
||||
|
||||
<!-- M3T4 1NPUTS: select inputs to nix flake update in /meta/. -->
|
||||
<section
|
||||
class="tab-pane"
|
||||
id="tab-pane-meta"
|
||||
data-tab-pane="meta"
|
||||
hidden
|
||||
role="tabpanel"
|
||||
aria-labelledby="tab-meta"
|
||||
>
|
||||
<p class="meta">
|
||||
select inputs to <code>nix flake update</code> in
|
||||
<code>/meta/</code>. selected agents rebuild in sequence after the
|
||||
lock bump; the submitting agent learns each outcome via the usual
|
||||
<code>rebuilt</code> system event.
|
||||
</p>
|
||||
<div id="meta-inputs-section">
|
||||
<p class="meta">loading…</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- BUILD L0GS: all-agents build log history. Click a row to expand
|
||||
stdout + stderr. Live builds stream in real time. Lazy-loaded the
|
||||
first time the tab is shown; auto-refreshes when
|
||||
|
|
|
|||
|
|
@ -1,101 +1,7 @@
|
|||
/* ─── SYST3M/BU1LDS section panels — used by the dashboard ───
|
||||
Rebuild queue, meta inputs and kept-state panels.
|
||||
Rebuild queue and kept-state panels.
|
||||
Imported by dashboard.css. */
|
||||
|
||||
/* ─── meta inputs panel ────────────────────────────────────────────── */
|
||||
.meta-inputs {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0 0 0.8em;
|
||||
display: grid;
|
||||
gap: 0.2em;
|
||||
}
|
||||
.meta-inputs li {
|
||||
padding: 0.25em 0.6em;
|
||||
border: 1px solid var(--border);
|
||||
background: color-mix(in srgb, var(--bg-elev) 60%, transparent);
|
||||
}
|
||||
.meta-inputs label {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 0.5em;
|
||||
cursor: pointer;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
.meta-input-name {
|
||||
color: var(--amber);
|
||||
font-weight: bold;
|
||||
}
|
||||
.meta-input-rev {
|
||||
color: var(--muted);
|
||||
}
|
||||
.meta-input-ts {
|
||||
color: var(--muted);
|
||||
font-size: 0.85em;
|
||||
}
|
||||
.meta-input-url {
|
||||
color: var(--muted);
|
||||
font-size: 0.85em;
|
||||
margin-left: auto;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.meta-inputs-bulk {
|
||||
margin: 0 0 0.5em;
|
||||
font-size: 0.8em;
|
||||
color: var(--muted);
|
||||
}
|
||||
.meta-bulk-btn {
|
||||
font: inherit;
|
||||
font-size: 1em;
|
||||
background: transparent;
|
||||
border: 1px solid var(--purple-dim);
|
||||
color: var(--cyan);
|
||||
padding: 0.1em 0.6em;
|
||||
margin-right: 0.2em;
|
||||
cursor: pointer;
|
||||
}
|
||||
.meta-bulk-btn:hover {
|
||||
border-color: var(--cyan);
|
||||
text-shadow: 0 0 6px currentColor;
|
||||
}
|
||||
.meta-input-twig {
|
||||
color: var(--purple-dim);
|
||||
margin-right: 0.1em;
|
||||
}
|
||||
.btn-meta-update {
|
||||
background: color-mix(in srgb, var(--purple) 12%, transparent);
|
||||
border: 1px solid var(--purple);
|
||||
color: var(--purple);
|
||||
text-shadow: 0 0 4px currentColor;
|
||||
padding: 0.3em 1em;
|
||||
font: inherit;
|
||||
font-size: 0.85em;
|
||||
letter-spacing: 0.08em;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
box-shadow 0.15s ease,
|
||||
background 0.15s ease;
|
||||
}
|
||||
.btn-meta-update:hover:not([disabled]) {
|
||||
background: color-mix(in srgb, var(--purple) 22%, transparent);
|
||||
box-shadow: 0 0 10px -2px currentColor;
|
||||
}
|
||||
.btn-meta-update[disabled] {
|
||||
opacity: 0.35;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.meta-update-running {
|
||||
margin: 0 0 0.7em;
|
||||
padding: 0.4em 0.7em;
|
||||
border: 1px solid var(--purple);
|
||||
background: color-mix(in srgb, var(--purple) 12%, transparent);
|
||||
color: var(--purple);
|
||||
font-size: 0.85em;
|
||||
animation: badge-pulse 1.6s ease-in-out infinite;
|
||||
}
|
||||
|
||||
/* ─── rebuild queue panel ──────────────────────────────────────────────── */
|
||||
.rebuild-queue {
|
||||
list-style: none;
|
||||
|
|
|
|||
|
|
@ -27,11 +27,8 @@ import {
|
|||
import {
|
||||
initBuilds,
|
||||
onBuildsTabShown,
|
||||
onMetaInputsChanged,
|
||||
onMetaUpdateRunning,
|
||||
onRebuildQueueChanged,
|
||||
rebuildCount,
|
||||
syncBuildsFromSnapshot,
|
||||
} from "./builds.js";
|
||||
import {
|
||||
applyTombstonesChanged,
|
||||
|
|
@ -160,7 +157,7 @@ import { renderUserMenu } from "./user-menu.js";
|
|||
// initJobqRollup mounts once and is a no-op on later calls; the
|
||||
// mount's own effect handles the actual fetch.
|
||||
initJobqRollup();
|
||||
syncBuildsFromSnapshot(s);
|
||||
|
||||
syncKeptFromSnapshot(s);
|
||||
renderContainers(s);
|
||||
renderKept();
|
||||
|
|
@ -216,8 +213,7 @@ import { renderUserMenu } from "./user-menu.js";
|
|||
applyRebuildQueueChanged();
|
||||
onRebuildQueueChanged();
|
||||
},
|
||||
meta_inputs_changed: onMetaInputsChanged,
|
||||
meta_update_running: onMetaUpdateRunning,
|
||||
|
||||
tombstones_changed: applyTombstonesChanged,
|
||||
};
|
||||
(function bindDashboardStream() {
|
||||
|
|
@ -229,12 +225,12 @@ import { renderUserMenu } from "./user-menu.js";
|
|||
// fallback when SharedWorker isn't supported.
|
||||
//
|
||||
// kinds= matches MUTATION_HANDLERS verbatim, so the stream carries
|
||||
// only the 8 wire kinds this page acts on.
|
||||
// only the 6 wire kinds this page acts on.
|
||||
const es = openStream(
|
||||
"/api/dashboard/stream?kinds=" +
|
||||
"transient_set,transient_cleared," +
|
||||
"container_state_changed,container_removed,rebuild_queue_changed," +
|
||||
"meta_inputs_changed,meta_update_running,tombstones_changed",
|
||||
"tombstones_changed",
|
||||
);
|
||||
es.onmessage = (e) => {
|
||||
let ev;
|
||||
|
|
@ -266,7 +262,7 @@ import { renderUserMenu } from "./user-menu.js";
|
|||
})();
|
||||
|
||||
// ─── tab routing ───────────────────────────────────────────────────────
|
||||
// Hash-based: `#agents` / `#rebuild` / `#meta` / `#buildlogs` / `#kept`
|
||||
// Hash-based: `#agents` / `#rebuild` / `#buildlogs` / `#kept`
|
||||
// activate the matching pane on the dashboard. Empty
|
||||
// hash defaults to 4G3NTS. ST4TS is NOT a tab — it is a separate page
|
||||
// (`/stats.html`) reached from the pages menu (links-menu.js). Tab routing
|
||||
|
|
|
|||
|
|
@ -20,8 +20,8 @@ export function paintAtomic(liveRoot, build) {
|
|||
}
|
||||
|
||||
// Epoch seconds from an API timestamp. The hive-c0re dashboard API
|
||||
// ships timestamps as RFC 3339 strings; a few feeds (rebuild queue,
|
||||
// meta inputs, turn stats) still carry unix-second numbers, so
|
||||
// ships timestamps as RFC 3339 strings; a few feeds (rebuild queue and
|
||||
// turn stats) still carry unix-second numbers, so
|
||||
// numbers pass through unchanged.
|
||||
export function epochSec(ts) {
|
||||
return typeof ts === "number" ? ts : Math.floor(Date.parse(ts) / 1000);
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ export function asyncBtn(btn, fn) {
|
|||
}
|
||||
|
||||
// Page-level submit interceptor for `data-async` forms — the pattern every
|
||||
// dashboard/agent action button uses (meta-update, spawn, cancel, purge,
|
||||
// dashboard/agent action button uses (spawn, cancel, purge,
|
||||
// rebuild, …). Without this a `data-async` form POSTs natively and the
|
||||
// browser navigates to the bare `ok` response page. Each page that renders
|
||||
// such forms must call this once at boot. `onSuccess` runs after a
|
||||
|
|
|
|||
Loading…
Reference in a new issue