dashboard: the build pages are tabs of the dashboard
The build lifecycle used to be its own page with three sub-tabs. They are now dashboard tabs (rebuild, meta, buildlogs) beside SW4RM and P3RM1SS10NS: builds.js is a module tabs.js imports, so the dashboard's one tab strip, /api/state snapshot and event stream (plus the meta_inputs_changed and meta_update_running kinds) serve them, and the page, its bundle entry, stylesheet and home tile are gone. The SW4RM banner's "view queue" link points at the #rebuild tab, and the rebuild tab carries a count pill of the rebuilds in flight. An empty tab-count pill drew a "0": its display beat the UA [hidden] rule.
This commit is contained in:
parent
b5053d9a80
commit
ee5c057d8f
22 changed files with 386 additions and 456 deletions
|
|
@ -1,23 +1,17 @@
|
|||
// BU1LDS page entry (/builds.html). The build lifecycle hub — rebuild
|
||||
// queue, live build log, meta inputs, and build log history on one page.
|
||||
// Three sub-tabs via <hive-tab-strip> (default = Rebuild Queue):
|
||||
// - R3BU1LD QU3U3: pending + running rebuilds + live build log inline
|
||||
// - M3T4 1NPUTS: nix flake input selector + meta-update trigger
|
||||
// - BUILD L0GS: all-agents build log history (lazy-loaded on tab show)
|
||||
// The build lifecycle tabs of /dashboard.html — rebuild queue, live build
|
||||
// log, meta inputs, 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:
|
||||
// - 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
|
||||
|
||||
import {
|
||||
$,
|
||||
fmtAgeSecs,
|
||||
openStream,
|
||||
openBuildLogStream,
|
||||
initServerWarnings,
|
||||
} from "./common.js";
|
||||
import { $, fmtAgeSecs, openBuildLogStream } from "./common.js";
|
||||
import { el } from "@hive/shared/dom.js";
|
||||
import { bindAsyncForms } from "@hive/shared/forms.js";
|
||||
import { themedConfirm } from "@hive/shared/modal.js";
|
||||
import { h, render } from "preact";
|
||||
import { fmtAgo, fmtDuration, truncate } from "./util.js";
|
||||
import "@hive/shared/hive-tab-strip.js";
|
||||
import { JobqGraph } from "@hive/shared/jobq-graph.js";
|
||||
|
||||
// ─── derived state ───────────────────────────────────────────────────────────
|
||||
|
|
@ -174,7 +168,7 @@ function renderMetaInputs(s) {
|
|||
// the generic component's.
|
||||
// Split from `renderRebuildQueue` below: `replaceChildren()` only
|
||||
// belongs on the *first* render, to clear the static "loading…"
|
||||
// placeholder `#rebuild-queue-section` starts with in `builds.html` —
|
||||
// placeholder `#rebuild-queue-section` starts with in `dashboard.html` —
|
||||
// Preact doesn't know that placeholder is there, so a later render
|
||||
// call must NOT repeat `replaceChildren()` (it would wipe Preact's own
|
||||
// tracked children out from under its diffing rather than let it
|
||||
|
|
@ -196,7 +190,6 @@ function renderRebuildQueue() {
|
|||
onUpdate: (nodes) => {
|
||||
jobqNodes = nodes || [];
|
||||
renderRebuildLiveLog();
|
||||
updateRebuildCount();
|
||||
},
|
||||
onCancel: async (id) => {
|
||||
const node = jobqNodes.find((n) => n.id === id);
|
||||
|
|
@ -384,33 +377,18 @@ function renderRebuildLiveLog() {
|
|||
}
|
||||
|
||||
// ─── rebuild-queue count pill ─────────────────────────────────────────────────
|
||||
function updateRebuildCount() {
|
||||
const pill = $("builds-tab-count-rebuild");
|
||||
if (!pill) return;
|
||||
// Pending/Running/Finishing = in flight (Finishing = own work done, a
|
||||
// sub-node still running — still counts). Root nodes only: each is one
|
||||
// queue entry.
|
||||
const n = jobqNodes.filter(
|
||||
// The queue's in-flight count, for the R3BU1LD QU3U3 tab's pill (tabs.js
|
||||
// reads it on its refresh tick). Pending/Running/Finishing = in flight
|
||||
// (Finishing = own work done, a sub-node still running — still counts).
|
||||
// Root nodes only: each is one queue entry.
|
||||
export function rebuildCount() {
|
||||
return jobqNodes.filter(
|
||||
(n) =>
|
||||
n.parent == null &&
|
||||
(n.state === "Pending" ||
|
||||
n.state === "Running" ||
|
||||
n.state === "Finishing"),
|
||||
).length;
|
||||
if (n > 0) {
|
||||
pill.textContent = String(n);
|
||||
pill.hidden = false;
|
||||
} else {
|
||||
pill.hidden = true;
|
||||
}
|
||||
}
|
||||
|
||||
// ─── render-all (cold load + any full re-render) ──────────────────────────────
|
||||
function renderAll() {
|
||||
renderMetaInputs({ meta_inputs: metaInputsState });
|
||||
// Rebuild-queue rendering + the count-pill/live-log it drives all happen
|
||||
// off JobqGraph's own onUpdate callback (see mountRebuildQueue) —
|
||||
// nothing to render here directly.
|
||||
}
|
||||
|
||||
// ─── BUILD L0GS tab ───────────────────────────────────────────────────────────
|
||||
|
|
@ -419,10 +397,7 @@ function renderAll() {
|
|||
// 2s) when rebuild_queue_changed fires while the BUILD L0GS tab is active.
|
||||
// Deep-link: ?id=N#buildlogs auto-expands the target row.
|
||||
|
||||
const buildList = $("build-list");
|
||||
const buildRefresh = $("build-refresh");
|
||||
let buildLogsLoaded = false;
|
||||
let buildTabs; // set in init
|
||||
|
||||
function fmtTs(unixSecs) {
|
||||
if (!unixSecs) return "";
|
||||
|
|
@ -431,6 +406,7 @@ function fmtTs(unixSecs) {
|
|||
}
|
||||
|
||||
async function fetchBuild() {
|
||||
const buildList = $("build-list");
|
||||
if (!buildList) return;
|
||||
buildList.replaceChildren();
|
||||
buildList.append(el("p", { class: "meta" }, "loading…"));
|
||||
|
|
@ -612,93 +588,54 @@ async function fetchBuild() {
|
|||
}
|
||||
}
|
||||
|
||||
if (buildRefresh) buildRefresh.addEventListener("click", fetchBuild);
|
||||
// ─── entry points for tabs.js ─────────────────────────────────────────────────
|
||||
// Everything below is called by tabs.js, which owns the tab strip, the
|
||||
// `/api/state` snapshot and the dashboard event stream.
|
||||
|
||||
// ─── SSE handlers ─────────────────────────────────────────────────────────────
|
||||
let buildRefreshTimer = null;
|
||||
const SSE_HANDLERS = {
|
||||
rebuild_queue_changed() {
|
||||
// No `ev.queue` payload read anymore — this event still carries its own
|
||||
// queue snapshot on the wire (tabs.js/SW4RM still consumes it for the
|
||||
// swarm badges, untouched by this), this page just treats it as a
|
||||
// refetch trigger. `JobqGraph` owns the actual fetch.
|
||||
jobqGraphToken += 1;
|
||||
renderRebuildQueue();
|
||||
// Auto-refresh build log list when the queue changes and BUILD L0GS is active.
|
||||
if (buildTabs && buildTabs.active() === "buildlogs") {
|
||||
if (buildRefreshTimer) clearTimeout(buildRefreshTimer);
|
||||
buildRefreshTimer = setTimeout(fetchBuild, 2000);
|
||||
}
|
||||
},
|
||||
meta_inputs_changed(ev) {
|
||||
metaInputsState = (ev.inputs || []).slice();
|
||||
renderMetaInputs({ meta_inputs: metaInputsState });
|
||||
},
|
||||
meta_update_running(ev) {
|
||||
metaUpdateRunning = !!ev.running;
|
||||
renderMetaInputs({ meta_inputs: metaInputsState });
|
||||
},
|
||||
};
|
||||
|
||||
// ─── boot ─────────────────────────────────────────────────────────────────────
|
||||
async function refreshState() {
|
||||
try {
|
||||
const resp = await fetch("/api/state");
|
||||
if (resp.ok) syncFromSnapshot(await resp.json());
|
||||
} catch {
|
||||
// best-effort
|
||||
}
|
||||
renderAll();
|
||||
}
|
||||
|
||||
async function init() {
|
||||
initServerWarnings();
|
||||
bindAsyncForms(() => refreshState());
|
||||
// Self-fetches on mount (JobqGraph's own effect) — no explicit initial
|
||||
// fetch needed here, unlike meta inputs below.
|
||||
/** 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. */
|
||||
export function initBuilds() {
|
||||
const refresh = $("build-refresh");
|
||||
if (refresh) refresh.addEventListener("click", fetchBuild);
|
||||
mountRebuildQueue();
|
||||
}
|
||||
|
||||
buildTabs = document.getElementById("builds-tabbar").configure({
|
||||
tabs: [
|
||||
{
|
||||
id: "rebuild",
|
||||
label: "R3BU1LD QU3U3",
|
||||
badgeId: "builds-tab-count-rebuild",
|
||||
},
|
||||
{ id: "meta", label: "M3T4 1NPUTS" },
|
||||
{ id: "buildlogs", label: "BUILD L0GS" },
|
||||
],
|
||||
defaultId: "rebuild",
|
||||
onShow: (id) => {
|
||||
if (id === "buildlogs" && !buildLogsLoaded) {
|
||||
buildLogsLoaded = true;
|
||||
fetchBuild();
|
||||
}
|
||||
},
|
||||
});
|
||||
/** The meta inputs, from an `/api/state` snapshot. */
|
||||
export function syncBuildsFromSnapshot(s) {
|
||||
syncFromSnapshot(s);
|
||||
renderMetaInputs({ meta_inputs: metaInputsState });
|
||||
}
|
||||
|
||||
await refreshState();
|
||||
|
||||
// kinds= matches SSE_HANDLERS below verbatim — narrows this from all
|
||||
// 13 wire kinds down to the 3 this page acts on. This page was one
|
||||
// of 4 unfiltered `/api/dashboard/stream` subscribers before this
|
||||
// (subscription discipline, part 1 of the dashboard-event-stream-
|
||||
// split issue).
|
||||
const es = openStream(
|
||||
"/api/dashboard/stream?kinds=rebuild_queue_changed,meta_inputs_changed,meta_update_running",
|
||||
);
|
||||
if (es) {
|
||||
es.onmessage = (e) => {
|
||||
let ev;
|
||||
try {
|
||||
ev = JSON.parse(e.data);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
const h = SSE_HANDLERS[ev.kind];
|
||||
if (h) h(ev);
|
||||
};
|
||||
/** A tab was shown: BUILD L0GS loads its list the first time. */
|
||||
export function onBuildsTabShown(tab) {
|
||||
if (tab === "buildlogs" && !buildLogsLoaded) {
|
||||
buildLogsLoaded = true;
|
||||
fetchBuild();
|
||||
}
|
||||
}
|
||||
|
||||
init();
|
||||
let buildRefreshTimer = null;
|
||||
|
||||
/** `rebuild_queue_changed`: a payload-less refetch trigger — `JobqGraph`
|
||||
* owns the actual fetch. */
|
||||
export function onRebuildQueueChanged() {
|
||||
jobqGraphToken += 1;
|
||||
renderRebuildQueue();
|
||||
// Auto-refresh the build log list while BUILD L0GS is the active tab.
|
||||
if (document.body.dataset.activeTab === "buildlogs") {
|
||||
if (buildRefreshTimer) clearTimeout(buildRefreshTimer);
|
||||
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 });
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue