//! HTTP handlers for directive CRUD and DAG progression. use axum::{ extract::{Path, Query, State}, http::StatusCode, response::IntoResponse, Json, }; use serde::Deserialize; use uuid::Uuid; use crate::db::models::{ BatchSetDirectiveMemoryRequest, CleanupTasksResponse, CreateDirectiveRequest, CreateDirectiveStepRequest, Directive, DirectiveListResponse, DirectiveMemory, DirectiveMemoryListResponse, DirectiveStep, DirectiveWithSteps, SetDirectiveMemoryRequest, UpdateDirectiveRequest, UpdateDirectiveStepRequest, UpdateGoalRequest, }; use crate::db::repository; use crate::server::auth::Authenticated; use crate::server::messages::ApiError; use crate::server::state::SharedState; /// Query parameters for the memory list endpoint. #[derive(Debug, Deserialize)] pub struct MemoryListQuery { pub category: Option, } // ============================================================================= // Directive CRUD // ============================================================================= /// List all directives for the authenticated user. #[utoipa::path( get, path = "/api/v1/directives", responses( (status = 200, description = "List of directives", body = DirectiveListResponse), (status = 401, description = "Unauthorized", body = ApiError), (status = 503, description = "Database not configured", body = ApiError), ), security(("bearer_auth" = []), ("api_key" = [])), tag = "Directives" )] pub async fn list_directives( State(state): State, Authenticated(auth): Authenticated, ) -> impl IntoResponse { let Some(ref pool) = state.db_pool else { return ( StatusCode::SERVICE_UNAVAILABLE, Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), ) .into_response(); }; match repository::list_directives_for_owner(pool, auth.owner_id).await { Ok(directives) => { let total = directives.len() as i64; Json(DirectiveListResponse { directives, total }).into_response() } Err(e) => { tracing::error!("Failed to list directives: {}", e); ( StatusCode::INTERNAL_SERVER_ERROR, Json(ApiError::new("LIST_FAILED", &e.to_string())), ) .into_response() } } } /// Create a new directive. #[utoipa::path( post, path = "/api/v1/directives", request_body = CreateDirectiveRequest, responses( (status = 201, description = "Directive created", body = Directive), (status = 401, description = "Unauthorized", body = ApiError), (status = 503, description = "Database not configured", body = ApiError), ), security(("bearer_auth" = []), ("api_key" = [])), tag = "Directives" )] pub async fn create_directive( State(state): State, Authenticated(auth): Authenticated, Json(req): Json, ) -> impl IntoResponse { let Some(ref pool) = state.db_pool else { return ( StatusCode::SERVICE_UNAVAILABLE, Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), ) .into_response(); }; match repository::create_directive_for_owner(pool, auth.owner_id, req).await { Ok(directive) => (StatusCode::CREATED, Json(directive)).into_response(), Err(e) => { tracing::error!("Failed to create directive: {}", e); ( StatusCode::INTERNAL_SERVER_ERROR, Json(ApiError::new("CREATE_FAILED", &e.to_string())), ) .into_response() } } } /// Get a directive with all its steps. #[utoipa::path( get, path = "/api/v1/directives/{id}", params(("id" = Uuid, Path, description = "Directive ID")), responses( (status = 200, description = "Directive with steps", body = DirectiveWithSteps), (status = 404, description = "Not found", body = ApiError), (status = 503, description = "Database not configured", body = ApiError), ), security(("bearer_auth" = []), ("api_key" = [])), tag = "Directives" )] pub async fn get_directive( 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(); }; match repository::get_directive_with_steps_for_owner(pool, auth.owner_id, id).await { Ok(Some((directive, steps))) => { Json(DirectiveWithSteps { directive, steps }).into_response() } Ok(None) => ( StatusCode::NOT_FOUND, Json(ApiError::new("NOT_FOUND", "Directive not found")), ) .into_response(), Err(e) => { tracing::error!("Failed to get directive: {}", e); ( StatusCode::INTERNAL_SERVER_ERROR, Json(ApiError::new("GET_FAILED", &e.to_string())), ) .into_response() } } } /// Update a directive. #[utoipa::path( put, path = "/api/v1/directives/{id}", params(("id" = Uuid, Path, description = "Directive ID")), request_body = UpdateDirectiveRequest, responses( (status = 200, description = "Directive updated", body = Directive), (status = 404, description = "Not found", body = ApiError), (status = 409, description = "Version conflict", body = ApiError), (status = 503, description = "Database not configured", body = ApiError), ), security(("bearer_auth" = []), ("api_key" = [])), tag = "Directives" )] pub async fn update_directive( State(state): State, Authenticated(auth): Authenticated, Path(id): Path, Json(req): Json, ) -> impl IntoResponse { let Some(ref pool) = state.db_pool else { return ( StatusCode::SERVICE_UNAVAILABLE, Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), ) .into_response(); }; match repository::update_directive_for_owner(pool, auth.owner_id, id, req).await { Ok(Some(directive)) => Json(directive).into_response(), Ok(None) => ( StatusCode::NOT_FOUND, Json(ApiError::new("NOT_FOUND", "Directive not found")), ) .into_response(), Err(repository::RepositoryError::VersionConflict { expected, actual }) => ( StatusCode::CONFLICT, Json(ApiError::new( "VERSION_CONFLICT", &format!("Expected version {}, but current is {}", expected, actual), )), ) .into_response(), Err(e) => { tracing::error!("Failed to update directive: {}", e); ( StatusCode::INTERNAL_SERVER_ERROR, Json(ApiError::new("UPDATE_FAILED", &e.to_string())), ) .into_response() } } } /// Delete a directive. #[utoipa::path( delete, path = "/api/v1/directives/{id}", params(("id" = Uuid, Path, description = "Directive ID")), responses( (status = 204, description = "Deleted"), (status = 404, description = "Not found", body = ApiError), (status = 503, description = "Database not configured", body = ApiError), ), security(("bearer_auth" = []), ("api_key" = [])), tag = "Directives" )] pub async fn delete_directive( 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(); }; match repository::delete_directive_for_owner(pool, auth.owner_id, id).await { Ok(true) => StatusCode::NO_CONTENT.into_response(), Ok(false) => ( StatusCode::NOT_FOUND, Json(ApiError::new("NOT_FOUND", "Directive not found")), ) .into_response(), Err(e) => { tracing::error!("Failed to delete directive: {}", e); ( StatusCode::INTERNAL_SERVER_ERROR, Json(ApiError::new("DELETE_FAILED", &e.to_string())), ) .into_response() } } } // ============================================================================= // Step CRUD // ============================================================================= /// Create a step in a directive. #[utoipa::path( post, path = "/api/v1/directives/{id}/steps", params(("id" = Uuid, Path, description = "Directive ID")), request_body = CreateDirectiveStepRequest, responses( (status = 201, description = "Step created", body = DirectiveStep), (status = 404, description = "Directive not found", body = ApiError), (status = 503, description = "Database not configured", body = ApiError), ), security(("bearer_auth" = []), ("api_key" = [])), tag = "Directives" )] pub async fn create_step( State(state): State, Authenticated(auth): Authenticated, Path(id): Path, Json(req): Json, ) -> 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 directive ownership match repository::get_directive_for_owner(pool, auth.owner_id, id).await { Ok(Some(_)) => {} Ok(None) => { return ( StatusCode::NOT_FOUND, Json(ApiError::new("NOT_FOUND", "Directive not found")), ) .into_response(); } Err(e) => { return ( StatusCode::INTERNAL_SERVER_ERROR, Json(ApiError::new("GET_FAILED", &e.to_string())), ) .into_response(); } } match repository::create_directive_step(pool, id, req).await { Ok(step) => (StatusCode::CREATED, Json(step)).into_response(), Err(e) => { tracing::error!("Failed to create step: {}", e); ( StatusCode::INTERNAL_SERVER_ERROR, Json(ApiError::new("CREATE_FAILED", &e.to_string())), ) .into_response() } } } /// Batch create steps in a directive. #[utoipa::path( post, path = "/api/v1/directives/{id}/steps/batch", params(("id" = Uuid, Path, description = "Directive ID")), request_body = Vec, responses( (status = 201, description = "Steps created", body = Vec), (status = 404, description = "Directive not found", body = ApiError), (status = 503, description = "Database not configured", body = ApiError), ), security(("bearer_auth" = []), ("api_key" = [])), tag = "Directives" )] pub async fn batch_create_steps( State(state): State, Authenticated(auth): Authenticated, Path(id): Path, Json(steps): Json>, ) -> 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 directive ownership match repository::get_directive_for_owner(pool, auth.owner_id, id).await { Ok(Some(_)) => {} Ok(None) => { return ( StatusCode::NOT_FOUND, Json(ApiError::new("NOT_FOUND", "Directive not found")), ) .into_response(); } Err(e) => { return ( StatusCode::INTERNAL_SERVER_ERROR, Json(ApiError::new("GET_FAILED", &e.to_string())), ) .into_response(); } } match repository::batch_create_directive_steps(pool, id, steps).await { Ok(created) => (StatusCode::CREATED, Json(created)).into_response(), Err(e) => { tracing::error!("Failed to batch create steps: {}", e); ( StatusCode::INTERNAL_SERVER_ERROR, Json(ApiError::new("CREATE_FAILED", &e.to_string())), ) .into_response() } } } /// Update a step. #[utoipa::path( put, path = "/api/v1/directives/{id}/steps/{step_id}", params( ("id" = Uuid, Path, description = "Directive ID"), ("step_id" = Uuid, Path, description = "Step ID"), ), request_body = UpdateDirectiveStepRequest, responses( (status = 200, description = "Step updated", body = DirectiveStep), (status = 404, description = "Not found", body = ApiError), (status = 503, description = "Database not configured", body = ApiError), ), security(("bearer_auth" = []), ("api_key" = [])), tag = "Directives" )] pub async fn update_step( State(state): State, Authenticated(auth): Authenticated, Path((id, step_id)): Path<(Uuid, Uuid)>, Json(req): Json, ) -> 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 directive ownership match repository::get_directive_for_owner(pool, auth.owner_id, id).await { Ok(Some(_)) => {} Ok(None) => { return ( StatusCode::NOT_FOUND, Json(ApiError::new("NOT_FOUND", "Directive not found")), ) .into_response(); } Err(e) => { return ( StatusCode::INTERNAL_SERVER_ERROR, Json(ApiError::new("GET_FAILED", &e.to_string())), ) .into_response(); } } match repository::update_directive_step(pool, step_id, req).await { Ok(Some(step)) => Json(step).into_response(), Ok(None) => ( StatusCode::NOT_FOUND, Json(ApiError::new("NOT_FOUND", "Step not found")), ) .into_response(), Err(e) => { tracing::error!("Failed to update step: {}", e); ( StatusCode::INTERNAL_SERVER_ERROR, Json(ApiError::new("UPDATE_FAILED", &e.to_string())), ) .into_response() } } } /// Delete a step. #[utoipa::path( delete, path = "/api/v1/directives/{id}/steps/{step_id}", params( ("id" = Uuid, Path, description = "Directive ID"), ("step_id" = Uuid, Path, description = "Step ID"), ), responses( (status = 204, description = "Deleted"), (status = 404, description = "Not found", body = ApiError), (status = 503, description = "Database not configured", body = ApiError), ), security(("bearer_auth" = []), ("api_key" = [])), tag = "Directives" )] pub async fn delete_step( State(state): State, Authenticated(auth): Authenticated, Path((id, step_id)): Path<(Uuid, 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(); }; // Verify directive ownership match repository::get_directive_for_owner(pool, auth.owner_id, id).await { Ok(Some(_)) => {} Ok(None) => { return ( StatusCode::NOT_FOUND, Json(ApiError::new("NOT_FOUND", "Directive not found")), ) .into_response(); } Err(e) => { return ( StatusCode::INTERNAL_SERVER_ERROR, Json(ApiError::new("GET_FAILED", &e.to_string())), ) .into_response(); } } match repository::delete_directive_step(pool, step_id).await { Ok(true) => StatusCode::NO_CONTENT.into_response(), Ok(false) => ( StatusCode::NOT_FOUND, Json(ApiError::new("NOT_FOUND", "Step not found")), ) .into_response(), Err(e) => { tracing::error!("Failed to delete step: {}", e); ( StatusCode::INTERNAL_SERVER_ERROR, Json(ApiError::new("DELETE_FAILED", &e.to_string())), ) .into_response() } } } // ============================================================================= // Directive Lifecycle Actions // ============================================================================= /// Start a directive: sets status=active, advances ready steps. #[utoipa::path( post, path = "/api/v1/directives/{id}/start", params(("id" = Uuid, Path, description = "Directive ID")), responses( (status = 200, description = "Directive started", body = DirectiveWithSteps), (status = 404, description = "Not found", body = ApiError), (status = 503, description = "Database not configured", body = ApiError), ), security(("bearer_auth" = []), ("api_key" = [])), tag = "Directives" )] pub async fn start_directive( 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(); }; // Set to active match repository::set_directive_status(pool, auth.owner_id, id, "active").await { Ok(Some(directive)) => { // Advance ready steps let _ = repository::advance_directive_ready_steps(pool, id).await; let steps = repository::list_directive_steps(pool, id).await.unwrap_or_default(); Json(DirectiveWithSteps { directive, steps }).into_response() } Ok(None) => ( StatusCode::NOT_FOUND, Json(ApiError::new("NOT_FOUND", "Directive not found")), ) .into_response(), Err(e) => { tracing::error!("Failed to start directive: {}", e); ( StatusCode::INTERNAL_SERVER_ERROR, Json(ApiError::new("START_FAILED", &e.to_string())), ) .into_response() } } } /// Pause a directive. #[utoipa::path( post, path = "/api/v1/directives/{id}/pause", params(("id" = Uuid, Path, description = "Directive ID")), responses( (status = 200, description = "Directive paused", body = Directive), (status = 404, description = "Not found", body = ApiError), (status = 503, description = "Database not configured", body = ApiError), ), security(("bearer_auth" = []), ("api_key" = [])), tag = "Directives" )] pub async fn pause_directive( 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(); }; match repository::set_directive_status(pool, auth.owner_id, id, "paused").await { Ok(Some(directive)) => Json(directive).into_response(), Ok(None) => ( StatusCode::NOT_FOUND, Json(ApiError::new("NOT_FOUND", "Directive not found")), ) .into_response(), Err(e) => ( StatusCode::INTERNAL_SERVER_ERROR, Json(ApiError::new("PAUSE_FAILED", &e.to_string())), ) .into_response(), } } /// Advance a directive: find newly-ready steps. If all steps done, set idle. #[utoipa::path( post, path = "/api/v1/directives/{id}/advance", params(("id" = Uuid, Path, description = "Directive ID")), responses( (status = 200, description = "Advance result", body = DirectiveWithSteps), (status = 404, description = "Not found", body = ApiError), (status = 503, description = "Database not configured", body = ApiError), ), security(("bearer_auth" = []), ("api_key" = [])), tag = "Directives" )] pub async fn advance_directive( 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 ownership let directive = match repository::get_directive_for_owner(pool, auth.owner_id, id).await { Ok(Some(d)) => d, Ok(None) => { return ( StatusCode::NOT_FOUND, Json(ApiError::new("NOT_FOUND", "Directive not found")), ) .into_response(); } Err(e) => { return ( StatusCode::INTERNAL_SERVER_ERROR, Json(ApiError::new("GET_FAILED", &e.to_string())), ) .into_response(); } }; // Advance ready steps let _ = repository::advance_directive_ready_steps(pool, id).await; // Check if idle let _ = repository::check_directive_idle(pool, id).await; // Return updated state let directive = match repository::get_directive_for_owner(pool, auth.owner_id, id).await { Ok(Some(d)) => d, _ => directive, }; let steps = repository::list_directive_steps(pool, id).await.unwrap_or_default(); Json(DirectiveWithSteps { directive, steps }).into_response() } /// Mark a step as completed. #[utoipa::path( post, path = "/api/v1/directives/{id}/steps/{step_id}/complete", params( ("id" = Uuid, Path, description = "Directive ID"), ("step_id" = Uuid, Path, description = "Step ID"), ), responses( (status = 200, description = "Step completed", body = DirectiveStep), (status = 404, description = "Not found", body = ApiError), (status = 503, description = "Database not configured", body = ApiError), ), security(("bearer_auth" = []), ("api_key" = [])), tag = "Directives" )] pub async fn complete_step( State(state): State, Authenticated(auth): Authenticated, Path((id, step_id)): Path<(Uuid, Uuid)>, ) -> impl IntoResponse { step_status_change(state, auth, id, step_id, "completed").await } /// Mark a step as failed. #[utoipa::path( post, path = "/api/v1/directives/{id}/steps/{step_id}/fail", params( ("id" = Uuid, Path, description = "Directive ID"), ("step_id" = Uuid, Path, description = "Step ID"), ), responses( (status = 200, description = "Step failed", body = DirectiveStep), (status = 404, description = "Not found", body = ApiError), (status = 503, description = "Database not configured", body = ApiError), ), security(("bearer_auth" = []), ("api_key" = [])), tag = "Directives" )] pub async fn fail_step( State(state): State, Authenticated(auth): Authenticated, Path((id, step_id)): Path<(Uuid, Uuid)>, ) -> impl IntoResponse { step_status_change(state, auth, id, step_id, "failed").await } /// Mark a step as skipped. #[utoipa::path( post, path = "/api/v1/directives/{id}/steps/{step_id}/skip", params( ("id" = Uuid, Path, description = "Directive ID"), ("step_id" = Uuid, Path, description = "Step ID"), ), responses( (status = 200, description = "Step skipped", body = DirectiveStep), (status = 404, description = "Not found", body = ApiError), (status = 503, description = "Database not configured", body = ApiError), ), security(("bearer_auth" = []), ("api_key" = [])), tag = "Directives" )] pub async fn skip_step( State(state): State, Authenticated(auth): Authenticated, Path((id, step_id)): Path<(Uuid, Uuid)>, ) -> impl IntoResponse { step_status_change(state, auth, id, step_id, "skipped").await } /// Helper for step status changes. async fn step_status_change( state: SharedState, auth: crate::server::auth::AuthenticatedUser, directive_id: Uuid, step_id: Uuid, new_status: &str, ) -> 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 directive ownership match repository::get_directive_for_owner(pool, auth.owner_id, directive_id).await { Ok(Some(_)) => {} Ok(None) => { return ( StatusCode::NOT_FOUND, Json(ApiError::new("NOT_FOUND", "Directive not found")), ) .into_response(); } Err(e) => { return ( StatusCode::INTERNAL_SERVER_ERROR, Json(ApiError::new("GET_FAILED", &e.to_string())), ) .into_response(); } } let req = UpdateDirectiveStepRequest { status: Some(new_status.to_string()), ..Default::default() }; match repository::update_directive_step(pool, step_id, req).await { Ok(Some(step)) => { // After step status change, advance the DAG let _ = repository::advance_directive_ready_steps(pool, directive_id).await; let _ = repository::check_directive_idle(pool, directive_id).await; Json(step).into_response() } Ok(None) => ( StatusCode::NOT_FOUND, Json(ApiError::new("NOT_FOUND", "Step not found")), ) .into_response(), Err(e) => { tracing::error!("Failed to update step status: {}", e); ( StatusCode::INTERNAL_SERVER_ERROR, Json(ApiError::new("UPDATE_FAILED", &e.to_string())), ) .into_response() } } } /// Update a directive's goal (triggers re-planning). #[utoipa::path( put, path = "/api/v1/directives/{id}/goal", params(("id" = Uuid, Path, description = "Directive ID")), request_body = UpdateGoalRequest, responses( (status = 200, description = "Goal updated", body = Directive), (status = 404, description = "Not found", body = ApiError), (status = 503, description = "Database not configured", body = ApiError), ), security(("bearer_auth" = []), ("api_key" = [])), tag = "Directives" )] pub async fn update_goal( State(state): State, Authenticated(auth): Authenticated, Path(id): Path, Json(req): Json, ) -> impl IntoResponse { let Some(ref pool) = state.db_pool else { return ( StatusCode::SERVICE_UNAVAILABLE, Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), ) .into_response(); }; match repository::update_directive_goal(pool, auth.owner_id, id, &req.goal).await { Ok(Some(directive)) => Json(directive).into_response(), Ok(None) => ( StatusCode::NOT_FOUND, Json(ApiError::new("NOT_FOUND", "Directive not found")), ) .into_response(), Err(e) => { tracing::error!("Failed to update goal: {}", e); ( StatusCode::INTERNAL_SERVER_ERROR, Json(ApiError::new("UPDATE_FAILED", &e.to_string())), ) .into_response() } } } // ============================================================================= // Directive Memory CRUD // ============================================================================= /// List all memories for a directive, optionally filtered by category. #[utoipa::path( get, path = "/api/v1/directives/{id}/memories", params( ("id" = Uuid, Path, description = "Directive ID"), ("category" = Option, Query, description = "Filter by category"), ), responses( (status = 200, description = "List of memories", body = DirectiveMemoryListResponse), (status = 404, description = "Directive not found", body = ApiError), (status = 503, description = "Database not configured", body = ApiError), ), security(("bearer_auth" = []), ("api_key" = [])), tag = "Directives" )] pub async fn list_memories( State(state): State, Authenticated(auth): Authenticated, Path(id): Path, Query(query): Query, ) -> 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 directive ownership match repository::get_directive_for_owner(pool, auth.owner_id, id).await { Ok(Some(_)) => {} Ok(None) => { return ( StatusCode::NOT_FOUND, Json(ApiError::new("NOT_FOUND", "Directive not found")), ) .into_response(); } Err(e) => { return ( StatusCode::INTERNAL_SERVER_ERROR, Json(ApiError::new("GET_FAILED", &e.to_string())), ) .into_response(); } } match repository::list_directive_memories(pool, id, query.category.as_deref()).await { Ok(memories) => { let total = memories.len() as i64; Json(DirectiveMemoryListResponse { memories, total }).into_response() } Err(e) => { tracing::error!("Failed to list memories: {}", e); ( StatusCode::INTERNAL_SERVER_ERROR, Json(ApiError::new("LIST_FAILED", &e.to_string())), ) .into_response() } } } /// Get a single memory entry by key. #[utoipa::path( get, path = "/api/v1/directives/{id}/memories/{key}", params( ("id" = Uuid, Path, description = "Directive ID"), ("key" = String, Path, description = "Memory key"), ), responses( (status = 200, description = "Memory entry", body = DirectiveMemory), (status = 404, description = "Not found", body = ApiError), (status = 503, description = "Database not configured", body = ApiError), ), security(("bearer_auth" = []), ("api_key" = [])), tag = "Directives" )] pub async fn get_memory( State(state): State, Authenticated(auth): Authenticated, Path((id, key)): Path<(Uuid, String)>, ) -> 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 directive ownership match repository::get_directive_for_owner(pool, auth.owner_id, id).await { Ok(Some(_)) => {} Ok(None) => { return ( StatusCode::NOT_FOUND, Json(ApiError::new("NOT_FOUND", "Directive not found")), ) .into_response(); } Err(e) => { return ( StatusCode::INTERNAL_SERVER_ERROR, Json(ApiError::new("GET_FAILED", &e.to_string())), ) .into_response(); } } match repository::get_directive_memory(pool, id, &key).await { Ok(Some(memory)) => Json(memory).into_response(), Ok(None) => ( StatusCode::NOT_FOUND, Json(ApiError::new("NOT_FOUND", "Memory entry not found")), ) .into_response(), Err(e) => { tracing::error!("Failed to get memory: {}", e); ( StatusCode::INTERNAL_SERVER_ERROR, Json(ApiError::new("GET_FAILED", &e.to_string())), ) .into_response() } } } /// Set (upsert) a single memory entry. #[utoipa::path( post, path = "/api/v1/directives/{id}/memories", params(("id" = Uuid, Path, description = "Directive ID")), request_body = SetDirectiveMemoryRequest, responses( (status = 200, description = "Memory entry set", body = DirectiveMemory), (status = 404, description = "Directive not found", body = ApiError), (status = 503, description = "Database not configured", body = ApiError), ), security(("bearer_auth" = []), ("api_key" = [])), tag = "Directives" )] pub async fn set_memory( State(state): State, Authenticated(auth): Authenticated, Path(id): Path, Json(req): Json, ) -> 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 directive ownership match repository::get_directive_for_owner(pool, auth.owner_id, id).await { Ok(Some(_)) => {} Ok(None) => { return ( StatusCode::NOT_FOUND, Json(ApiError::new("NOT_FOUND", "Directive not found")), ) .into_response(); } Err(e) => { return ( StatusCode::INTERNAL_SERVER_ERROR, Json(ApiError::new("GET_FAILED", &e.to_string())), ) .into_response(); } } match repository::set_directive_memory(pool, id, &req).await { Ok(memory) => Json(memory).into_response(), Err(e) => { tracing::error!("Failed to set memory: {}", e); ( StatusCode::INTERNAL_SERVER_ERROR, Json(ApiError::new("SET_FAILED", &e.to_string())), ) .into_response() } } } /// Batch set multiple memory entries. #[utoipa::path( post, path = "/api/v1/directives/{id}/memories/batch", params(("id" = Uuid, Path, description = "Directive ID")), request_body = BatchSetDirectiveMemoryRequest, responses( (status = 200, description = "Memory entries set", body = Vec), (status = 404, description = "Directive not found", body = ApiError), (status = 503, description = "Database not configured", body = ApiError), ), security(("bearer_auth" = []), ("api_key" = [])), tag = "Directives" )] pub async fn batch_set_memories( State(state): State, Authenticated(auth): Authenticated, Path(id): Path, Json(req): Json, ) -> 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 directive ownership match repository::get_directive_for_owner(pool, auth.owner_id, id).await { Ok(Some(_)) => {} Ok(None) => { return ( StatusCode::NOT_FOUND, Json(ApiError::new("NOT_FOUND", "Directive not found")), ) .into_response(); } Err(e) => { return ( StatusCode::INTERNAL_SERVER_ERROR, Json(ApiError::new("GET_FAILED", &e.to_string())), ) .into_response(); } } match repository::batch_set_directive_memories(pool, id, &req.entries).await { Ok(memories) => Json(memories).into_response(), Err(e) => { tracing::error!("Failed to batch set memories: {}", e); ( StatusCode::INTERNAL_SERVER_ERROR, Json(ApiError::new("SET_FAILED", &e.to_string())), ) .into_response() } } } /// Delete a single memory entry by key. #[utoipa::path( delete, path = "/api/v1/directives/{id}/memories/{key}", params( ("id" = Uuid, Path, description = "Directive ID"), ("key" = String, Path, description = "Memory key"), ), responses( (status = 204, description = "Deleted"), (status = 404, description = "Not found", body = ApiError), (status = 503, description = "Database not configured", body = ApiError), ), security(("bearer_auth" = []), ("api_key" = [])), tag = "Directives" )] pub async fn delete_memory( State(state): State, Authenticated(auth): Authenticated, Path((id, key)): Path<(Uuid, String)>, ) -> 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 directive ownership match repository::get_directive_for_owner(pool, auth.owner_id, id).await { Ok(Some(_)) => {} Ok(None) => { return ( StatusCode::NOT_FOUND, Json(ApiError::new("NOT_FOUND", "Directive not found")), ) .into_response(); } Err(e) => { return ( StatusCode::INTERNAL_SERVER_ERROR, Json(ApiError::new("GET_FAILED", &e.to_string())), ) .into_response(); } } match repository::delete_directive_memory(pool, id, &key).await { Ok(true) => StatusCode::NO_CONTENT.into_response(), Ok(false) => ( StatusCode::NOT_FOUND, Json(ApiError::new("NOT_FOUND", "Memory entry not found")), ) .into_response(), Err(e) => { tracing::error!("Failed to delete memory: {}", e); ( StatusCode::INTERNAL_SERVER_ERROR, Json(ApiError::new("DELETE_FAILED", &e.to_string())), ) .into_response() } } } /// Clear all memories for a directive. #[utoipa::path( delete, path = "/api/v1/directives/{id}/memories", params(("id" = Uuid, Path, description = "Directive ID")), responses( (status = 204, description = "All memories cleared"), (status = 404, description = "Directive not found", body = ApiError), (status = 503, description = "Database not configured", body = ApiError), ), security(("bearer_auth" = []), ("api_key" = [])), tag = "Directives" )] pub async fn clear_memories( 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 directive ownership match repository::get_directive_for_owner(pool, auth.owner_id, id).await { Ok(Some(_)) => {} Ok(None) => { return ( StatusCode::NOT_FOUND, Json(ApiError::new("NOT_FOUND", "Directive not found")), ) .into_response(); } Err(e) => { return ( StatusCode::INTERNAL_SERVER_ERROR, Json(ApiError::new("GET_FAILED", &e.to_string())), ) .into_response(); } } match repository::clear_directive_memories(pool, id).await { Ok(_) => StatusCode::NO_CONTENT.into_response(), Err(e) => { tracing::error!("Failed to clear memories: {}", e); ( StatusCode::INTERNAL_SERVER_ERROR, Json(ApiError::new("CLEAR_FAILED", &e.to_string())), ) .into_response() } } } // ============================================================================= // Task Cleanup // ============================================================================= /// Clean up terminal tasks associated with a directive. #[utoipa::path( post, path = "/api/v1/directives/{id}/cleanup-tasks", params(("id" = Uuid, Path, description = "Directive ID")), responses( (status = 200, description = "Tasks cleaned up", body = CleanupTasksResponse), (status = 404, description = "Not found", body = ApiError), (status = 503, description = "Database not configured", body = ApiError), ), security(("bearer_auth" = []), ("api_key" = [])), tag = "Directives" )] pub async fn cleanup_tasks( 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 directive ownership match repository::get_directive_for_owner(pool, auth.owner_id, id).await { Ok(Some(_)) => {} Ok(None) => { return ( StatusCode::NOT_FOUND, Json(ApiError::new("NOT_FOUND", "Directive not found")), ) .into_response(); } Err(e) => { return ( StatusCode::INTERNAL_SERVER_ERROR, Json(ApiError::new("GET_FAILED", &e.to_string())), ) .into_response(); } } match repository::cleanup_directive_tasks(pool, auth.owner_id, id).await { Ok(deleted) => Json(CleanupTasksResponse { deleted }).into_response(), Err(e) => { tracing::error!("Failed to cleanup directive tasks: {}", e); ( StatusCode::INTERNAL_SERVER_ERROR, Json(ApiError::new("CLEANUP_FAILED", &e.to_string())), ) .into_response() } } }