diff options
Diffstat (limited to 'makima/src/server/handlers/mesh.rs')
| -rw-r--r-- | makima/src/server/handlers/mesh.rs | 275 |
1 files changed, 275 insertions, 0 deletions
diff --git a/makima/src/server/handlers/mesh.rs b/makima/src/server/handlers/mesh.rs index 0e72bdf..1a5b9c1 100644 --- a/makima/src/server/handlers/mesh.rs +++ b/makima/src/server/handlers/mesh.rs @@ -2099,6 +2099,281 @@ pub async fn get_worktree_info( } // ============================================================================= +// Task Diff +// ============================================================================= + +/// Response for the task diff endpoint. +#[derive(Debug, serde::Serialize, utoipa::ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct TaskDiffApiResponse { + /// Task ID. + pub task_id: Uuid, + /// Whether the diff was retrieved successfully. + pub success: bool, + /// The diff content. + pub diff: Option<String>, + /// Error message if failed. + pub error: Option<String>, +} + +/// Get the diff for a task's changes. +#[utoipa::path( + get, + path = "/api/v1/mesh/tasks/{id}/diff", + params( + ("id" = Uuid, Path, description = "Task ID") + ), + responses( + (status = 200, description = "Task diff", body = TaskDiffApiResponse), + (status = 401, description = "Unauthorized", body = ApiError), + (status = 404, description = "Task not found", body = ApiError), + (status = 503, description = "Database not configured or daemon not connected", body = ApiError), + (status = 500, description = "Internal server error", body = ApiError), + ), + security( + ("bearer_auth" = []), + ("api_key" = []) + ), + tag = "Mesh" +)] +pub async fn get_task_diff( + State(state): State<SharedState>, + Authenticated(auth): Authenticated, + Path(id): Path<Uuid>, +) -> impl IntoResponse { + let Some(ref pool) = state.db_pool else { + return ( + StatusCode::SERVICE_UNAVAILABLE, + Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), + ) + .into_response(); + }; + + // Get the task (scoped by owner) + let task = match repository::get_task_for_owner(pool, id, auth.owner_id).await { + Ok(Some(t)) => t, + Ok(None) => { + return ( + StatusCode::NOT_FOUND, + Json(ApiError::new("NOT_FOUND", "Task not found")), + ) + .into_response(); + } + Err(e) => { + tracing::error!("Failed to get task {}: {}", id, e); + return ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(ApiError::new("DB_ERROR", e.to_string())), + ) + .into_response(); + } + }; + + // Get daemon running the task + let Some(daemon_id) = task.daemon_id else { + return ( + StatusCode::SERVICE_UNAVAILABLE, + Json(ApiError::new("NO_DAEMON", "Task has no assigned daemon")), + ) + .into_response(); + }; + + // Create oneshot channel for response + let (tx, rx) = oneshot::channel(); + + // Store the sender for the daemon message handler to use + state.pending_task_diff.insert(id, tx); + + // Send GetTaskDiff command to daemon + let command = DaemonCommand::GetTaskDiff { task_id: id }; + + if let Err(e) = state.send_daemon_command(daemon_id, command).await { + // Clean up pending request on error + state.pending_task_diff.remove(&id); + tracing::error!("Failed to send GetTaskDiff command: {}", e); + return ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(ApiError::new("DAEMON_ERROR", e)), + ) + .into_response(); + } + + // Wait for daemon response with timeout + match tokio::time::timeout(Duration::from_secs(15), rx).await { + Ok(Ok(response)) => { + Json(TaskDiffApiResponse { + task_id: id, + success: response.success, + diff: response.diff, + error: response.error, + }) + .into_response() + } + Ok(Err(_)) => { + // Channel was dropped (sender side closed) + ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(ApiError::new("DAEMON_DISCONNECTED", "Daemon disconnected before responding")), + ) + .into_response() + } + Err(_) => { + // Timeout - clean up pending request + state.pending_task_diff.remove(&id); + ( + StatusCode::GATEWAY_TIMEOUT, + Json(ApiError::new("TIMEOUT", "Daemon did not respond in time")), + ) + .into_response() + } + } +} + +// ============================================================================= +// Worktree Commit +// ============================================================================= + +/// Request body for worktree commit. +#[derive(Debug, serde::Deserialize, utoipa::ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct CommitWorktreeRequest { + /// Optional commit message. Defaults to "Worktree commit" if not provided. + pub message: Option<String>, +} + +/// Response for the worktree commit endpoint. +#[derive(Debug, serde::Serialize, utoipa::ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct CommitWorktreeApiResponse { + /// Task ID. + pub task_id: Uuid, + /// Whether the commit was successful. + pub success: bool, + /// The commit SHA if successful. + pub commit_sha: Option<String>, + /// Error message if failed. + pub error: Option<String>, +} + +/// Commit changes in a task's worktree. +#[utoipa::path( + post, + path = "/api/v1/mesh/tasks/{id}/worktree-commit", + params( + ("id" = Uuid, Path, description = "Task ID") + ), + request_body = CommitWorktreeRequest, + responses( + (status = 200, description = "Worktree commit result", body = CommitWorktreeApiResponse), + (status = 401, description = "Unauthorized", body = ApiError), + (status = 404, description = "Task not found", body = ApiError), + (status = 503, description = "Database not configured or daemon not connected", body = ApiError), + (status = 500, description = "Internal server error", body = ApiError), + ), + security( + ("bearer_auth" = []), + ("api_key" = []) + ), + tag = "Mesh" +)] +pub async fn commit_worktree( + State(state): State<SharedState>, + Authenticated(auth): Authenticated, + Path(id): Path<Uuid>, + Json(body): Json<CommitWorktreeRequest>, +) -> impl IntoResponse { + let Some(ref pool) = state.db_pool else { + return ( + StatusCode::SERVICE_UNAVAILABLE, + Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), + ) + .into_response(); + }; + + // Get the task (scoped by owner) + let task = match repository::get_task_for_owner(pool, id, auth.owner_id).await { + Ok(Some(t)) => t, + Ok(None) => { + return ( + StatusCode::NOT_FOUND, + Json(ApiError::new("NOT_FOUND", "Task not found")), + ) + .into_response(); + } + Err(e) => { + tracing::error!("Failed to get task {}: {}", id, e); + return ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(ApiError::new("DB_ERROR", e.to_string())), + ) + .into_response(); + } + }; + + // Get daemon running the task + let Some(daemon_id) = task.daemon_id else { + return ( + StatusCode::SERVICE_UNAVAILABLE, + Json(ApiError::new("NO_DAEMON", "Task has no assigned daemon")), + ) + .into_response(); + }; + + // Create oneshot channel for response + let (tx, rx) = oneshot::channel(); + + // Store the sender for the daemon message handler to use + state.pending_worktree_commit.insert(id, tx); + + // Send CommitWorktree command to daemon + let command = DaemonCommand::CommitWorktree { + task_id: id, + message: body.message, + }; + + if let Err(e) = state.send_daemon_command(daemon_id, command).await { + // Clean up pending request on error + state.pending_worktree_commit.remove(&id); + tracing::error!("Failed to send CommitWorktree command: {}", e); + return ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(ApiError::new("DAEMON_ERROR", e)), + ) + .into_response(); + } + + // Wait for daemon response with timeout + match tokio::time::timeout(Duration::from_secs(15), rx).await { + Ok(Ok(response)) => { + Json(CommitWorktreeApiResponse { + task_id: id, + success: response.success, + commit_sha: response.commit_sha, + error: response.error, + }) + .into_response() + } + Ok(Err(_)) => { + // Channel was dropped (sender side closed) + ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(ApiError::new("DAEMON_DISCONNECTED", "Daemon disconnected before responding")), + ) + .into_response() + } + Err(_) => { + // Timeout - clean up pending request + state.pending_worktree_commit.remove(&id); + ( + StatusCode::GATEWAY_TIMEOUT, + Json(ApiError::new("TIMEOUT", "Daemon did not respond in time")), + ) + .into_response() + } + } +} + +// ============================================================================= // Task Patches // ============================================================================= |
