diff --git a/backend/src/handlers/article_history.rs b/backend/src/handlers/article_history.rs new file mode 100644 index 0000000..b64e07b --- /dev/null +++ b/backend/src/handlers/article_history.rs @@ -0,0 +1,78 @@ +//! Handlers for article history and provenance endpoints. + +use axum::extract::{Path, Query, State}; +use axum::response::IntoResponse; +use axum::Json; +use serde::Deserialize; +use uuid::Uuid; + +use crate::app_state::AppState; +use crate::db; +use crate::errors::AppError; +use crate::middleware::auth::AuthUser; + +#[derive(Deserialize)] +pub struct HistoryQuery { + pub limit: Option, + pub offset: Option, + pub status: Option, + pub source_type: Option, +} + +/// GET /api/v1/article-history +/// +/// Returns paginated article history with optional filters. +pub async fn list_history( + auth_user: AuthUser, + State(state): State, + Query(params): Query, +) -> Result { + let limit = params.limit.unwrap_or(50).clamp(1, 200); + let offset = params.offset.unwrap_or(0).max(0); + + let items = db::article_history::list_history( + &state.pool, + auth_user.id, + limit, + offset, + params.status.as_deref(), + params.source_type.as_deref(), + ) + .await?; + + let total = db::article_history::count_history( + &state.pool, + auth_user.id, + params.status.as_deref(), + params.source_type.as_deref(), + ) + .await?; + + Ok(Json(serde_json::json!({ + "items": items, + "total": total + }))) +} + +/// GET /api/v1/syntheses/:id/provenance +/// +/// Returns all article history entries for the generation run +/// that produced the given synthesis. +pub async fn get_provenance( + auth_user: AuthUser, + State(state): State, + Path(synthesis_id): Path, +) -> Result { + // Verify the synthesis belongs to this user and get its job_id + let synthesis = db::syntheses::get_by_id_for_user(&state.pool, synthesis_id, auth_user.id) + .await? + .ok_or_else(|| AppError::NotFound("Synthesis not found".into()))?; + + let job_id = synthesis.job_id.ok_or_else(|| { + AppError::NotFound("No tracing data available for this synthesis".into()) + })?; + + let items = db::article_history::list_by_job_id(&state.pool, auth_user.id, job_id).await?; + + Ok(Json(items)) +} diff --git a/backend/src/handlers/mod.rs b/backend/src/handlers/mod.rs index 8f97d1d..2bae72d 100644 --- a/backend/src/handlers/mod.rs +++ b/backend/src/handlers/mod.rs @@ -1,3 +1,4 @@ +pub mod article_history; pub mod admin; pub mod api_keys; pub mod auth; diff --git a/backend/src/router.rs b/backend/src/router.rs index df70ef0..31f2f47 100644 --- a/backend/src/router.rs +++ b/backend/src/router.rs @@ -54,6 +54,9 @@ pub fn build_router(state: AppState, config: &AppConfig) -> Router { // to avoid ambiguity with path parameter matching .route("/syntheses/generate", post(handlers::generation::trigger_generate)) .route("/syntheses/generate/{job_id}/progress", get(handlers::generation::progress_stream)) + // Article history & provenance routes (authenticated) + .route("/article-history", get(handlers::article_history::list_history)) + .route("/syntheses/{id}/provenance", get(handlers::article_history::get_provenance)) // Syntheses CRUD routes (authenticated) .route("/syntheses", get(handlers::syntheses::list)) .route("/syntheses/{id}", get(handlers::syntheses::get))