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
commit 2f1b846baf
6 changed files with 119 additions and 3 deletions

View file

@ -72,6 +72,14 @@ pub struct Coordinator {
/// snapshot.
dashboard_events: broadcast::Sender<DashboardEvent>,
event_seq: AtomicU64,
/// Count of dashboard-triggered `meta-update` runs currently in
/// flight. `post_meta_update` returns 200 immediately and does the
/// multi-minute `nix flake update` + agent-rebuild ripple in a
/// background task, so without this the META INPUTS panel showed no
/// sign anything was happening (issue #259). Held via
/// `MetaUpdateGuard`; a count > 0 surfaces on `/api/state` as
/// `meta_update_running` and via the `MetaUpdateRunning` event.
meta_updates_active: AtomicU64,
/// Last container snapshot seen by `rescan_containers_and_emit`,
/// keyed by `ContainerView.name`. The rescan diffs a fresh
/// `container_view::build_all` against this map and emits one
@ -109,6 +117,33 @@ impl Drop for TransientGuard {
}
}
/// RAII guard for the `meta-update` in-progress flag, held for the
/// duration of a `run_meta_update` background task. Created by
/// `Coordinator::meta_update_guard`. Drop decrements the active-run
/// count; the count crossing back to 0 emits
/// `MetaUpdateRunning { running: false }`, so a concurrent pair of
/// updates only flips the dashboard flag once.
pub struct MetaUpdateGuard {
coord: Arc<Coordinator>,
}
impl Drop for MetaUpdateGuard {
fn drop(&mut self) {
if self
.coord
.meta_updates_active
.fetch_sub(1, Ordering::SeqCst)
== 1
{
self.coord
.emit_dashboard_event(DashboardEvent::MetaUpdateRunning {
seq: self.coord.next_seq(),
running: false,
});
}
}
}
#[derive(Debug, Clone, Copy)]
pub enum TransientKind {
/// `lifecycle::spawn` is running (nixos-container create + update + start).
@ -165,6 +200,7 @@ impl Coordinator {
transient: Mutex::new(HashMap::new()),
dashboard_events,
event_seq: AtomicU64::new(0),
meta_updates_active: AtomicU64::new(0),
last_containers: tokio::sync::Mutex::new(HashMap::new()),
shutdown_tx,
})
@ -218,6 +254,31 @@ impl Coordinator {
let _ = self.dashboard_events.send(event);
}
/// Mark a `meta-update` as in flight and return an RAII guard that
/// clears it on drop (including drop-via-panic). The first
/// concurrent run emits `MetaUpdateRunning { running: true }`; the
/// last one to finish emits `running: false`. The dashboard's META
/// INPUTS panel reads the flag to show a disabled "updating…"
/// state while the lock bump + rebuild ripple runs (issue #259).
pub fn meta_update_guard(self: &Arc<Self>) -> MetaUpdateGuard {
if self.meta_updates_active.fetch_add(1, Ordering::SeqCst) == 0 {
self.emit_dashboard_event(DashboardEvent::MetaUpdateRunning {
seq: self.next_seq(),
running: true,
});
}
MetaUpdateGuard {
coord: Arc::clone(self),
}
}
/// True while at least one dashboard-triggered `meta-update` is
/// running. Surfaced on `/api/state` as `meta_update_running` so a
/// client that cold-loads mid-update sees the in-progress state.
pub fn meta_update_in_progress(&self) -> bool {
self.meta_updates_active.load(Ordering::SeqCst) > 0
}
/// Emit `ApprovalAdded` immediately after the row is inserted in
/// sqlite. Caller passes the diff text it already computed (or
/// `None` for spawn approvals which carry no diff).