rebuild_queue: dashboard panel + snapshot field + SSE event wireup
This commit is contained in:
parent
11db5c2a8f
commit
47d2f766c9
4 changed files with 202 additions and 0 deletions
|
|
@ -493,6 +493,22 @@
|
|||
renderMetaInputs({ meta_inputs: metaInputsState });
|
||||
}
|
||||
|
||||
// Derived rebuild queue state — cold-loaded from
|
||||
// `/api/state.rebuild_queue`, then mutated live by the
|
||||
// `rebuild_queue_changed` snapshot event. Same shape as the meta-
|
||||
// inputs panel (full snapshot per change, no diff).
|
||||
let rebuildQueueState = [];
|
||||
function syncRebuildQueueFromSnapshot(s) {
|
||||
rebuildQueueState = (s.rebuild_queue || []).slice();
|
||||
}
|
||||
function applyRebuildQueueChanged(ev) {
|
||||
rebuildQueueState = (ev.queue || []).slice();
|
||||
renderRebuildQueueFromState();
|
||||
}
|
||||
function renderRebuildQueueFromState() {
|
||||
renderRebuildQueue({ rebuild_queue: rebuildQueueState });
|
||||
}
|
||||
|
||||
// Derived transient state — cold-loaded from /api/state.transients,
|
||||
// then mutated live by `transient_set` / `transient_cleared`. Keyed
|
||||
// by agent name so add/remove are O(1). `since_unix` is wall-clock so
|
||||
|
|
@ -1653,6 +1669,119 @@
|
|||
return s.length <= n ? s : s.slice(0, n - 1) + '…';
|
||||
}
|
||||
|
||||
// ─── rebuild queue ──────────────────────────────────────────────────────
|
||||
// Glyph + verb per QueueKind. Mirrors the labels used in
|
||||
// hive-c0re::rebuild_queue::QueueKind::as_str.
|
||||
const QUEUE_KIND_GLYPH = {
|
||||
rebuild: '↻',
|
||||
meta_update: '◆',
|
||||
spawn: '✨',
|
||||
destroy: '🗑',
|
||||
};
|
||||
const QUEUE_STATE_GLYPH = {
|
||||
queued: '⏸',
|
||||
running: '▶',
|
||||
done: '✔',
|
||||
failed: '✖',
|
||||
cancelled: '⊘',
|
||||
};
|
||||
|
||||
function renderRebuildQueue(s) {
|
||||
const root = $('rebuild-queue-section');
|
||||
if (!root) return;
|
||||
root.innerHTML = '';
|
||||
const queue = s.rebuild_queue || [];
|
||||
if (!queue.length) {
|
||||
root.append(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.
|
||||
const tops = queue.filter((e) => e.parent_id == null);
|
||||
const childrenOf = new Map();
|
||||
for (const e of queue) {
|
||||
if (e.parent_id != null) {
|
||||
if (!childrenOf.has(e.parent_id)) childrenOf.set(e.parent_id, []);
|
||||
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));
|
||||
}
|
||||
root.append(ul);
|
||||
}
|
||||
|
||||
function renderQueueEntry(entry, _byId, isChild) {
|
||||
const li = el('li', {
|
||||
class: 'rebuild-queue-entry rqe-' + entry.state,
|
||||
'data-id': String(entry.id),
|
||||
});
|
||||
if (isChild) li.classList.add('rqe-child');
|
||||
// State glyph + kind + agent.
|
||||
li.append(
|
||||
el('span', { class: 'rqe-state', title: entry.state }, QUEUE_STATE_GLYPH[entry.state] || '?'),
|
||||
' ',
|
||||
el('span', { class: 'rqe-kind', title: entry.kind },
|
||||
(QUEUE_KIND_GLYPH[entry.kind] || '?') + ' ' + entry.kind),
|
||||
' ',
|
||||
el('code', { class: 'rqe-agent' }, entry.agent),
|
||||
);
|
||||
// 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.
|
||||
if (entry.state === 'queued') {
|
||||
li.append(' ', el('span', { class: 'rqe-when' }, '· 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', {
|
||||
class: 'rqe-when',
|
||||
'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)));
|
||||
}
|
||||
// Reason (truncated; full text on hover).
|
||||
if (entry.reason) {
|
||||
const r = entry.reason.split('\n')[0];
|
||||
li.append(' ', el('span', { class: 'rqe-reason', title: entry.reason }, '— ' + truncate(r, 60)));
|
||||
}
|
||||
// Error block, when failed.
|
||||
if (entry.error) {
|
||||
li.append(el('pre', { class: 'rqe-error', title: entry.error }, truncate(entry.error, 200)));
|
||||
}
|
||||
return li;
|
||||
}
|
||||
|
||||
function fmtElapsed(secs) {
|
||||
if (secs < 60) return secs + 's running';
|
||||
if (secs < 3600) return Math.floor(secs / 60) + 'm ' + (secs % 60) + 's running';
|
||||
return Math.floor(secs / 3600) + 'h ' + Math.floor((secs % 3600) / 60) + 'm running';
|
||||
}
|
||||
|
||||
// Tick once per second to refresh "running Xs" badges in place
|
||||
// (mirrors the question-TTL ticker pattern from #335).
|
||||
setInterval(() => {
|
||||
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));
|
||||
span.textContent = '· ' + fmtElapsed(elapsed);
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
// ─── reminders ──────────────────────────────────────────────────────────
|
||||
// Reminders aren't part of /api/state (separate sqlite table, separate
|
||||
// mutation cadence). Refresh fires alongside refreshState() so a
|
||||
|
|
@ -1764,6 +1893,7 @@
|
|||
'inbox-section',
|
||||
'approvals-section',
|
||||
'meta-inputs-section',
|
||||
'rebuild-queue-section',
|
||||
'reminders-section',
|
||||
];
|
||||
// <details> sections that should survive a refresh need a stable
|
||||
|
|
@ -1830,6 +1960,7 @@
|
|||
syncContainersFromSnapshot(s);
|
||||
syncTombstonesFromSnapshot(s);
|
||||
syncMetaInputsFromSnapshot(s);
|
||||
syncRebuildQueueFromSnapshot(s);
|
||||
renderContainers(s);
|
||||
renderTombstones(s);
|
||||
// Sync the derived approvals + questions stores from the
|
||||
|
|
@ -1842,6 +1973,7 @@
|
|||
syncApprovalsFromSnapshot(s);
|
||||
renderApprovals();
|
||||
renderMetaInputs(s);
|
||||
renderRebuildQueue(s);
|
||||
refreshReminders();
|
||||
restoreOpenDetails(openDetails);
|
||||
notifyDeltas(s);
|
||||
|
|
@ -1963,6 +2095,7 @@
|
|||
tombstones_changed: (ev) => { applyTombstonesChanged(ev); },
|
||||
meta_inputs_changed: (ev) => { applyMetaInputsChanged(ev); },
|
||||
meta_update_running: (ev) => { applyMetaUpdateRunning(ev); },
|
||||
rebuild_queue_changed: (ev) => { applyRebuildQueueChanged(ev); },
|
||||
},
|
||||
// Both history backfill and live frames flow through here, so the
|
||||
// inbox section ends up populated correctly on first paint and
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue