//! HTTP handlers for repository history management. //! Provides endpoints for listing, suggesting, and deleting repository history entries. use axum::{ extract::{Path, Query, State}, http::StatusCode, response::IntoResponse, Json, }; use uuid::Uuid; use crate::db::models::{RepositoryHistoryListResponse, RepositorySuggestionsQuery}; use crate::db::repository; use crate::server::auth::Authenticated; use crate::server::messages::ApiError; use crate::server::state::SharedState; /// List all repository history entries for the authenticated user. /// Returns entries ordered by use_count DESC, last_used_at DESC. #[utoipa::path( get, path = "/api/v1/settings/repository-history", responses( (status = 200, description = "List of repository history entries", body = RepositoryHistoryListResponse), (status = 401, description = "Unauthorized", body = ApiError), (status = 503, description = "Database not configured", body = ApiError), (status = 500, description = "Internal server error", body = ApiError), ), security( ("bearer_auth" = []), ("api_key" = []) ), tag = "Settings" )] pub async fn list_repository_history( 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_repository_history_for_owner(pool, auth.owner_id).await { Ok(entries) => { let total = entries.len() as i64; Json(RepositoryHistoryListResponse { entries, total }).into_response() } Err(e) => { tracing::error!("Failed to list repository history: {}", e); ( StatusCode::INTERNAL_SERVER_ERROR, Json(ApiError::new("DB_ERROR", e.to_string())), ) .into_response() } } } /// Get repository suggestions based on history. /// Optionally filter by source_type (remote/local) and search query. #[utoipa::path( get, path = "/api/v1/settings/repository-history/suggestions", params( ("source_type" = Option, Query, description = "Filter by source type: 'remote' or 'local'"), ("query" = Option, Query, description = "Search query to filter by name or URL/path"), ("limit" = Option, Query, description = "Limit results (default: 10)") ), responses( (status = 200, description = "List of repository suggestions", body = RepositoryHistoryListResponse), (status = 401, description = "Unauthorized", body = ApiError), (status = 503, description = "Database not configured", body = ApiError), (status = 500, description = "Internal server error", body = ApiError), ), security( ("bearer_auth" = []), ("api_key" = []) ), tag = "Settings" )] pub async fn get_repository_suggestions( State(state): State, Authenticated(auth): Authenticated, Query(params): 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(); }; let limit = params.limit.unwrap_or(10).min(50); // Cap at 50 for safety tracing::debug!( owner_id = %auth.owner_id, source_type = ?params.source_type, query = ?params.query, limit = limit, "Fetching repository suggestions" ); match repository::get_repository_suggestions( pool, auth.owner_id, params.source_type.as_deref(), params.query.as_deref(), limit, ) .await { Ok(entries) => { // Debug log to help diagnose filtering issues for entry in &entries { tracing::debug!( id = %entry.id, name = %entry.name, source_type = %entry.source_type, has_url = entry.repository_url.is_some(), has_path = entry.local_path.is_some(), "Repository suggestion entry" ); } let total = entries.len() as i64; Json(RepositoryHistoryListResponse { entries, total }).into_response() } Err(e) => { tracing::error!("Failed to get repository suggestions: {}", e); ( StatusCode::INTERNAL_SERVER_ERROR, Json(ApiError::new("DB_ERROR", e.to_string())), ) .into_response() } } } /// Delete a repository history entry. #[utoipa::path( delete, path = "/api/v1/settings/repository-history/{id}", params( ("id" = Uuid, Path, description = "Repository history entry ID") ), responses( (status = 204, description = "Entry deleted"), (status = 401, description = "Unauthorized", body = ApiError), (status = 404, description = "Entry not found", body = ApiError), (status = 503, description = "Database not configured", body = ApiError), (status = 500, description = "Internal server error", body = ApiError), ), security( ("bearer_auth" = []), ("api_key" = []) ), tag = "Settings" )] pub async fn delete_repository_history( 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_repository_history(pool, id, auth.owner_id).await { Ok(true) => StatusCode::NO_CONTENT.into_response(), Ok(false) => ( StatusCode::NOT_FOUND, Json(ApiError::new("NOT_FOUND", "Repository history entry not found")), ) .into_response(), Err(e) => { tracing::error!("Failed to delete repository history {}: {}", id, e); ( StatusCode::INTERNAL_SERVER_ERROR, Json(ApiError::new("DB_ERROR", e.to_string())), ) .into_response() } } }