treefmt: apply prettier

Pure `nix fmt` output from the commit before this one — no hand edits.
203 files: 52 md, 42 tsx, 32 js, 32 css, 21 ts, 13 html, 8 json, 3 mjs.

Reproduce with `nix develop -c nix fmt` on the parent commit; the result
should be byte-identical to this tree.

None of the 13 `.prettierignore` entries appears here — verified by
intersecting the changed-file list against the ignore file, with a
control proving the intersection finds a match when one exists.
This commit is contained in:
atlas 2026-09-02 14:29:33 +02:00
commit 39b95c2ede
203 changed files with 10090 additions and 6085 deletions

View file

@ -10,14 +10,20 @@
// renderers here are direct copies from core.js / logs.js with only the
// deep-link URL and count-pill id adjusted.
import { $, fmtAgeSecs, openStream, openBuildLogStream, initServerWarnings } 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';
import {
$,
fmtAgeSecs,
openStream,
openBuildLogStream,
initServerWarnings,
} 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 ───────────────────────────────────────────────────────────
let metaInputsState = [];
@ -36,93 +42,122 @@ function syncFromSnapshot(s) {
// ─── meta inputs ─────────────────────────────────────────────────────────────
function renderMetaInputs(s) {
const root = $('meta-inputs-section');
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),
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'));
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.'));
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 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);
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' });
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,
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,
"data-meta-input": inp.name,
});
if (checkedInputs.has(inp.name)) cb.checked = true;
const label = el('label', { for: id, title: inp.name });
const label = el("label", { for: id, title: inp.name });
label.append(cb);
if (depth > 0) label.append(el('span', { class: 'meta-input-twig' }, '└ '));
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)),
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)));
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: '' });
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');
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', '');
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);
f.addEventListener("change", refreshDisabled);
function setAllChecked(val) {
for (const b of f.querySelectorAll('input[data-meta-input]')) b.checked = 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(',');
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);
}
@ -150,41 +185,50 @@ function renderMetaInputs(s) {
// tracked children out from under its diffing rather than let it
// update them minimally).
function mountRebuildQueue() {
const root = $('rebuild-queue-section');
const root = $("rebuild-queue-section");
if (!root) return;
root.replaceChildren();
renderRebuildQueue();
}
function renderRebuildQueue() {
const root = $('rebuild-queue-section');
const root = $("rebuild-queue-section");
if (!root) return;
render(h(JobqGraph, {
endpoint: '/api/jobq/graph',
cancellable: true,
refreshToken: jobqGraphToken,
onUpdate: (nodes) => {
jobqNodes = nodes || [];
renderRebuildLiveLog();
updateRebuildCount();
},
onCancel: async (id) => {
const node = jobqNodes.find((n) => n.id === id);
const label = node ? node.payload.label : 'node ' + id;
if (!(await themedConfirm({
message: `cancel ${label}? a group root cancels the whole subtree; a mid-tree node cancels just that branch.`,
danger: true, confirmLabel: '✕ cancel',
}))) return;
try {
const r = await fetch('/api/rebuild-queue/' + id + '/cancel', { method: 'POST' });
if (!r.ok) throw new Error('http ' + r.status);
// No manual refresh: cancel flips node state, which fires
// rebuild_queue_changed over SSE — the existing handler below
// already bumps jobqGraphToken and re-renders on that tick.
} catch (err) {
console.error('cancel failed', err);
}
},
}), root);
render(
h(JobqGraph, {
endpoint: "/api/jobq/graph",
cancellable: true,
refreshToken: jobqGraphToken,
onUpdate: (nodes) => {
jobqNodes = nodes || [];
renderRebuildLiveLog();
updateRebuildCount();
},
onCancel: async (id) => {
const node = jobqNodes.find((n) => n.id === id);
const label = node ? node.payload.label : "node " + id;
if (
!(await themedConfirm({
message: `cancel ${label}? a group root cancels the whole subtree; a mid-tree node cancels just that branch.`,
danger: true,
confirmLabel: "✕ cancel",
}))
)
return;
try {
const r = await fetch("/api/rebuild-queue/" + id + "/cancel", {
method: "POST",
});
if (!r.ok) throw new Error("http " + r.status);
// No manual refresh: cancel flips node state, which fires
// rebuild_queue_changed over SSE — the existing handler below
// already bumps jobqGraphToken and re-renders on that tick.
} catch (err) {
console.error("cancel failed", err);
}
},
}),
root,
);
}
// ─── running-rebuild live log ─────────────────────────────────────────────────
@ -196,13 +240,16 @@ function renderRebuildQueue() {
// {stdout, stderr}) rather than SSE — a running node's log re-fetches on the
// rebuild_queue_changed tick; a terminal node's log is static (one last fetch
// on transition, then done).
let liveLogId = null; // current node id being shown
let liveLogDone = false; // true once the node left 'Running'
let liveLogId = null; // current node id being shown
let liveLogDone = false; // true once the node left 'Running'
let liveLogCollapsed = false;
let liveLogPollTimer = null;
function clearLiveLogPoll() {
if (liveLogPollTimer) { clearInterval(liveLogPollTimer); liveLogPollTimer = null; }
if (liveLogPollTimer) {
clearInterval(liveLogPollTimer);
liveLogPollTimer = null;
}
}
// First running node with a log, in wire order (root-then-subtree per
@ -210,26 +257,39 @@ function clearLiveLogPoll() {
// Gate on build_log_id so lock/noop/store-only nodes don't open a blank
// panel.
function findLiveBuild() {
return jobqNodes.find((n) => n.state === 'Running' && n.payload.data && n.payload.data.build_log_id != null)
|| null;
return (
jobqNodes.find(
(n) =>
n.state === "Running" &&
n.payload.data &&
n.payload.data.build_log_id != null,
) || null
);
}
async function fetchAndRenderLiveLog(nodeId, pre) {
try {
const r = await fetch('/api/build-log/' + nodeId);
const r = await fetch("/api/build-log/" + nodeId);
if (!r.ok) return;
const data = await r.json();
const text = [data.stdout, data.stderr ? '--- stderr ---\n' + data.stderr : ''].filter(Boolean).join('\n');
const text = [
data.stdout,
data.stderr ? "--- stderr ---\n" + data.stderr : "",
]
.filter(Boolean)
.join("\n");
if (pre.textContent !== text) {
const atBottom = pre.scrollHeight - pre.scrollTop - pre.clientHeight < 40;
pre.textContent = text;
if (atBottom) pre.scrollTop = pre.scrollHeight;
}
} catch { /* network blip — ignore, next poll will retry */ }
} catch {
/* network blip — ignore, next poll will retry */
}
}
function renderRebuildLiveLog() {
const root = $('rebuild-live-log');
const root = $("rebuild-live-log");
if (!root) return;
const liveNode = findLiveBuild();
@ -237,22 +297,26 @@ function renderRebuildLiveLog() {
clearLiveLogPoll();
liveLogId = null;
liveLogDone = false;
if (!root.hidden) { root.hidden = true; root.replaceChildren(); }
if (!root.hidden) {
root.hidden = true;
root.replaceChildren();
}
return;
}
// Same node, already polling — just let the timer tick (or do a final
// fetch if the node just went non-running and we haven't marked done yet).
if (liveNode.id === liveLogId) {
if (!liveLogDone && liveNode.state !== 'Running') {
if (!liveLogDone && liveNode.state !== "Running") {
clearLiveLogPoll();
liveLogDone = true;
const pre = root.querySelector('.rebuild-live-log-output');
const badge = root.querySelector('.rebuild-live-log-badge');
const pre = root.querySelector(".rebuild-live-log-output");
const badge = root.querySelector(".rebuild-live-log-badge");
if (pre) fetchAndRenderLiveLog(liveNode.id, pre);
if (badge) {
const ok = liveNode.state !== 'Failed';
badge.className = 'rebuild-live-log-badge ' + (ok ? 'rll-ok' : 'rll-fail');
const ok = liveNode.state !== "Failed";
badge.className =
"rebuild-live-log-badge " + (ok ? "rll-ok" : "rll-fail");
badge.textContent = liveNode.state;
}
}
@ -266,54 +330,84 @@ function renderRebuildLiveLog() {
root.hidden = false;
root.replaceChildren();
const pre = el('pre', { class: 'rebuild-live-log-output' }, '');
const pre = el("pre", { class: "rebuild-live-log-output" }, "");
pre.hidden = liveLogCollapsed;
const badge = el('span', { class: 'rebuild-live-log-badge rll-running' }, 'live');
const toggle = el('button', {
type: 'button',
class: 'rebuild-live-log-toggle',
'aria-expanded': String(!liveLogCollapsed),
title: liveLogCollapsed ? 'expand live log' : 'collapse live log',
}, liveLogCollapsed ? '▸' : '▾');
toggle.addEventListener('click', () => {
const badge = el(
"span",
{ class: "rebuild-live-log-badge rll-running" },
"live",
);
const toggle = el(
"button",
{
type: "button",
class: "rebuild-live-log-toggle",
"aria-expanded": String(!liveLogCollapsed),
title: liveLogCollapsed ? "expand live log" : "collapse live log",
},
liveLogCollapsed ? "▸" : "▾",
);
toggle.addEventListener("click", () => {
liveLogCollapsed = !liveLogCollapsed;
pre.hidden = liveLogCollapsed;
toggle.textContent = liveLogCollapsed ? '▸' : '▾';
toggle.setAttribute('aria-expanded', String(!liveLogCollapsed));
toggle.title = liveLogCollapsed ? 'expand live log' : 'collapse live log';
toggle.textContent = liveLogCollapsed ? "▸" : "▾";
toggle.setAttribute("aria-expanded", String(!liveLogCollapsed));
toggle.title = liveLogCollapsed ? "expand live log" : "collapse live log";
});
const liveAgent = liveNode.payload.data && liveNode.payload.data.agent;
const header = el('div', { class: 'rebuild-live-log-header' },
toggle, ' ',
el('span', { class: 'rebuild-live-log-title' }, 'live build log — '),
const header = el(
"div",
{ class: "rebuild-live-log-header" },
toggle,
" ",
el("span", { class: "rebuild-live-log-title" }, "live build log — "),
// Label the specific node's agent, not the whole group's agent set.
el('code', { class: 'rqe-agent' }, liveAgent || ''),
' ', el('span', { class: 'rqe-kind' }, liveNode.payload.label),
' ', badge, ' ',
el('a', {
class: 'rebuild-live-log-raw',
href: '/api/build-log/' + liveNode.id + '/raw',
download: 'build-log-' + liveNode.id + '.txt',
}, '↓ raw'),
el("code", { class: "rqe-agent" }, liveAgent || ""),
" ",
el("span", { class: "rqe-kind" }, liveNode.payload.label),
" ",
badge,
" ",
el(
"a",
{
class: "rebuild-live-log-raw",
href: "/api/build-log/" + liveNode.id + "/raw",
download: "build-log-" + liveNode.id + ".txt",
},
"↓ raw",
),
);
root.append(header, pre);
// Start polling.
fetchAndRenderLiveLog(liveNode.id, pre);
liveLogPollTimer = setInterval(() => fetchAndRenderLiveLog(liveNode.id, pre), 2000);
liveLogPollTimer = setInterval(
() => fetchAndRenderLiveLog(liveNode.id, pre),
2000,
);
}
// ─── rebuild-queue count pill ─────────────────────────────────────────────────
function updateRebuildCount() {
const pill = $('builds-tab-count-rebuild');
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((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; }
const n = 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) ──────────────────────────────
@ -330,43 +424,63 @@ 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');
const buildList = $("build-list");
const buildRefresh = $("build-refresh");
let buildLogsLoaded = false;
let buildTabs; // set in init
function fmtTs(unixSecs) {
if (!unixSecs) return '';
if (!unixSecs) return "";
const age = Math.floor(Date.now() / 1000) - unixSecs;
return fmtAgeSecs(Math.max(0, age)) + ' ago';
return fmtAgeSecs(Math.max(0, age)) + " ago";
}
async function fetchBuild() {
if (!buildList) return;
buildList.replaceChildren();
buildList.append(el('p', { class: 'meta' }, 'loading…'));
buildList.append(el("p", { class: "meta" }, "loading…"));
try {
const resp = await fetch('/api/build-logs?limit=30');
if (!resp.ok) throw new Error('http ' + resp.status);
const resp = await fetch("/api/build-logs?limit=30");
if (!resp.ok) throw new Error("http " + resp.status);
const rows = await resp.json();
buildList.replaceChildren();
if (!rows || rows.length === 0) {
buildList.append(el('p', { class: 'meta' }, '(no build logs yet)'));
buildList.append(el("p", { class: "meta" }, "(no build logs yet)"));
return;
}
const ul = el('ul', { class: 'build-logs-list' });
const ul = el("ul", { class: "build-logs-list" });
for (const h of rows) {
const li = el('li', { class: 'build-logs-item', 'data-log-id': String(h.id) });
const li = el("li", {
class: "build-logs-item",
"data-log-id": String(h.id),
});
const live = !h.status;
const ok = h.status === 'ok';
const statusClass = live ? 'hive-pill-sm badge-running' : ok ? 'hive-pill-sm badge-ok' : 'hive-pill-sm badge-fail';
const statusLabel = live ? 'live' : ok ? 'ok' : 'fail';
const age = h.finished_at ? fmtTs(h.finished_at) : (live ? '' : fmtTs(h.started_at));
const runtime = h.runtime_secs != null
? el('span', { class: 'build-logs-runtime meta' }, fmtDuration(Math.max(0, h.runtime_secs)))
const ok = h.status === "ok";
const statusClass = live
? "hive-pill-sm badge-running"
: ok
? "hive-pill-sm badge-ok"
: "hive-pill-sm badge-fail";
const statusLabel = live ? "live" : ok ? "ok" : "fail";
const age = h.finished_at
? fmtTs(h.finished_at)
: live
? el('span', { class: 'build-logs-runtime meta build-logs-live-dur' }, '…')
: el('span', { class: 'build-logs-runtime meta' }, '');
? ""
: fmtTs(h.started_at);
const runtime =
h.runtime_secs != null
? el(
"span",
{ class: "build-logs-runtime meta" },
fmtDuration(Math.max(0, h.runtime_secs)),
)
: live
? el(
"span",
{ class: "build-logs-runtime meta build-logs-live-dur" },
"…",
)
: el("span", { class: "build-logs-runtime meta" }, "");
let durTimer = null;
if (live && h.started_at) {
@ -378,78 +492,106 @@ async function fetchBuild() {
durTimer = setInterval(updateDur, 1000);
}
const rowBtn = el('button', {
type: 'button',
class: 'build-logs-row-btn',
'aria-expanded': 'false',
},
el('span', { class: statusClass }, statusLabel),
el('span', { class: 'build-logs-agent' }, h.agent),
const rowBtn = el(
"button",
{
type: "button",
class: "build-logs-row-btn",
"aria-expanded": "false",
},
el("span", { class: statusClass }, statusLabel),
el("span", { class: "build-logs-agent" }, h.agent),
runtime,
el('span', { class: 'build-logs-kind' }, h.kind),
el('span', { class: 'build-logs-age meta' }, age),
el('span', { class: 'build-logs-cmdline meta' }, h.cmdline),
el("span", { class: "build-logs-kind" }, h.kind),
el("span", { class: "build-logs-age meta" }, age),
el("span", { class: "build-logs-cmdline meta" }, h.cmdline),
);
const detail = el('div', { class: 'build-logs-detail' });
const detail = el("div", { class: "build-logs-detail" });
detail.hidden = true;
let streamEs = null;
rowBtn.addEventListener('click', async () => {
const expanded = rowBtn.getAttribute('aria-expanded') === 'true';
rowBtn.setAttribute('aria-expanded', String(!expanded));
rowBtn.addEventListener("click", async () => {
const expanded = rowBtn.getAttribute("aria-expanded") === "true";
rowBtn.setAttribute("aria-expanded", String(!expanded));
detail.hidden = expanded;
if (expanded) {
if (streamEs) { streamEs.close(); streamEs = null; }
if (streamEs) {
streamEs.close();
streamEs = null;
}
return;
}
if (detail.dataset.loaded) return;
detail.replaceChildren();
detail.append(
el('a', {
href: '/api/build-logs/id/' + h.id + '/raw',
download: 'build-log-' + h.id + '.txt',
class: 'build-logs-dl',
}, '↓ download raw'),
el(
"a",
{
href: "/api/build-logs/id/" + h.id + "/raw",
download: "build-log-" + h.id + ".txt",
class: "build-logs-dl",
},
"↓ download raw",
),
);
if (live) {
const pre = el('pre', { class: 'build-logs-output build-logs-live' }, '');
const badge = el('span', { class: 'build-logs-live-badge hive-pill-sm badge-running' }, 'live');
const pre = el(
"pre",
{ class: "build-logs-output build-logs-live" },
"",
);
const badge = el(
"span",
{ class: "build-logs-live-badge hive-pill-sm badge-running" },
"live",
);
detail.append(badge, pre);
streamEs = openBuildLogStream(h.id, pre, {
onDone: (status) => {
if (durTimer) { clearInterval(durTimer); durTimer = null; }
if (durTimer) {
clearInterval(durTimer);
durTimer = null;
}
if (h.started_at) {
const elapsed = Math.floor(Date.now() / 1000) - h.started_at;
runtime.textContent = fmtDuration(Math.max(0, elapsed));
}
badge.className = status === 'ok' ? 'hive-pill-sm badge-ok' : 'hive-pill-sm badge-fail';
badge.className =
status === "ok"
? "hive-pill-sm badge-ok"
: "hive-pill-sm badge-fail";
badge.textContent = status;
streamEs = null;
detail.dataset.loaded = '1';
detail.dataset.loaded = "1";
},
onError: () => {
if (durTimer) { clearInterval(durTimer); durTimer = null; }
badge.textContent = 'stream error';
badge.className = 'hive-pill-sm badge-fail';
if (durTimer) {
clearInterval(durTimer);
durTimer = null;
}
badge.textContent = "stream error";
badge.className = "hive-pill-sm badge-fail";
streamEs = null;
},
});
} else {
const pre = el('pre', { class: 'build-logs-output' }, 'fetching…');
const pre = el("pre", { class: "build-logs-output" }, "fetching…");
detail.append(pre);
try {
const r2 = await fetch('/api/build-logs/id/' + h.id);
const r2 = await fetch("/api/build-logs/id/" + h.id);
if (!r2.ok) {
pre.textContent = 'error ' + r2.status;
pre.textContent = "error " + r2.status;
} else {
const full = await r2.json();
const out = [full.stdout, full.stderr].filter(Boolean).join('\n--- stderr ---\n');
pre.textContent = out || '(empty)';
const out = [full.stdout, full.stderr]
.filter(Boolean)
.join("\n--- stderr ---\n");
pre.textContent = out || "(empty)";
}
detail.dataset.loaded = '1';
detail.dataset.loaded = "1";
} catch (err) {
pre.textContent = 'fetch failed: ' + err;
pre.textContent = "fetch failed: " + err;
}
}
});
@ -458,24 +600,24 @@ async function fetchBuild() {
}
buildList.append(ul);
// Deep-link: ?id=N auto-expands the target row.
const deepId = new URLSearchParams(location.search).get('id');
const deepId = new URLSearchParams(location.search).get("id");
if (deepId) {
const target = ul.querySelector('[data-log-id="' + deepId + '"]');
if (target) {
const btn = target.querySelector('.build-logs-row-btn');
const btn = target.querySelector(".build-logs-row-btn");
if (btn) {
btn.click();
target.scrollIntoView({ behavior: 'smooth', block: 'start' });
target.scrollIntoView({ behavior: "smooth", block: "start" });
}
}
}
} catch (err) {
buildList.replaceChildren();
buildList.append(el('p', { class: 'meta' }, 'fetch failed: ' + err));
buildList.append(el("p", { class: "meta" }, "fetch failed: " + err));
}
}
if (buildRefresh) buildRefresh.addEventListener('click', fetchBuild);
if (buildRefresh) buildRefresh.addEventListener("click", fetchBuild);
// ─── SSE handlers ─────────────────────────────────────────────────────────────
let buildRefreshTimer = null;
@ -488,7 +630,7 @@ const SSE_HANDLERS = {
jobqGraphToken += 1;
renderRebuildQueue();
// Auto-refresh build log list when the queue changes and BUILD L0GS is active.
if (buildTabs && buildTabs.active() === 'buildlogs') {
if (buildTabs && buildTabs.active() === "buildlogs") {
if (buildRefreshTimer) clearTimeout(buildRefreshTimer);
buildRefreshTimer = setTimeout(fetchBuild, 2000);
}
@ -506,7 +648,7 @@ const SSE_HANDLERS = {
// ─── boot ─────────────────────────────────────────────────────────────────────
async function refreshState() {
try {
const resp = await fetch('/api/state');
const resp = await fetch("/api/state");
if (resp.ok) syncFromSnapshot(await resp.json());
} catch {
// best-effort
@ -521,15 +663,19 @@ async function init() {
// fetch needed here, unlike meta inputs below.
mountRebuildQueue();
buildTabs = document.getElementById('builds-tabbar').configure({
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' },
{
id: "rebuild",
label: "R3BU1LD QU3U3",
badgeId: "builds-tab-count-rebuild",
},
{ id: "meta", label: "M3T4 1NPUTS" },
{ id: "buildlogs", label: "BUILD L0GS" },
],
defaultId: 'rebuild',
defaultId: "rebuild",
onShow: (id) => {
if (id === 'buildlogs' && !buildLogsLoaded) {
if (id === "buildlogs" && !buildLogsLoaded) {
buildLogsLoaded = true;
fetchBuild();
}
@ -544,12 +690,16 @@ async function init() {
// (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',
"/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; }
try {
ev = JSON.parse(e.data);
} catch {
return;
}
const h = SSE_HANDLERS[ev.kind];
if (h) h(ev);
};