dashboard: lifecycle_action helper collapses start/stop/restart/rebuild

five POST handlers (post_kill / post_restart / post_start / post_rebuild)
were all repeating the same boilerplate: strip prefix, set_transient,
call lifecycle::X, clear_transient, match the result. extract one
helper that takes the transient kind, error-message verb, the work
body, and an optional 'on success' tail (used by kill to also
unregister + emit HelperEvent::Killed). each handler shrinks to a
single lifecycle_action(..) call. zero behavior change.
This commit is contained in:
müde 2026-05-15 20:12:03 +02:00
parent 89ccc5e6c5
commit 7b4adea325
3 changed files with 76 additions and 47 deletions

View file

@ -456,15 +456,51 @@ async fn post_rebuild(State(state): State<AppState>, AxumPath(name): AxumPath<St
"rebuild: hyperhive_flake has no canonical path; manual rebuild only via `hive-c0re rebuild`",
);
};
let logical = strip_container_prefix(&name);
state
.coord
.set_transient(&logical, crate::coordinator::TransientKind::Rebuilding);
let result = crate::auto_update::rebuild_agent(&state.coord, &logical, &current_rev).await;
let coord = state.coord.clone();
lifecycle_action(
&state,
&name,
crate::coordinator::TransientKind::Rebuilding,
"rebuild",
move |n| {
let coord = coord.clone();
let rev = current_rev.clone();
async move { crate::auto_update::rebuild_agent(&coord, &n, &rev).await }
},
|_, _| {},
)
.await
}
/// 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);
state.coord.set_transient(&logical, kind);
let result = body(logical.clone()).await;
state.coord.clear_transient(&logical);
match result {
Ok(()) => Redirect::to("/").into_response(),
Err(e) => error_response(&format!("rebuild {logical} failed: {e:#}")),
Ok(()) => {
extra(state, &logical);
Redirect::to("/").into_response()
}
Err(e) => error_response(&format!("{verb} {logical} failed: {e:#}")),
}
}
@ -473,49 +509,44 @@ async fn post_kill(State(state): State<AppState>, AxumPath(name): AxumPath<Strin
if logical == lifecycle::MANAGER_NAME {
return error_response("kill: refusing to stop the manager");
}
state
.coord
.set_transient(&logical, crate::coordinator::TransientKind::Stopping);
let result = lifecycle::kill(&logical).await;
state.coord.clear_transient(&logical);
match result {
Ok(()) => {
state.coord.unregister_agent(&logical);
state
.coord
.notify_manager(&hive_sh4re::HelperEvent::Killed {
agent: logical.clone(),
});
Redirect::to("/").into_response()
}
Err(e) => error_response(&format!("kill {logical} failed: {e:#}")),
}
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
}
async fn post_restart(State(state): State<AppState>, AxumPath(name): AxumPath<String>) -> Response {
let logical = strip_container_prefix(&name);
state
.coord
.set_transient(&logical, crate::coordinator::TransientKind::Restarting);
let result = lifecycle::restart(&logical).await;
state.coord.clear_transient(&logical);
match result {
Ok(()) => Redirect::to("/").into_response(),
Err(e) => error_response(&format!("restart {logical} failed: {e:#}")),
}
lifecycle_action(
&state,
&name,
crate::coordinator::TransientKind::Restarting,
"restart",
|n| async move { lifecycle::restart(&n).await },
|_, _| {},
)
.await
}
async fn post_start(State(state): State<AppState>, AxumPath(name): AxumPath<String>) -> Response {
let logical = strip_container_prefix(&name);
state
.coord
.set_transient(&logical, crate::coordinator::TransientKind::Starting);
let result = lifecycle::start(&logical).await;
state.coord.clear_transient(&logical);
match result {
Ok(()) => Redirect::to("/").into_response(),
Err(e) => error_response(&format!("start {logical} failed: {e:#}")),
}
lifecycle_action(
&state,
&name,
crate::coordinator::TransientKind::Starting,
"start",
|n| async move { lifecycle::start(&n).await },
|_, _| {},
)
.await
}
async fn post_update_all(State(state): State<AppState>) -> Response {