summaryrefslogtreecommitdiff
path: root/makima/src/server
diff options
context:
space:
mode:
Diffstat (limited to 'makima/src/server')
-rw-r--r--makima/src/server/handlers/contracts.rs32
-rw-r--r--makima/src/server/handlers/mod.rs1
-rw-r--r--makima/src/server/handlers/repository_history.rs173
-rw-r--r--makima/src/server/mod.rs15
-rw-r--r--makima/src/server/openapi.rs18
5 files changed, 234 insertions, 5 deletions
diff --git a/makima/src/server/handlers/contracts.rs b/makima/src/server/handlers/contracts.rs
index a3aa00a..3ce29e1 100644
--- a/makima/src/server/handlers/contracts.rs
+++ b/makima/src/server/handlers/contracts.rs
@@ -631,6 +631,22 @@ pub async fn add_remote_repository(
Ok(repo) => {
// Update supervisor task with repository info if this is a primary repo
update_supervisor_with_repo_if_needed(pool, id, auth.owner_id, &repo).await;
+
+ // Track repository in history for future suggestions
+ if let Err(e) = repository::add_or_update_repository_history(
+ pool,
+ auth.owner_id,
+ &req.name,
+ Some(&req.repository_url),
+ None,
+ "remote",
+ )
+ .await
+ {
+ // Log but don't fail the request if history tracking fails
+ tracing::warn!("Failed to track repository in history: {}", e);
+ }
+
(StatusCode::CREATED, Json(repo)).into_response()
}
Err(e) => {
@@ -705,6 +721,22 @@ pub async fn add_local_repository(
Ok(repo) => {
// Update supervisor task with repository info if this is a primary repo
update_supervisor_with_repo_if_needed(pool, id, auth.owner_id, &repo).await;
+
+ // Track repository in history for future suggestions
+ if let Err(e) = repository::add_or_update_repository_history(
+ pool,
+ auth.owner_id,
+ &req.name,
+ None,
+ Some(&req.local_path),
+ "local",
+ )
+ .await
+ {
+ // Log but don't fail the request if history tracking fails
+ tracing::warn!("Failed to track repository in history: {}", e);
+ }
+
(StatusCode::CREATED, Json(repo)).into_response()
}
Err(e) => {
diff --git a/makima/src/server/handlers/mod.rs b/makima/src/server/handlers/mod.rs
index 0ce6c85..b5650fd 100644
--- a/makima/src/server/handlers/mod.rs
+++ b/makima/src/server/handlers/mod.rs
@@ -14,6 +14,7 @@ pub mod mesh_daemon;
pub mod mesh_merge;
pub mod mesh_supervisor;
pub mod mesh_ws;
+pub mod repository_history;
pub mod templates;
pub mod transcript_analysis;
pub mod users;
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()
+ }
+ }
+}
diff --git a/makima/src/server/mod.rs b/makima/src/server/mod.rs
index 27ee06c..0eba009 100644
--- a/makima/src/server/mod.rs
+++ b/makima/src/server/mod.rs
@@ -18,7 +18,7 @@ use tower_http::trace::TraceLayer;
use utoipa::OpenApi;
use utoipa_swagger_ui::SwaggerUi;
-use crate::server::handlers::{api_keys, chat, contract_chat, contract_daemon, contracts, file_ws, files, listen, mesh, mesh_chat, mesh_daemon, mesh_merge, mesh_supervisor, mesh_ws, templates, transcript_analysis, users, versions};
+use crate::server::handlers::{api_keys, chat, contract_chat, contract_daemon, contracts, file_ws, files, listen, mesh, mesh_chat, mesh_daemon, mesh_merge, mesh_supervisor, mesh_ws, repository_history, templates, transcript_analysis, users, versions};
use crate::server::openapi::ApiDoc;
use crate::server::state::SharedState;
@@ -191,6 +191,19 @@ pub fn make_router(state: SharedState) -> Router {
// Template endpoints
.route("/templates", get(templates::list_templates))
.route("/templates/{id}", get(templates::get_template))
+ // Settings endpoints
+ .route(
+ "/settings/repository-history",
+ get(repository_history::list_repository_history),
+ )
+ .route(
+ "/settings/repository-history/suggestions",
+ get(repository_history::get_repository_suggestions),
+ )
+ .route(
+ "/settings/repository-history/{id}",
+ axum::routing::delete(repository_history::delete_repository_history),
+ )
.with_state(state);
let swagger = SwaggerUi::new("/swagger-ui")
diff --git a/makima/src/server/openapi.rs b/makima/src/server/openapi.rs
index c4f0f19..afa114b 100644
--- a/makima/src/server/openapi.rs
+++ b/makima/src/server/openapi.rs
@@ -10,15 +10,16 @@ use crate::db::models::{
Daemon, DaemonDirectoriesResponse, DaemonDirectory, DaemonListResponse, File, FileListResponse,
FileSummary, MergeCommitRequest, MergeCompleteCheckResponse, MergeResolveRequest,
MergeResultResponse, MergeSkipRequest, MergeStartRequest, MergeStatusResponse,
- MeshChatConversation, MeshChatHistoryResponse, MeshChatMessageRecord, SendMessageRequest, Task,
- TaskEventListResponse, TaskListResponse, TaskSummary, TaskWithSubtasks, TranscriptEntry,
- UpdateContractRequest, UpdateFileRequest, UpdateTaskRequest,
+ MeshChatConversation, MeshChatHistoryResponse, MeshChatMessageRecord,
+ RepositoryHistoryEntry, RepositoryHistoryListResponse, RepositorySuggestionsQuery,
+ SendMessageRequest, Task, TaskEventListResponse, TaskListResponse, TaskSummary,
+ TaskWithSubtasks, TranscriptEntry, UpdateContractRequest, UpdateFileRequest, UpdateTaskRequest,
};
use crate::server::auth::{
ApiKey, ApiKeyInfoResponse, CreateApiKeyRequest, CreateApiKeyResponse,
RefreshApiKeyRequest, RefreshApiKeyResponse, RevokeApiKeyResponse,
};
-use crate::server::handlers::{api_keys, contract_chat, contracts, files, listen, mesh, mesh_chat, mesh_merge, users};
+use crate::server::handlers::{api_keys, contract_chat, contracts, files, listen, mesh, mesh_chat, mesh_merge, repository_history, users};
use crate::server::messages::{ApiError, AudioEncoding, StartMessage, StopMessage, TranscriptMessage};
#[derive(OpenApi)]
@@ -94,6 +95,10 @@ use crate::server::messages::{ApiError, AudioEncoding, StartMessage, StopMessage
contract_chat::contract_chat_handler,
contract_chat::get_contract_chat_history,
contract_chat::clear_contract_chat_history,
+ // Repository history/settings endpoints
+ repository_history::list_repository_history,
+ repository_history::get_repository_suggestions,
+ repository_history::delete_repository_history,
),
components(
schemas(
@@ -166,6 +171,10 @@ use crate::server::messages::{ApiError, AudioEncoding, StartMessage, StopMessage
AddLocalRepositoryRequest,
CreateManagedRepositoryRequest,
ChangePhaseRequest,
+ // Repository history schemas
+ RepositoryHistoryEntry,
+ RepositoryHistoryListResponse,
+ RepositorySuggestionsQuery,
)
),
tags(
@@ -175,6 +184,7 @@ use crate::server::messages::{ApiError, AudioEncoding, StartMessage, StopMessage
(name = "Contracts", description = "Contract management with workflow phases"),
(name = "API Keys", description = "API key management for programmatic access"),
(name = "Users", description = "User account management"),
+ (name = "Settings", description = "User settings including repository history"),
)
)]
pub struct ApiDoc;