dashboard: transient_set / transient_cleared mutation events + client derived state
This commit is contained in:
parent
1879b2f485
commit
7956e1c627
3 changed files with 103 additions and 7 deletions
|
|
@ -202,6 +202,43 @@
|
|||
}
|
||||
});
|
||||
|
||||
// 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
|
||||
// the elapsed-seconds badge ticks without polling.
|
||||
const transientsState = new Map();
|
||||
function syncTransientsFromSnapshot(s) {
|
||||
transientsState.clear();
|
||||
for (const t of s.transients || []) {
|
||||
// Snapshot ships `secs` (server-computed); reconstruct an
|
||||
// approximate since_unix so the live ticker keeps progressing
|
||||
// without surprising jumps when the next snapshot lands.
|
||||
const nowUnix = Math.floor(Date.now() / 1000);
|
||||
transientsState.set(t.name, {
|
||||
kind: t.kind,
|
||||
since_unix: t.since_unix ?? (nowUnix - (t.secs || 0)),
|
||||
});
|
||||
}
|
||||
}
|
||||
function applyTransientSet(ev) {
|
||||
transientsState.set(ev.name, {
|
||||
kind: ev.transient_kind,
|
||||
since_unix: ev.since_unix,
|
||||
});
|
||||
renderContainersFromState();
|
||||
}
|
||||
function applyTransientCleared(ev) {
|
||||
if (transientsState.delete(ev.name)) renderContainersFromState();
|
||||
}
|
||||
// Re-render using the last cached snapshot (containers come from
|
||||
// /api/state, transients overlay from the derived map). The snapshot
|
||||
// is stashed on window.__hyperhive_state by refreshState; on cold
|
||||
// load before the first snapshot we just skip.
|
||||
function renderContainersFromState() {
|
||||
const s = window.__hyperhive_state;
|
||||
if (s) renderContainers(s);
|
||||
}
|
||||
|
||||
// ─── state rendering ────────────────────────────────────────────────────
|
||||
function renderContainers(s) {
|
||||
const root = $('containers-section');
|
||||
|
|
@ -226,20 +263,22 @@
|
|||
));
|
||||
}
|
||||
|
||||
if (s.transients.length) {
|
||||
if (transientsState.size) {
|
||||
const ul = el('ul');
|
||||
for (const t of s.transients) {
|
||||
const nowUnix = Math.floor(Date.now() / 1000);
|
||||
for (const [name, t] of transientsState) {
|
||||
const secs = Math.max(0, nowUnix - t.since_unix);
|
||||
ul.append(el('li', {},
|
||||
el('span', { class: 'glyph spinner' }, '◐'), ' ',
|
||||
el('span', { class: 'agent' }, t.name), ' ',
|
||||
el('span', { class: 'agent' }, name), ' ',
|
||||
el('span', { class: 'role role-pending' }, t.kind + '…'), ' ',
|
||||
el('span', { class: 'meta' }, `nixos-container create + start (${t.secs}s)`),
|
||||
el('span', { class: 'meta' }, `nixos-container create + start (${secs}s)`),
|
||||
));
|
||||
}
|
||||
root.append(ul);
|
||||
}
|
||||
|
||||
if (!s.containers.length && !s.transients.length) {
|
||||
if (!s.containers.length && !transientsState.size) {
|
||||
root.append(el('p', { class: 'empty' }, 'no managed containers'));
|
||||
return;
|
||||
}
|
||||
|
|
@ -1037,6 +1076,10 @@
|
|||
// names from here instead of refetching on every keystroke).
|
||||
window.__hyperhive_state = s;
|
||||
const openDetails = snapshotOpenDetails();
|
||||
// Sync transients first so renderContainers below sees the
|
||||
// current derived map (it reads from `transientsState`, not
|
||||
// from `s.transients`).
|
||||
syncTransientsFromSnapshot(s);
|
||||
renderContainers(s);
|
||||
renderTombstones(s);
|
||||
// Sync the derived approvals + questions stores from the
|
||||
|
|
@ -1058,7 +1101,7 @@
|
|||
// refresh on operator-bound messages; this catches the rest
|
||||
// (approvals, tombstones, questions).
|
||||
const anyPending = s.containers.some((c) => c.pending);
|
||||
const next = (s.transients.length || anyPending) ? 2000 : 5000;
|
||||
const next = (transientsState.size || anyPending) ? 2000 : 5000;
|
||||
if (pollTimer) { clearTimeout(pollTimer); pollTimer = null; }
|
||||
if (next) pollTimer = setTimeout(refreshState, next);
|
||||
} catch (err) {
|
||||
|
|
@ -1114,6 +1157,8 @@
|
|||
approval_resolved: (ev) => { applyApprovalResolved(ev); },
|
||||
question_added: (ev) => { applyQuestionAdded(ev); },
|
||||
question_resolved: (ev) => { applyQuestionResolved(ev); },
|
||||
transient_set: (ev) => { applyTransientSet(ev); },
|
||||
transient_cleared: (ev) => { applyTransientCleared(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