Watch
0
0
Fork
You've already forked hyperhive
0

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:
atlas 2026-10-25 13:33:15 +01:00
commit 4aa11b3a94
64 changed files with 1042 additions and 3793 deletions

View file

@ -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);

View file

@ -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 });
}

View file

@ -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 {

View file

@ -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

View file

@ -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;

View file

@ -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

View file

@ -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);