Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6f0b176778 | ||
|
|
029ae69953 |
1 changed files with 119 additions and 15 deletions
|
|
@ -2442,6 +2442,15 @@ window.marked = marked;
|
|||
}
|
||||
|
||||
// ─── rebuild queue ──────────────────────────────────────────────────────
|
||||
// Keyed row cache for the rebuild-queue list. Maps entry.id → { el, fingerprint }.
|
||||
// Same pattern as containerRowCache: reuse <li> nodes whose state hasn't
|
||||
// changed rather than replacing the entire list on every snapshot event.
|
||||
// The elapsed-time ticker (data-rqe-elapsed + 1s setInterval below) already
|
||||
// updates running-entry timestamps in-place, so started_at doesn't need to
|
||||
// invalidate — including it in the fingerprint only matters for the initial
|
||||
// render of a newly-running entry.
|
||||
const rebuildQueueRowCache = new Map();
|
||||
|
||||
// Glyph + verb per QueueKind. Mirrors the labels used in
|
||||
// hive-c0re::rebuild_queue::QueueKind::as_str.
|
||||
const QUEUE_KIND_GLYPH = {
|
||||
|
|
@ -2461,15 +2470,38 @@ window.marked = marked;
|
|||
cancelled: '⊘',
|
||||
};
|
||||
|
||||
// Fingerprint for a single rebuild-queue row. Everything visible in the
|
||||
// row except the ticking elapsed seconds (handled by data-rqe-elapsed
|
||||
// ticker, not by re-rendering).
|
||||
function rebuildQueueEntryFingerprint(entry, isChild) {
|
||||
return JSON.stringify({
|
||||
state: entry.state,
|
||||
step: entry.step,
|
||||
kind: entry.kind,
|
||||
agent: entry.agent,
|
||||
source: entry.source,
|
||||
started_at: entry.started_at,
|
||||
enqueued_at: entry.enqueued_at,
|
||||
finished_at: entry.finished_at,
|
||||
reason: entry.reason,
|
||||
error: entry.error,
|
||||
build_log_id: entry.build_log_id,
|
||||
isChild,
|
||||
});
|
||||
}
|
||||
|
||||
function renderRebuildQueue(s) {
|
||||
const root = $('rebuild-queue-section');
|
||||
if (!root) return;
|
||||
root.replaceChildren();
|
||||
const queue = s.rebuild_queue || [];
|
||||
|
||||
if (!queue.length) {
|
||||
root.append(el('p', { class: 'empty' }, 'queue is empty — nothing pending or in flight.'));
|
||||
// Queue drained — show placeholder and purge cache.
|
||||
rebuildQueueRowCache.clear();
|
||||
root.replaceChildren(el('p', { class: 'empty' }, 'queue is empty — nothing pending or in flight.'));
|
||||
return;
|
||||
}
|
||||
|
||||
// Index by id for parent lookup.
|
||||
const byId = new Map(queue.map((e) => [e.id, e]));
|
||||
// Top-level entries first; children render nested under their parent.
|
||||
|
|
@ -2481,21 +2513,58 @@ window.marked = marked;
|
|||
childrenOf.get(e.parent_id).push(e);
|
||||
}
|
||||
}
|
||||
const ul = el('ul', { class: 'rebuild-queue' });
|
||||
for (const top of tops) {
|
||||
ul.append(renderQueueEntry(top, byId));
|
||||
for (const child of childrenOf.get(top.id) || []) {
|
||||
ul.append(renderQueueEntry(child, byId, true));
|
||||
}
|
||||
}
|
||||
// Children whose parent isn't in the snapshot (history-evicted) still render flat.
|
||||
const orphans = queue.filter(
|
||||
(e) => e.parent_id != null && !byId.has(e.parent_id),
|
||||
);
|
||||
for (const o of orphans) {
|
||||
ul.append(renderQueueEntry(o, byId, true));
|
||||
|
||||
// Build ordered list of <li>, reusing cached nodes for unchanged entries.
|
||||
const orderedLis = [];
|
||||
function addEntry(entry, isChild) {
|
||||
const fp = rebuildQueueEntryFingerprint(entry, isChild);
|
||||
const cached = rebuildQueueRowCache.get(entry.id);
|
||||
let li;
|
||||
if (cached && cached.fingerprint === fp) {
|
||||
li = cached.el;
|
||||
} else {
|
||||
li = renderQueueEntry(entry, byId, isChild);
|
||||
rebuildQueueRowCache.set(entry.id, { el: li, fingerprint: fp });
|
||||
}
|
||||
orderedLis.push(li);
|
||||
}
|
||||
root.append(ul);
|
||||
for (const top of tops) {
|
||||
addEntry(top, false);
|
||||
for (const child of childrenOf.get(top.id) || []) {
|
||||
addEntry(child, true);
|
||||
}
|
||||
}
|
||||
for (const o of orphans) {
|
||||
addEntry(o, true);
|
||||
}
|
||||
|
||||
// Drop cache entries for IDs no longer in the snapshot.
|
||||
const liveIds = new Set(queue.map((e) => e.id));
|
||||
for (const [id, entry] of rebuildQueueRowCache) {
|
||||
if (!liveIds.has(id)) {
|
||||
entry.el.remove();
|
||||
rebuildQueueRowCache.delete(id);
|
||||
}
|
||||
}
|
||||
|
||||
// Get or create the <ul>; remove any "empty" placeholder if present.
|
||||
let ul = root.querySelector('ul.rebuild-queue');
|
||||
if (!ul) {
|
||||
ul = el('ul', { class: 'rebuild-queue' });
|
||||
root.replaceChildren(ul);
|
||||
}
|
||||
|
||||
// Reconcile DOM order without a wipe.
|
||||
for (let i = 0; i < orderedLis.length; i++) {
|
||||
if (ul.children[i] !== orderedLis[i]) {
|
||||
ul.insertBefore(orderedLis[i], ul.children[i] ?? null);
|
||||
}
|
||||
}
|
||||
while (ul.children.length > orderedLis.length) ul.lastChild.remove();
|
||||
}
|
||||
|
||||
function renderQueueEntry(entry, _byId, isChild) {
|
||||
|
|
@ -2516,9 +2585,15 @@ window.marked = marked;
|
|||
// Source chip (manual / meta_update / auto_update / crash_recover).
|
||||
li.append(' ', el('span', { class: 'rqe-source rqe-source-' + entry.source }, entry.source));
|
||||
// Timing: queued Xs ago when pending, elapsed when running,
|
||||
// finished Xs ago for terminal.
|
||||
// finished Xs ago for terminal. Queued + terminal stamps use
|
||||
// data-rqe-enqueued / data-rqe-finished so the 30s ticker below
|
||||
// can keep them fresh — keyed rows persist across snapshots, so
|
||||
// without a ticker "queued 2m ago" would never advance.
|
||||
if (entry.state === 'queued') {
|
||||
li.append(' ', el('span', { class: 'rqe-when' }, '· queued ' + fmtAgo(entry.enqueued_at)));
|
||||
li.append(' ', el('span', {
|
||||
class: 'rqe-when',
|
||||
'data-rqe-enqueued': String(entry.enqueued_at),
|
||||
}, '· queued ' + fmtAgo(entry.enqueued_at)));
|
||||
} else if (entry.state === 'running' && entry.started_at) {
|
||||
const elapsed = Math.max(0, Math.floor(Date.now() / 1000 - entry.started_at));
|
||||
li.append(' ', el('span', {
|
||||
|
|
@ -2526,7 +2601,11 @@ window.marked = marked;
|
|||
'data-rqe-elapsed': String(entry.started_at),
|
||||
}, '· ' + fmtElapsed(elapsed)));
|
||||
} else if (entry.finished_at) {
|
||||
li.append(' ', el('span', { class: 'rqe-when' }, '· ' + entry.state + ' ' + fmtAgo(entry.finished_at)));
|
||||
li.append(' ', el('span', {
|
||||
class: 'rqe-when',
|
||||
'data-rqe-finished': String(entry.finished_at),
|
||||
'data-rqe-state': entry.state,
|
||||
}, '· ' + entry.state + ' ' + fmtAgo(entry.finished_at)));
|
||||
}
|
||||
// Reason (truncated; full text on hover).
|
||||
if (entry.reason) {
|
||||
|
|
@ -2602,14 +2681,39 @@ window.marked = marked;
|
|||
// `renderQueueEntry`; the ticker avoids a full re-render just for the
|
||||
// wall-clock update.
|
||||
setInterval(() => {
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
for (const span of document.querySelectorAll('.rqe-when[data-rqe-elapsed]')) {
|
||||
const started = parseInt(span.dataset.rqeElapsed, 10);
|
||||
if (!started) continue;
|
||||
const elapsed = Math.max(0, Math.floor(Date.now() / 1000 - started));
|
||||
const elapsed = Math.max(0, now - started);
|
||||
span.textContent = '· ' + fmtElapsed(elapsed);
|
||||
}
|
||||
for (const span of document.querySelectorAll('.build-logs-runtime[data-bl-elapsed]')) {
|
||||
const started = parseInt(span.dataset.blElapsed, 10);
|
||||
if (!started) continue;
|
||||
const elapsed = Math.max(0, now - started);
|
||||
span.textContent = fmtElapsed(elapsed);
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
// 30s ticker for queued-age and terminal-age labels. Keyed rows
|
||||
// persist across rebuild_queue_changed snapshots, so without this
|
||||
// "queued 1m ago" / "done 5m ago" labels would never advance.
|
||||
setInterval(() => {
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
for (const span of document.querySelectorAll('.rqe-when[data-rqe-enqueued]')) {
|
||||
const enqueued = parseInt(span.dataset.rqeEnqueued, 10);
|
||||
if (!enqueued) continue;
|
||||
span.textContent = '· queued ' + fmtAgo(enqueued);
|
||||
}
|
||||
for (const span of document.querySelectorAll('.rqe-when[data-rqe-finished]')) {
|
||||
const finished = parseInt(span.dataset.rqeFinished, 10);
|
||||
if (!finished) continue;
|
||||
const state = span.dataset.rqeState || '';
|
||||
span.textContent = '· ' + state + ' ' + fmtAgo(finished);
|
||||
}
|
||||
}, 30_000);
|
||||
|
||||
// ─── reminders ──────────────────────────────────────────────────────────
|
||||
// Reminders aren't part of /api/state (separate sqlite table, separate
|
||||
// mutation cadence). refreshReminders() is called from refreshState() for
|
||||
|
|
|
|||
Loading…
Reference in a new issue