hive-c0re: route dashboard start/stop through the rebuild queue

This commit is contained in:
damocles 2026-06-21 13:05:36 +02:00 committed by mara
commit 2966f682ce
2 changed files with 72 additions and 65 deletions

View file

@ -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<F, Fut>(
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<Output = anyhow::Result<()>>,
{
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<AppState>,
AxumPath(name): AxumPath<String>,
@ -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<AppState>) -> Response {