summaryrefslogtreecommitdiff
path: root/makima/src/server/handlers/repository_history.rs
diff options
context:
space:
mode:
authorsoryu <soryu@soryu.co>2026-01-15 03:37:44 +0000
committerGitHub <noreply@github.com>2026-01-15 03:37:44 +0000
commit764bd28d08ceaef03cd4050f9568a62d77bbcfca (patch)
treedbd83ea7d213902f2b8021acc98798b6f3545946 /makima/src/server/handlers/repository_history.rs
parenteeafe072bc6bb81459f7d087b48fc921afe9cc11 (diff)
downloadsoryu-764bd28d08ceaef03cd4050f9568a62d77bbcfca.tar.gz
soryu-764bd28d08ceaef03cd4050f9568a62d77bbcfca.zip
Add repository history feature to store and suggest previously used repositories (#18)
- Add repository_history table migration with repo_type, repo_path, use_count, last_used_at - Add RepositoryHistoryEntry model and CRUD database functions - Create API endpoints: GET/POST/DELETE /api/v1/repository-history, GET /api/v1/repository-history/suggestions - Update add_remote_repository and add_local_repository handlers to automatically track history - Update frontend API with repository history types and functions - Add Repository History section to Settings page with list of entries and delete functionality - Add suggestions dropdown to RepositoryPanel when entering new repository URL/path - Suggestions filter by repo type (remote vs local) and match on user input Test plan: - Add a remote repository to a contract - verify it appears in Settings history - Add a local repository to a contract - verify it appears in Settings history - Add same repository again - verify use_count increments, not duplicate - When adding new repository, verify suggestions appear based on history - Delete a history entry from Settings - verify it's removed - Verify suggestions only show matching type (remote for remote, local for local) Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Diffstat (limited to 'makima/src/server/handlers/repository_history.rs')
-rw-r--r--makima/src/server/handlers/repository_history.rs173
1 files changed, 173 insertions, 0 deletions
diff --git a/makima/src/server/handlers/repository_history.rs b/makima/src/server/handlers/repository_history.rs
new file mode 100644
index 0000000..c788d84
--- /dev/null
+++ b/makima/src/server/handlers/repository_history.rs
@@ -0,0 +1,173 @@
+//! 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<SharedState>,
+ 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<String>, Query, description = "Filter by source type: 'remote' or 'local'"),
+ ("query" = Option<String>, Query, description = "Search query to filter by name or URL/path"),
+ ("limit" = Option<i32>, 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<SharedState>,
+ Authenticated(auth): Authenticated,
+ Query(params): Query<RepositorySuggestionsQuery>,
+) -> 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
+
+ match repository::get_repository_suggestions(
+ pool,
+ auth.owner_id,
+ params.source_type.as_deref(),
+ params.query.as_deref(),
+ limit,
+ )
+ .await
+ {
+ Ok(entries) => {
+ 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<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();
+ };
+
+ 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()
+ }
+ }
+}