diff --git a/frontend/packages/dashboard/src/swarm.js b/frontend/packages/dashboard/src/swarm.js index 27c2a4a9..6f48ee6b 100644 --- a/frontend/packages/dashboard/src/swarm.js +++ b/frontend/packages/dashboard/src/swarm.js @@ -244,7 +244,7 @@ function buildAgentMenu(c, forgeBase) { danger: true, confirmLabel: opts.confirmLabel || 'confirm', checkboxes: opts.graceful - ? [{ name: 'graceful', label: 'stop gracefully — let the agent finish its turn and flush state before the container stops' }] + ? [{ name: 'graceful', label: opts.gracefulLabel || 'stop gracefully — let the agent finish its turn and flush state before the container stops' }] : [], }); if (!r) return; @@ -277,7 +277,12 @@ function buildAgentMenu(c, forgeBase) { // Show only actions that are applicable in the current state. if (c.running) { dropdown.append( - menuItem('↺ R3ST4RT', { action: '/api/restart/', confirm: `restart ${c.name}?` }), + menuItem('↺ R3ST4RT', { + action: '/api/restart/', + confirm: `restart ${c.name}?`, + graceful: true, + gracefulLabel: 'restart gracefully — let the agent finish its turn and flush state before the container restarts', + }), menuItem('■ ST0P', { action: '/api/kill/', confirm: `stop ${c.name}?`, confirmLabel: '■ stop', graceful: true }), ); } else { @@ -929,6 +934,8 @@ export function renderSelectionBar(containers) { addBulkButton(actions, 'btn-restart', '↺ R3ST4RT', allRunning, selected, { action: '/api/restart/', confirm: (names) => `restart ${names.length} agent${names.length === 1 ? '' : 's'} (${names.join(', ')})?`, + graceful: true, + gracefulLabel: 'restart gracefully — let each agent finish its turn and flush state before the container restarts', disabledTitle: why('↺ R3ST4RT', stoppedNames.map((n) => `\`${n}\` is stopped`)), }); addBulkButton(actions, 'btn-stop', '■ ST0P', allRunning, selected, { @@ -1106,7 +1113,7 @@ function addBulkButton(parent, btnClass, label, enabled, selected, opts) { danger: true, confirmLabel: opts.confirmLabel || 'confirm', checkboxes: opts.graceful - ? [{ name: 'graceful', label: 'stop gracefully — let each agent finish its turn and flush state before the container stops' }] + ? [{ name: 'graceful', label: opts.gracefulLabel || 'stop gracefully — let each agent finish its turn and flush state before the container stops' }] : [], }); if (!r) return; diff --git a/hive-c0re/src/dashboard/lifecycle_ops.rs b/hive-c0re/src/dashboard/lifecycle_ops.rs index 8ae3c24c..7960fe49 100644 --- a/hive-c0re/src/dashboard/lifecycle_ops.rs +++ b/hive-c0re/src/dashboard/lifecycle_ops.rs @@ -15,11 +15,12 @@ use axum::{ }; use serde::Deserialize; -/// Query params for `post_kill`. `?graceful=1` routes to the graceful-stop -/// orchestration (quiesce the harness, flush `/state`, then container stop) -/// instead of an immediate hard stop. Defaults false → today's hard kill. +/// Query params for `post_kill` / `post_restart`. `?graceful=1` routes to +/// the graceful-stop/-restart orchestration (quiesce the harness, flush +/// `/state`, then container stop/restart) instead of an immediate hard +/// action. Defaults false → today's hard kill/restart. #[derive(Deserialize)] -pub(super) struct KillParams { +pub(super) struct GracefulParams { #[serde(default)] graceful: bool, } @@ -48,7 +49,7 @@ pub(super) async fn post_rebuild( pub(super) async fn post_kill( State(state): State, AxumPath(name): AxumPath, - Query(params): Query, + Query(params): Query, ) -> Response { let logical = strip_container_prefix(&name); if let Some(reject) = guard_agent_name(&state, &logical).await { @@ -92,11 +93,22 @@ pub(super) async fn post_kill( pub(super) async fn post_restart( State(state): State, AxumPath(name): AxumPath, + Query(params): Query, ) -> Response { let logical = strip_container_prefix(&name); if let Some(reject) = guard_agent_name(&state, &logical).await { return reject; } + if params.graceful { + submit::graceful_restart( + &state.coord, + &logical, + Source::Manual, + "manual via dashboard graceful restart".to_owned(), + ) + .await; + return (StatusCode::OK, "ok").into_response(); + } submit::restart( &state.coord, &logical,