From 2966f682ce7649d2e37129451ecc48e12c66cbd4 Mon Sep 17 00:00:00 2001 From: damocles Date: Sun, 21 Jun 2026 13:05:36 +0200 Subject: [PATCH] hive-c0re: route dashboard start/stop through the rebuild queue --- hive-c0re/src/dashboard/lifecycle_ops.rs | 88 +++++++----------------- hive-c0re/src/rebuild_queue.rs | 49 +++++++++++++ 2 files changed, 72 insertions(+), 65 deletions(-) diff --git a/hive-c0re/src/dashboard/lifecycle_ops.rs b/hive-c0re/src/dashboard/lifecycle_ops.rs index f55cad61..e11aa503 100644 --- a/hive-c0re/src/dashboard/lifecycle_ops.rs +++ b/hive-c0re/src/dashboard/lifecycle_ops.rs @@ -1,9 +1,10 @@ //! Container lifecycle endpoints for the dashboard. //! -//! Rebuild / restart / update-all enqueue onto the rebuild queue; kill / -//! start run the lifecycle op directly through `lifecycle_action` (which -//! marks the container transient for the duration so the dashboard can -//! spinner); destroy delegates to `actions::destroy` (optionally purging). +//! Rebuild / restart / start / stop (hard + graceful) / update-all all +//! enqueue onto the rebuild queue, so each shows a visible queued→running +//! transient on the dashboard — a direct sub-second start/stop only flashed +//! the badge; destroy delegates to `actions::destroy` (optionally +//! purging). use axum::{ extract::{Form, Path as AxumPath, Query, State}, @@ -43,44 +44,6 @@ pub(super) async fn post_rebuild( (StatusCode::OK, "ok").into_response() } -/// Common shape for the simple lifecycle action handlers (start / -/// stop / restart / rebuild): strip the container prefix, mark -/// transient for the duration so the dashboard can spinner, run the -/// lifecycle op, clear transient, redirect on success or surface the -/// error. `verb` only appears in the error message; `extra` runs on -/// success after `clear_transient` for handlers that need follow-up -/// (e.g. `kill` also unregisters the agent + fires `HelperEvent`). -async fn lifecycle_action( - state: &AppState, - name: &str, - kind: crate::coordinator::TransientKind, - verb: &str, - body: F, - extra: impl FnOnce(&AppState, &str), -) -> Response -where - F: FnOnce(String) -> Fut, - Fut: std::future::Future>, -{ - let logical = strip_container_prefix(name); - let guard = state.coord.transient_guard(&logical, kind); - let result = body(logical.clone()).await; - drop(guard); - match result { - Ok(()) => { - extra(state, &logical); - // Rescan so the running/needs_login/needs_update flip on - // the affected row lands on every dashboard's SSE channel - // without waiting for a snapshot poll. 200 + matching - // `data-no-refresh` on the form skip the post-submit - // /api/state refetch. - state.coord.rescan_containers_and_emit().await; - (StatusCode::OK, "ok").into_response() - } - Err(e) => error_response(&format!("{verb} {logical} failed: {e:#}")), - } -} - pub(super) async fn post_kill( State(state): State, AxumPath(name): AxumPath, @@ -116,20 +79,15 @@ pub(super) async fn post_kill( // `manager_server.rs::ManagerRequest::Kill` stays in place: a // manager calling Kill on its own container is self-suicide // mid-call, not a legitimate operator action. - lifecycle_action( - &state, - &name, - crate::coordinator::TransientKind::Stopping, - "kill", - |n| async move { lifecycle::kill(&n).await }, - |s, n| { - s.coord.unregister_agent(n); - s.coord.notify_manager(&hive_sh4re::HelperEvent::Killed { - agent: n.to_owned(), - }); - }, - ) - .await + state.coord.rebuild_queue.enqueue( + crate::rebuild_queue::QueueKind::Stop, + logical, + crate::rebuild_queue::QueueSource::Manual, + "manual via dashboard stop".to_owned(), + None, + ); + state.coord.emit_rebuild_queue_snapshot(); + (StatusCode::OK, "ok").into_response() } pub(super) async fn post_restart( @@ -159,15 +117,15 @@ pub(super) async fn post_start( if let Some(reject) = guard_agent_name(&state, &logical).await { return reject; } - lifecycle_action( - &state, - &name, - crate::coordinator::TransientKind::Starting, - "start", - |n| async move { lifecycle::start(&n).await }, - |s, n| s.coord.kick_agent(n, "container started"), - ) - .await + state.coord.rebuild_queue.enqueue( + crate::rebuild_queue::QueueKind::Start, + logical, + crate::rebuild_queue::QueueSource::Manual, + "manual via dashboard start".to_owned(), + None, + ); + state.coord.emit_rebuild_queue_snapshot(); + (StatusCode::OK, "ok").into_response() } pub(super) async fn post_update_all(State(state): State) -> Response { diff --git a/hive-c0re/src/rebuild_queue.rs b/hive-c0re/src/rebuild_queue.rs index 1c78aa1b..5e1c6930 100644 --- a/hive-c0re/src/rebuild_queue.rs +++ b/hive-c0re/src/rebuild_queue.rs @@ -46,6 +46,15 @@ pub enum QueueKind { /// stop-checkpoint turn (flush durable `/state`), wait for it to drain, /// then `nixos-container stop`. Falls back to a hard stop on timeout. GracefulStop, + /// Start a stopped container (`lifecycle::start`). Routed through the + /// queue so the dashboard shows a visible queued→running transient — a + /// direct sub-second start only flashes the badge — and bulk starts + /// serialise legibly on the queue. Fast op. + Start, + /// Hard-stop a container (`lifecycle::kill`), no quiesce. Routed through + /// the queue for the same visible-progress reason as `Start`; the + /// quiescing variant is `GracefulStop`. Fast op. + Stop, } impl QueueKind { @@ -59,6 +68,8 @@ impl QueueKind { QueueKind::Restart => "restart", QueueKind::PermChange => "perm_change", QueueKind::GracefulStop => "graceful_stop", + QueueKind::Start => "start", + QueueKind::Stop => "stop", } } } @@ -845,9 +856,47 @@ async fn dispatch( crate::auto_update::rebuild_agent(coord, name, ¤t_rev, Some(entry.id)).await } (QueueKind::GracefulStop, _) => run_graceful_stop(coord, entry).await, + (QueueKind::Start, _) => run_start(coord, entry).await, + (QueueKind::Stop, _) => run_stop(coord, entry).await, } } +/// Start a stopped container off the queue (`QueueKind::Start`), with a +/// `Starting` transient so the dashboard shows a visible queued→running +/// progression rather than the sub-second flash of a direct start. +async fn run_start( + coord: &std::sync::Arc, + entry: &QueueEntry, +) -> anyhow::Result<()> { + let name = &entry.agent; + let _guard = coord.transient_guard(name, crate::coordinator::TransientKind::Starting); + coord.set_queue_step(Some(entry.id), "nixos-container start"); + crate::lifecycle::start(name).await?; + coord.kick_agent(name, "container started"); + coord.rescan_containers_and_emit().await; + Ok(()) +} + +/// Hard-stop a container off the queue (`QueueKind::Stop`) — same teardown as +/// a direct kill (unregister + `Killed` event), but with a `Stopping` +/// transient for visible queue progress. The quiescing variant is +/// `run_graceful_stop`. +async fn run_stop( + coord: &std::sync::Arc, + entry: &QueueEntry, +) -> anyhow::Result<()> { + let name = &entry.agent; + let _guard = coord.transient_guard(name, crate::coordinator::TransientKind::Stopping); + coord.set_queue_step(Some(entry.id), "nixos-container stop"); + crate::lifecycle::kill(name).await?; + coord.unregister_agent(name); + coord.notify_manager(&hive_sh4re::HelperEvent::Killed { + agent: name.clone(), + }); + coord.rescan_containers_and_emit().await; + Ok(()) +} + /// Run one `GracefulStop` entry: signal the harness to quiesce (it returns /// `GracefulStop` on its next `Recv`, runs one stop-checkpoint turn to flush /// durable `/state`, then exits), wait for it to drain — bounded by