From b61a907bac09a7649ca3f6d850e771b3b75c7015 Mon Sep 17 00:00:00 2001 From: soryu Date: Thu, 22 Jan 2026 01:26:53 +0000 Subject: Add daemon restart feature from settings (#18) * Add daemon restart feature from settings This adds the ability to restart a connected daemon from the settings page. The feature includes: - Backend: RestartDaemon command added to DaemonCommand enum - Backend: New POST /api/v1/mesh/daemons/{id}/restart endpoint - Backend: Daemon gracefully shuts down tasks and exits with code 42 (can be used by process managers like systemd to detect restart requests) - Frontend: restartDaemon() API function - Frontend: Restart button in Connected Daemons section of settings - Frontend: Confirmation dialog before restart to prevent accidental restarts When a daemon receives the restart command, it: 1. Gracefully shuts down all running Claude processes (5s timeout) 2. Exits with code 42 to signal restart requested 3. The daemon can be restarted by a process manager or manually Co-Authored-By: Claude Opus 4.5 --- makima/src/server/handlers/mesh.rs | 109 +++++++++++++++++++++++++++++++++++++ 1 file changed, 109 insertions(+) (limited to 'makima/src/server/handlers') diff --git a/makima/src/server/handlers/mesh.rs b/makima/src/server/handlers/mesh.rs index 99c3d9d..53e1587 100644 --- a/makima/src/server/handlers/mesh.rs +++ b/makima/src/server/handlers/mesh.rs @@ -3561,3 +3561,112 @@ pub async fn branch_task( ) .into_response() } + +// ============================================================================= +// Daemon Management +// ============================================================================= + +/// Response for restart daemon request. +#[derive(Debug, serde::Serialize, utoipa::ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct RestartDaemonResponse { + /// Whether the restart command was sent successfully. + pub success: bool, + /// The daemon ID that received the restart command. + pub daemon_id: Uuid, + /// Message describing the result. + pub message: String, +} + +/// Restart a daemon by ID (requires authentication). +/// +/// Sends a restart command to the specified daemon, which will cause it to +/// gracefully terminate and restart. Any running tasks will be interrupted. +#[utoipa::path( + post, + path = "/api/v1/mesh/daemons/{id}/restart", + params( + ("id" = Uuid, Path, description = "Daemon ID") + ), + responses( + (status = 200, description = "Restart command sent", body = RestartDaemonResponse), + (status = 401, description = "Unauthorized", body = ApiError), + (status = 404, description = "Daemon not found or not connected", body = ApiError), + (status = 503, description = "Database not configured", body = ApiError), + (status = 500, description = "Internal server error", body = ApiError), + ), + security( + ("bearer_auth" = []), + ("api_key" = []) + ), + tag = "Mesh" +)] +pub async fn restart_daemon( + State(state): State, + Authenticated(auth): Authenticated, + Path(id): Path, +) -> impl IntoResponse { + let Some(ref pool) = state.db_pool else { + return ( + StatusCode::SERVICE_UNAVAILABLE, + Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), + ) + .into_response(); + }; + + // Verify the daemon exists and belongs to this owner + match repository::get_daemon_for_owner(pool, id, auth.owner_id).await { + Ok(Some(_)) => {} + Ok(None) => { + return ( + StatusCode::NOT_FOUND, + Json(ApiError::new("NOT_FOUND", "Daemon not found")), + ) + .into_response(); + } + Err(e) => { + tracing::error!("Failed to get daemon {}: {}", id, e); + return ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(ApiError::new("DB_ERROR", e.to_string())), + ) + .into_response(); + } + } + + // Check if daemon is connected + if !state.is_daemon_connected(id) { + return ( + StatusCode::NOT_FOUND, + Json(ApiError::new( + "DAEMON_NOT_CONNECTED", + "Daemon is not currently connected", + )), + ) + .into_response(); + } + + // Send restart command to daemon + let command = DaemonCommand::RestartDaemon; + if let Err(e) = state.send_daemon_command(id, command).await { + tracing::error!("Failed to send restart command to daemon {}: {}", id, e); + return ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(ApiError::new("DAEMON_ERROR", e)), + ) + .into_response(); + } + + tracing::info!( + daemon_id = %id, + owner_id = %auth.owner_id, + "Restart command sent to daemon" + ); + + Json(RestartDaemonResponse { + success: true, + daemon_id: id, + message: "Restart command sent. The daemon will restart shortly.".to_string(), + }) + .into_response() +} -- cgit v1.2.3