dashboard: show meta-update progress in the META INPUTS panel

post_meta_update returns 200 immediately and runs the nix flake
update + agent-rebuild ripple in a background task, so the META
INPUTS panel looked idle for the whole multi-minute window (#259).

Track in-flight runs with a Coordinator atomic counter, exposed via
an RAII MetaUpdateGuard held across run_meta_update. Surface it as
the meta_update_running snapshot field plus a MetaUpdateRunning SSE
event (flipped only when the count crosses 0, so concurrent runs
flip the flag once). The panel shows a pulsing in-progress banner
and disables the update button while a run is active.
This commit is contained in:
iris 2026-05-22 21:54:28 +02:00
parent 20d2b48fe5
commit 2f1b846baf
6 changed files with 119 additions and 3 deletions

View file

@ -454,11 +454,18 @@
// avoids ordering races between a same-tick destroy + purge.
let tombstonesState = [];
let metaInputsState = [];
// True while a dashboard-triggered meta-update (flake lock bump +
// agent rebuild ripple) runs in the background. Cold-loaded from
// `s.meta_update_running`, then flipped live by the
// `meta_update_running` event. Drives the META INPUTS panel's
// disabled "updating…" state (issue #259).
let metaUpdateRunning = false;
function syncTombstonesFromSnapshot(s) {
tombstonesState = (s.tombstones || []).slice();
}
function syncMetaInputsFromSnapshot(s) {
metaInputsState = (s.meta_inputs || []).slice();
metaUpdateRunning = !!s.meta_update_running;
}
function applyTombstonesChanged(ev) {
tombstonesState = (ev.tombstones || []).slice();
@ -468,6 +475,10 @@
metaInputsState = (ev.inputs || []).slice();
renderMetaInputsFromState();
}
function applyMetaUpdateRunning(ev) {
metaUpdateRunning = !!ev.running;
renderMetaInputsFromState();
}
function renderTombstonesFromState() {
renderTombstones({ tombstones: tombstonesState });
}
@ -1482,6 +1493,11 @@
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.'));
}
const form = el('form', {
method: 'POST',
action: '/meta-update',
@ -1528,11 +1544,13 @@
type: 'submit',
class: 'btn btn-meta-update',
disabled: '',
}, '◆ UPD4TE & R3BU1LD');
}, metaUpdateRunning ? '⏳ UPD4T1NG…' : '◆ UPD4TE & R3BU1LD');
form.append(btn);
function refreshDisabled() {
const any = form.querySelectorAll('input[data-meta-input]:checked').length > 0;
if (any) btn.removeAttribute('disabled');
// Stay disabled while an update is already in flight — no
// stacking a second run on top of the rebuild ripple.
if (any && !metaUpdateRunning) btn.removeAttribute('disabled');
else btn.setAttribute('disabled', '');
}
form.addEventListener('change', refreshDisabled);
@ -1857,6 +1875,7 @@
container_removed: (ev) => { applyContainerRemoved(ev); },
tombstones_changed: (ev) => { applyTombstonesChanged(ev); },
meta_inputs_changed: (ev) => { applyMetaInputsChanged(ev); },
meta_update_running: (ev) => { applyMetaUpdateRunning(ev); },
},
// Both history backfill and live frames flow through here, so the
// inbox section ends up populated correctly on first paint and

View file

@ -446,6 +446,17 @@ code {
opacity: 0.35;
cursor: not-allowed;
}
/* In-progress banner for the META INPUTS panel: shown while a
dashboard-triggered meta-update runs in the background (issue #259). */
.meta-update-running {
margin: 0 0 0.7em;
padding: 0.4em 0.7em;
border: 1px solid var(--purple);
background: rgba(203, 166, 247, 0.12);
color: var(--purple);
font-size: 0.85em;
animation: badge-pulse 1.6s ease-in-out infinite;
}
.history-note {
margin-left: 1.8em;
margin-top: 0.2em;