feat(#2637): rework builds.js for new DagView wire shape
- SSE live-log → 2s polling on GET /api/build-log/<node_id> ({stdout,stderr} JSON)
- DagView top-level started_at/finished_at used directly (isoToSecs); no client
min/max derivation
- rollupState/entryKind/isoToSecs derive state+kind from NodeView[]; Done nodes
absent from wire so fully-done DAGs disappear naturally
- rollupState([]) returns 'done' defensively for empty node arrays
- Log links and live-log panel gated on n.has_log (backend field, mirrors old
build_log_id != null — excludes lock/noop/store-only nodes)
- Raw download: /api/build-log/<node_id>/raw (text/plain)
This commit is contained in:
parent
29f45ddd48
commit
ecc2ebe682
1 changed files with 130 additions and 127 deletions
|
|
@ -19,6 +19,31 @@ let metaInputsState = [];
|
||||||
let metaUpdateRunning = false;
|
let metaUpdateRunning = false;
|
||||||
let rebuildQueueState = [];
|
let rebuildQueueState = [];
|
||||||
|
|
||||||
|
// ─── DAG-derived helpers ──────────────────────────────────────────────────────
|
||||||
|
// DagView no longer carries top-level `kind`/`state`/`started_at`/`finished_at`
|
||||||
|
// — these are all derived from the NodeView array by the client.
|
||||||
|
|
||||||
|
// Rollup state from nodes (failed > cancelled > running > queued).
|
||||||
|
// `Done` nodes are excluded from the payload, so a fully-done DAG is absent;
|
||||||
|
// an empty nodes array should not arise in practice — return 'done' defensively.
|
||||||
|
function rollupState(nodes) {
|
||||||
|
const ns = nodes || [];
|
||||||
|
if (!ns.length) return 'done';
|
||||||
|
if (ns.some((n) => n.state === 'failed')) return 'failed';
|
||||||
|
if (ns.some((n) => n.state === 'cancelled')) return 'cancelled';
|
||||||
|
if (ns.some((n) => n.state === 'running')) return 'running';
|
||||||
|
return 'queued';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Parse an RFC3339 datetime string (from DateTime<Utc>) to unix seconds.
|
||||||
|
function isoToSecs(s) {
|
||||||
|
if (!s) return null;
|
||||||
|
const ms = Date.parse(s);
|
||||||
|
return Number.isFinite(ms) ? Math.floor(ms / 1000) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function syncFromSnapshot(s) {
|
function syncFromSnapshot(s) {
|
||||||
metaInputsState = (s.meta_inputs || []).slice();
|
metaInputsState = (s.meta_inputs || []).slice();
|
||||||
metaUpdateRunning = !!s.meta_update_running;
|
metaUpdateRunning = !!s.meta_update_running;
|
||||||
|
|
@ -120,19 +145,6 @@ function renderMetaInputs(s) {
|
||||||
|
|
||||||
// ─── rebuild queue ────────────────────────────────────────────────────────────
|
// ─── rebuild queue ────────────────────────────────────────────────────────────
|
||||||
const rebuildQueueRowCache = new Map();
|
const rebuildQueueRowCache = new Map();
|
||||||
const QUEUE_KIND_GLYPH = {
|
|
||||||
rebuild: '↻',
|
|
||||||
meta_update: '◆',
|
|
||||||
spawn: '✨',
|
|
||||||
destroy: '🗑',
|
|
||||||
restart: '↺',
|
|
||||||
boot: '⚡',
|
|
||||||
perm_change: '🔑',
|
|
||||||
graceful_stop: '⏹',
|
|
||||||
start: '▶',
|
|
||||||
stop: '■',
|
|
||||||
reconcile: '⇄',
|
|
||||||
};
|
|
||||||
const QUEUE_STATE_GLYPH = {
|
const QUEUE_STATE_GLYPH = {
|
||||||
queued: '⏸',
|
queued: '⏸',
|
||||||
running: '▶',
|
running: '▶',
|
||||||
|
|
@ -140,30 +152,7 @@ const QUEUE_STATE_GLYPH = {
|
||||||
failed: '✖',
|
failed: '✖',
|
||||||
cancelled: '⊘',
|
cancelled: '⊘',
|
||||||
};
|
};
|
||||||
// Short display labels for the per-DAG node kinds (the primitive ops).
|
|
||||||
const NODE_KIND_LABEL = {
|
|
||||||
prebuild: 'prebuild',
|
|
||||||
stop_for_update: 'stop',
|
|
||||||
swap: 'swap',
|
|
||||||
post_swap: 'post swap',
|
|
||||||
provision: 'provision',
|
|
||||||
create: 'create',
|
|
||||||
meta_lock: 'meta lock',
|
|
||||||
reconcile: 'reconcile',
|
|
||||||
signal: 'signal',
|
|
||||||
drain: 'drain',
|
|
||||||
write_dropin: 'dropin',
|
|
||||||
write_perm_file: 'perm file',
|
|
||||||
approval_deploy: 'deploy',
|
|
||||||
set_wanted: 'set wanted',
|
|
||||||
noop: 'noop',
|
|
||||||
};
|
|
||||||
|
|
||||||
// The currently-running node of a DAG (per-node `step` labels + build
|
|
||||||
// logs live on nodes now; the DAG's `state` is a roll-up).
|
|
||||||
function runningNode(entry) {
|
|
||||||
return (entry.nodes || []).find((n) => n.state === 'running') || null;
|
|
||||||
}
|
|
||||||
function firstFailedNode(entry) {
|
function firstFailedNode(entry) {
|
||||||
return (entry.nodes || []).find((n) => n.state === 'failed') || null;
|
return (entry.nodes || []).find((n) => n.state === 'failed') || null;
|
||||||
}
|
}
|
||||||
|
|
@ -310,16 +299,16 @@ function nodeComponents(nodes) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function rebuildQueueEntryFingerprint(entry) {
|
function rebuildQueueEntryFingerprint(entry) {
|
||||||
|
const nodes = entry.nodes || [];
|
||||||
return JSON.stringify({
|
return JSON.stringify({
|
||||||
state: entry.state,
|
state: rollupState(nodes),
|
||||||
kind: entry.kind,
|
|
||||||
agent: entryAgents(entry),
|
agent: entryAgents(entry),
|
||||||
source: entry.source,
|
source: entry.source,
|
||||||
started_at: entry.started_at,
|
started_at: isoToSecs(entry.started_at),
|
||||||
enqueued_at: entry.enqueued_at,
|
created_at: entry.created_at,
|
||||||
finished_at: entry.finished_at,
|
finished_at: isoToSecs(entry.finished_at),
|
||||||
reason: entry.reason,
|
reason: entry.reason,
|
||||||
nodes: (entry.nodes || []).map((n) => [n.kind, n.state, n.step, n.build_log_id, n.error]),
|
nodes: nodes.map((n) => [n.kind, n.state, isoToSecs(n.started_at), isoToSecs(n.finished_at), n.error]),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -375,62 +364,57 @@ function renderRebuildQueue(s) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderQueueEntry(entry) {
|
function renderQueueEntry(entry) {
|
||||||
|
const nodes = entry.nodes || [];
|
||||||
|
const state = rollupState(nodes);
|
||||||
|
const startedAt = isoToSecs(entry.started_at);
|
||||||
|
const finishedAt = isoToSecs(entry.finished_at);
|
||||||
|
const createdAt = isoToSecs(entry.created_at);
|
||||||
|
|
||||||
const li = el('li', {
|
const li = el('li', {
|
||||||
class: 'rebuild-queue-entry rqe-' + entry.state,
|
class: 'rebuild-queue-entry rqe-' + state,
|
||||||
'data-id': String(entry.id),
|
'data-id': String(entry.id),
|
||||||
});
|
});
|
||||||
li.append(
|
li.append(
|
||||||
el('span', { class: 'rqe-state', title: entry.state }, QUEUE_STATE_GLYPH[entry.state] || '?'),
|
el('span', { class: 'rqe-state', title: state }, QUEUE_STATE_GLYPH[state] || '?'),
|
||||||
' ',
|
' ',
|
||||||
el('span', { class: 'rqe-kind', title: entry.kind },
|
el('span', { class: 'rqe-kind' }, entry.source),
|
||||||
(QUEUE_KIND_GLYPH[entry.kind] || '?') + ' ' + entry.kind),
|
|
||||||
' ',
|
' ',
|
||||||
el('code', { class: 'rqe-agent' }, entryAgents(entry)),
|
el('code', { class: 'rqe-agent' }, entryAgents(entry)),
|
||||||
);
|
);
|
||||||
li.append(' ', el('span', { class: 'rqe-source rqe-source-' + entry.source }, entry.source));
|
li.append(' ', el('span', { class: 'rqe-source rqe-source-' + entry.source }, entry.source));
|
||||||
if (entry.state === 'queued') {
|
if (state === 'queued') {
|
||||||
li.append(' ', el('span', {
|
li.append(' ', el('span', {
|
||||||
class: 'rqe-when',
|
class: 'rqe-when',
|
||||||
'data-rqe-enqueued': String(entry.enqueued_at),
|
'data-rqe-enqueued': String(createdAt ?? ''),
|
||||||
}, '· queued ' + fmtAgo(entry.enqueued_at)));
|
}, '· queued ' + (createdAt ? fmtAgo(createdAt) : '')));
|
||||||
} else if (entry.state === 'running' && entry.started_at) {
|
} else if (state === 'running' && startedAt) {
|
||||||
const elapsed = Math.max(0, Math.floor(Date.now() / 1000 - entry.started_at));
|
const elapsed = Math.max(0, Math.floor(Date.now() / 1000) - startedAt);
|
||||||
li.append(' ', el('span', {
|
li.append(' ', el('span', {
|
||||||
class: 'rqe-when',
|
class: 'rqe-when',
|
||||||
'data-rqe-elapsed': String(entry.started_at),
|
'data-rqe-elapsed': String(startedAt),
|
||||||
}, '· ' + fmtElapsed(elapsed)));
|
}, '· ' + fmtElapsed(elapsed)));
|
||||||
} else if (entry.finished_at) {
|
} else if (finishedAt) {
|
||||||
li.append(' ', el('span', {
|
li.append(' ', el('span', {
|
||||||
class: 'rqe-when',
|
class: 'rqe-when',
|
||||||
'data-rqe-finished': String(entry.finished_at),
|
'data-rqe-finished': String(finishedAt),
|
||||||
'data-rqe-state': entry.state,
|
'data-rqe-state': state,
|
||||||
}, '· ' + entry.state + ' ' + fmtAgo(entry.finished_at)));
|
}, '· ' + state + ' ' + fmtAgo(finishedAt)));
|
||||||
}
|
}
|
||||||
if (entry.reason) {
|
if (entry.reason) {
|
||||||
const r = entry.reason.split('\n')[0];
|
const r = entry.reason.split('\n')[0];
|
||||||
li.append(' ', el('span', { class: 'rqe-reason', title: entry.reason }, '— ' + truncate(r, 60)));
|
li.append(' ', el('span', { class: 'rqe-reason', title: entry.reason }, '— ' + truncate(r, 60)));
|
||||||
}
|
}
|
||||||
// Per-node chain: every DAG node in dependency order with its own
|
// Per-node chain: every DAG node in dependency order with its own state
|
||||||
// state glyph, live step label, and build-log link. This is the
|
// glyph and a log link. The DAG's actual shape is its `deps` graph —
|
||||||
// node-aware render that makes queue jumps / partial progress
|
// render that structure (via nodeComponents) not a heuristic grouping.
|
||||||
// visible (e.g. reconcile running while swap failed).
|
// `Done` nodes are excluded from the payload by the backend, so only
|
||||||
//
|
// live nodes appear here; `Failed` DAGs linger until the history cap.
|
||||||
// A DAG's actual shape is its `deps` graph, not an incidental property
|
|
||||||
// like `n.agent` — render *that* structure (via nodeComponents, which
|
|
||||||
// splits on `deps` and topo-sorts each piece), not a heuristic grouping.
|
|
||||||
// A DAG made of independent subgraphs (e.g. a multi-agent restart, no
|
|
||||||
// cross-agent deps) naturally comes back as multiple components and
|
|
||||||
// gets one line each; a single connected DAG (the common case) stays
|
|
||||||
// one component and renders exactly as the old one-line chain did.
|
|
||||||
const nodes = entry.nodes || [];
|
|
||||||
if (nodes.length) {
|
if (nodes.length) {
|
||||||
const components = nodeComponents(nodes);
|
const components = nodeComponents(nodes);
|
||||||
const multi = components.length > 1;
|
const multi = components.length > 1;
|
||||||
for (const compNodes of components) {
|
for (const compNodes of components) {
|
||||||
const chain = el('div', { class: 'rqe-nodes' });
|
const chain = el('div', { class: 'rqe-nodes' });
|
||||||
if (multi) {
|
if (multi) {
|
||||||
// Label from the component's own nodes (informational only — the
|
|
||||||
// split itself came from `deps`, not from agent).
|
|
||||||
const agents = [];
|
const agents = [];
|
||||||
for (const n of compNodes) {
|
for (const n of compNodes) {
|
||||||
if (n.agent && !agents.includes(n.agent)) agents.push(n.agent);
|
if (n.agent && !agents.includes(n.agent)) agents.push(n.agent);
|
||||||
|
|
@ -443,43 +427,43 @@ function renderQueueEntry(entry) {
|
||||||
class: 'rqe-node rqe-node-' + n.state,
|
class: 'rqe-node rqe-node-' + n.state,
|
||||||
title: n.kind + ' · ' + n.state + (n.error ? ' — ' + n.error : ''),
|
title: n.kind + ' · ' + n.state + (n.error ? ' — ' + n.error : ''),
|
||||||
},
|
},
|
||||||
(QUEUE_STATE_GLYPH[n.state] || '?') + ' ' + (NODE_KIND_LABEL[n.kind] || n.kind));
|
(QUEUE_STATE_GLYPH[n.state] || '?') + ' ' + n.kind);
|
||||||
chain.append(chip);
|
chain.append(chip);
|
||||||
if (n.build_log_id != null) {
|
// Log link — keyed by node id, fetched on demand from /api/build-log/<node_id>.
|
||||||
|
// `n.has_log` is set by the backend exactly when the node has a captured
|
||||||
|
// log (equiv to old `build_log_id != null`). Lock/noop/store-only nodes
|
||||||
|
// have has_log=false and never get a link.
|
||||||
|
if (n.has_log) {
|
||||||
chain.append(el('a', {
|
chain.append(el('a', {
|
||||||
class: 'rqe-log-link rqe-node-log',
|
class: 'rqe-log-link rqe-node-log',
|
||||||
href: '/builds.html?id=' + n.build_log_id + '#buildlogs',
|
href: '/api/build-log/' + n.id + '/raw',
|
||||||
target: '_blank',
|
target: '_blank',
|
||||||
title: 'view build log #' + n.build_log_id,
|
title: 'download build log for ' + n.kind + ' node',
|
||||||
}, '⎙'));
|
}, '⎙'));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
li.append(chain);
|
li.append(chain);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const running = runningNode(entry);
|
|
||||||
if (running && running.step) {
|
|
||||||
li.append(el('div', { class: 'rqe-step' }, '↳ ' + running.step));
|
|
||||||
}
|
|
||||||
const failed = firstFailedNode(entry);
|
const failed = firstFailedNode(entry);
|
||||||
if (failed && failed.error) {
|
if (failed && failed.error) {
|
||||||
li.append(el('pre', { class: 'rqe-error', title: failed.error }, truncate(failed.error, 200)));
|
li.append(el('pre', { class: 'rqe-error', title: failed.error }, truncate(failed.error, 200)));
|
||||||
}
|
}
|
||||||
if (entry.state === 'queued') {
|
if (state === 'queued') {
|
||||||
const cancelForm = el('form', {
|
const cancelForm = el('form', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
action: '/api/rebuild-queue/' + entry.id + '/cancel',
|
action: '/api/rebuild-queue/' + entry.id + '/cancel',
|
||||||
class: 'inline rqe-cancel',
|
class: 'inline rqe-cancel',
|
||||||
'data-async': '',
|
'data-async': '',
|
||||||
'data-confirm':
|
'data-confirm':
|
||||||
`cancel ${entry.kind} for \`${entryAgents(entry)}\` (queue id ${entry.id})? ` +
|
`cancel ${entry.source} for \`${entryAgents(entry)}\` (queue id ${entry.id})? ` +
|
||||||
`the row drops from the queue and never runs. running / done / failed entries can't be cancelled this way.`,
|
`the row drops from the queue and never runs. running / done / failed entries can't be cancelled this way.`,
|
||||||
});
|
});
|
||||||
cancelForm.append(el('button', {
|
cancelForm.append(el('button', {
|
||||||
type: 'submit',
|
type: 'submit',
|
||||||
class: 'rqe-cancel-btn',
|
class: 'rqe-cancel-btn',
|
||||||
title: 'cancel this queued ' + entry.kind,
|
title: 'cancel this queued ' + entry.source,
|
||||||
'aria-label': 'cancel queued ' + entry.kind + ' for ' + entryAgents(entry),
|
'aria-label': 'cancel queued ' + entry.source + ' for ' + entryAgents(entry),
|
||||||
}, '✗'));
|
}, '✗'));
|
||||||
li.append(cancelForm);
|
li.append(cancelForm);
|
||||||
}
|
}
|
||||||
|
|
@ -487,51 +471,83 @@ function renderQueueEntry(entry) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── running-rebuild live log ─────────────────────────────────────────────────
|
// ─── running-rebuild live log ─────────────────────────────────────────────────
|
||||||
// One persistent live-log panel for the first currently-building node
|
// One persistent live-log panel for the first currently-building node.
|
||||||
// (build logs are per-node now; with buildSlots = 1 at most one nix
|
// Keyed to that node's id and kept in its own container (#rebuild-live-log)
|
||||||
// build runs at a time). Keyed to that node's build_log_id and kept in
|
// so the queue's row re-render never tears down an open poll.
|
||||||
// its own container (#rebuild-live-log) so the queue's row re-render —
|
//
|
||||||
// which rebuilds rows as the build step advances — never tears down the
|
// Build logs are fetched by polling GET /api/build-log/<node_id> (returns
|
||||||
// open SSE stream.
|
// {stdout, stderr}) rather than SSE — a running node's log re-fetches on the
|
||||||
let liveLogEs = null;
|
// rebuild_queue_changed tick; a terminal node's log is static (one last fetch
|
||||||
let liveLogId = null;
|
// on transition, then done).
|
||||||
let liveLogDone = false;
|
let liveLogId = null; // current node id being shown
|
||||||
|
let liveLogDone = false; // true once the node left 'running'
|
||||||
let liveLogCollapsed = false;
|
let liveLogCollapsed = false;
|
||||||
|
let liveLogPollTimer = null;
|
||||||
|
|
||||||
function closeLiveLogStream() {
|
function clearLiveLogPoll() {
|
||||||
if (liveLogEs) { liveLogEs.close(); liveLogEs = null; }
|
if (liveLogPollTimer) { clearInterval(liveLogPollTimer); liveLogPollTimer = null; }
|
||||||
}
|
}
|
||||||
|
|
||||||
// First (dag, node) pair with a running node that opened a build log.
|
// First (entry, node) pair with a running node that has a log.
|
||||||
|
// Gate on has_log so lock/noop/store-only nodes don't open a blank panel.
|
||||||
function findLiveBuild(queue) {
|
function findLiveBuild(queue) {
|
||||||
for (const e of queue || []) {
|
for (const e of queue || []) {
|
||||||
if (e.state !== 'running') continue;
|
if (rollupState(e.nodes || []) !== 'running') continue;
|
||||||
for (const n of e.nodes || []) {
|
const node = (e.nodes || []).find((n) => n.state === 'running' && n.has_log);
|
||||||
if (n.state === 'running' && n.build_log_id != null) {
|
if (node) return { entry: e, node };
|
||||||
return { entry: e, node: n };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function fetchAndRenderLiveLog(nodeId, pre) {
|
||||||
|
try {
|
||||||
|
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');
|
||||||
|
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 */ }
|
||||||
|
}
|
||||||
|
|
||||||
function renderRebuildLiveLog(queue) {
|
function renderRebuildLiveLog(queue) {
|
||||||
const root = $('rebuild-live-log');
|
const root = $('rebuild-live-log');
|
||||||
if (!root) return;
|
if (!root) return;
|
||||||
const live = findLiveBuild(queue);
|
const live = findLiveBuild(queue);
|
||||||
|
|
||||||
if (!live) {
|
if (!live) {
|
||||||
closeLiveLogStream();
|
clearLiveLogPoll();
|
||||||
liveLogId = null;
|
liveLogId = null;
|
||||||
liveLogDone = false;
|
liveLogDone = false;
|
||||||
if (!root.hidden) { root.hidden = true; root.replaceChildren(); }
|
if (!root.hidden) { root.hidden = true; root.replaceChildren(); }
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const { entry: running, node: liveNode } = live;
|
const { entry: running, node: liveNode } = live;
|
||||||
if (liveNode.build_log_id === liveLogId && (liveLogEs || liveLogDone)) return;
|
|
||||||
|
|
||||||
closeLiveLogStream();
|
// Same node, already polling — just let the timer tick (or do a final
|
||||||
liveLogId = liveNode.build_log_id;
|
// fetch if the node just went non-running and we haven't marked done yet).
|
||||||
|
if (liveNode.id === liveLogId) {
|
||||||
|
if (!liveLogDone && liveNode.state !== 'running') {
|
||||||
|
clearLiveLogPoll();
|
||||||
|
liveLogDone = true;
|
||||||
|
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');
|
||||||
|
badge.textContent = liveNode.state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// New node — tear down old poll, rebuild the panel.
|
||||||
|
clearLiveLogPoll();
|
||||||
|
liveLogId = liveNode.id;
|
||||||
liveLogDone = false;
|
liveLogDone = false;
|
||||||
root.hidden = false;
|
root.hidden = false;
|
||||||
root.replaceChildren();
|
root.replaceChildren();
|
||||||
|
|
@ -555,35 +571,21 @@ function renderRebuildLiveLog(queue) {
|
||||||
const header = el('div', { class: 'rebuild-live-log-header' },
|
const header = el('div', { class: 'rebuild-live-log-header' },
|
||||||
toggle, ' ',
|
toggle, ' ',
|
||||||
el('span', { class: 'rebuild-live-log-title' }, 'live build log — '),
|
el('span', { class: 'rebuild-live-log-title' }, 'live build log — '),
|
||||||
// This header labels one specific node's log stream (`liveNode`), not
|
// Label the specific node's agent, not the whole DAG's agent set.
|
||||||
// the DAG as a whole — so it's the node's own agent, not the DAG's
|
|
||||||
// full agent set (which would mislabel a single agent's log with every
|
|
||||||
// agent once DAGs span multiple).
|
|
||||||
el('code', { class: 'rqe-agent' }, liveNode.agent),
|
el('code', { class: 'rqe-agent' }, liveNode.agent),
|
||||||
' ', el('span', { class: 'rqe-kind' },
|
' ', el('span', { class: 'rqe-kind' }, running.source + ' · ' + liveNode.kind),
|
||||||
(running.kind || 'rebuild') + ' · ' + (NODE_KIND_LABEL[liveNode.kind] || liveNode.kind)),
|
|
||||||
' ', badge, ' ',
|
' ', badge, ' ',
|
||||||
el('a', {
|
el('a', {
|
||||||
class: 'rebuild-live-log-raw',
|
class: 'rebuild-live-log-raw',
|
||||||
href: '/api/build-logs/id/' + liveNode.build_log_id + '/raw',
|
href: '/api/build-log/' + liveNode.id + '/raw',
|
||||||
download: 'build-log-' + liveNode.build_log_id + '.txt',
|
download: 'build-log-' + liveNode.id + '.txt',
|
||||||
}, '↓ raw'),
|
}, '↓ raw'),
|
||||||
);
|
);
|
||||||
root.append(header, pre);
|
root.append(header, pre);
|
||||||
|
|
||||||
liveLogEs = openBuildLogStream(liveNode.build_log_id, pre, {
|
// Start polling.
|
||||||
onDone: (status) => {
|
fetchAndRenderLiveLog(liveNode.id, pre);
|
||||||
liveLogDone = true;
|
liveLogPollTimer = setInterval(() => fetchAndRenderLiveLog(liveNode.id, pre), 2000);
|
||||||
badge.className = 'rebuild-live-log-badge ' + (status === 'ok' ? 'rll-ok' : 'rll-fail');
|
|
||||||
badge.textContent = status;
|
|
||||||
liveLogEs = null;
|
|
||||||
},
|
|
||||||
onError: () => {
|
|
||||||
badge.className = 'rebuild-live-log-badge rll-fail';
|
|
||||||
badge.textContent = 'stream error';
|
|
||||||
liveLogEs = null;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── rebuild-queue count pill ─────────────────────────────────────────────────
|
// ─── rebuild-queue count pill ─────────────────────────────────────────────────
|
||||||
|
|
@ -592,7 +594,8 @@ function updateRebuildCount() {
|
||||||
if (!pill) return;
|
if (!pill) return;
|
||||||
let n = 0;
|
let n = 0;
|
||||||
for (const e of rebuildQueueState) {
|
for (const e of rebuildQueueState) {
|
||||||
if (e.state === 'queued' || e.state === 'running') n++;
|
const s = rollupState(e.nodes || []);
|
||||||
|
if (s === 'queued' || s === 'running') n++;
|
||||||
}
|
}
|
||||||
if (n > 0) { pill.textContent = String(n); pill.hidden = false; }
|
if (n > 0) { pill.textContent = String(n); pill.hidden = false; }
|
||||||
else { pill.hidden = true; }
|
else { pill.hidden = true; }
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue