diff options
Diffstat (limited to 'makima/src/server/mod.rs')
| -rw-r--r-- | makima/src/server/mod.rs | 118 |
1 files changed, 117 insertions, 1 deletions
diff --git a/makima/src/server/mod.rs b/makima/src/server/mod.rs index a096a5c..568b287 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, file_ws, files, listen, mesh, mesh_chat, mesh_daemon, mesh_merge, mesh_ws, 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, templates, users, versions}; use crate::server::openapi::ApiDoc; use crate::server::state::SharedState; @@ -53,6 +53,7 @@ pub fn make_router(state: SharedState) -> Router { .delete(files::delete_file), ) .route("/files/{id}/chat", post(chat::chat_handler)) + .route("/files/{id}/sync-from-repo", post(files::sync_file_from_repo)) // Version history endpoints .route("/files/{id}/versions", get(versions::list_versions)) .route("/files/{id}/versions/{version}", get(versions::get_version)) @@ -95,6 +96,20 @@ pub fn make_router(state: SharedState) -> Router { .route("/mesh/tasks/{id}/merge/abort", post(mesh_merge::merge_abort)) .route("/mesh/tasks/{id}/merge/skip", post(mesh_merge::merge_skip)) .route("/mesh/tasks/{id}/merge/check", get(mesh_merge::merge_check)) + // Checkpoint endpoints + .route("/mesh/tasks/{id}/checkpoint", post(mesh_supervisor::create_checkpoint)) + .route("/mesh/tasks/{id}/checkpoints", get(mesh_supervisor::list_checkpoints)) + // Supervisor endpoints (for supervisor.sh) + .route("/mesh/supervisor/contracts/{contract_id}/tasks", get(mesh_supervisor::list_contract_tasks)) + .route("/mesh/supervisor/contracts/{contract_id}/tree", get(mesh_supervisor::get_contract_tree)) + .route("/mesh/supervisor/tasks", post(mesh_supervisor::spawn_task)) + .route("/mesh/supervisor/tasks/{task_id}/wait", post(mesh_supervisor::wait_for_task)) + .route("/mesh/supervisor/tasks/{task_id}/read-file", post(mesh_supervisor::read_worktree_file)) + // Supervisor git operations + .route("/mesh/supervisor/branches", post(mesh_supervisor::create_branch)) + .route("/mesh/supervisor/tasks/{task_id}/merge", post(mesh_supervisor::merge_task)) + .route("/mesh/supervisor/tasks/{task_id}/diff", get(mesh_supervisor::get_task_diff)) + .route("/mesh/supervisor/pr", post(mesh_supervisor::create_pr)) // Mesh WebSocket endpoints .route("/mesh/tasks/subscribe", get(mesh_ws::task_subscription_handler)) .route("/mesh/daemons/connect", get(mesh_daemon::daemon_handler)) @@ -113,6 +128,59 @@ pub fn make_router(state: SharedState) -> Router { ) .route("/users/me/password", axum::routing::put(users::change_password_handler)) .route("/users/me/email", axum::routing::put(users::change_email_handler)) + // Contract endpoints + .route( + "/contracts", + get(contracts::list_contracts).post(contracts::create_contract), + ) + .route( + "/contracts/{id}", + get(contracts::get_contract) + .put(contracts::update_contract) + .delete(contracts::delete_contract), + ) + .route("/contracts/{id}/phase", post(contracts::change_phase)) + .route("/contracts/{id}/events", get(contracts::get_events)) + .route("/contracts/{id}/chat", post(contract_chat::contract_chat_handler)) + .route( + "/contracts/{id}/chat/history", + get(contract_chat::get_contract_chat_history).delete(contract_chat::clear_contract_chat_history), + ) + // Contract daemon endpoints (for tasks to interact with contracts) + .route("/contracts/{id}/daemon/status", get(contract_daemon::get_contract_status)) + .route("/contracts/{id}/daemon/checklist", get(contract_daemon::get_contract_checklist)) + .route("/contracts/{id}/daemon/goals", get(contract_daemon::get_contract_goals)) + .route("/contracts/{id}/daemon/report", post(contract_daemon::post_progress_report)) + .route("/contracts/{id}/daemon/suggest-action", post(contract_daemon::get_suggest_action)) + .route("/contracts/{id}/daemon/completion-action", post(contract_daemon::get_completion_action)) + .route( + "/contracts/{id}/daemon/files", + get(contract_daemon::list_contract_files).post(contract_daemon::create_contract_file), + ) + .route( + "/contracts/{id}/daemon/files/{file_id}", + get(contract_daemon::get_contract_file).put(contract_daemon::update_contract_file), + ) + // Contract repository endpoints + .route("/contracts/{id}/repositories/remote", post(contracts::add_remote_repository)) + .route("/contracts/{id}/repositories/local", post(contracts::add_local_repository)) + .route("/contracts/{id}/repositories/managed", post(contracts::create_managed_repository)) + .route( + "/contracts/{id}/repositories/{repo_id}", + axum::routing::delete(contracts::delete_repository), + ) + .route( + "/contracts/{id}/repositories/{repo_id}/primary", + axum::routing::put(contracts::set_repository_primary), + ) + // Contract task association endpoints + .route( + "/contracts/{id}/tasks/{task_id}", + post(contracts::add_task_to_contract).delete(contracts::remove_task_from_contract), + ) + // Template endpoints + .route("/templates", get(templates::list_templates)) + .route("/templates/{id}", get(templates::get_template)) .with_state(state); let swagger = SwaggerUi::new("/swagger-ui") @@ -131,12 +199,60 @@ pub fn make_router(state: SharedState) -> Router { .layer(TraceLayer::new_for_http()) } +/// Stale daemon cleanup interval in seconds +const DAEMON_CLEANUP_INTERVAL_SECS: u64 = 60; +/// Daemon heartbeat timeout in seconds (delete daemons older than this) +const DAEMON_HEARTBEAT_TIMEOUT_SECS: i64 = 120; + /// Run the HTTP server with graceful shutdown support. /// /// # Arguments /// * `state` - Shared application state containing ML models /// * `addr` - Address to bind to (e.g., "0.0.0.0:8080") pub async fn run_server(state: SharedState, addr: &str) -> anyhow::Result<()> { + // Start background daemon cleanup task if database is available + if let Some(pool) = state.db_pool.clone() { + // Initial cleanup of any stale daemons from previous server run + match crate::db::repository::delete_stale_daemons(&pool, 0).await { + Ok(deleted) if deleted > 0 => { + tracing::info!( + deleted = deleted, + "Cleaned up stale daemons from previous server run" + ); + } + Err(e) => { + tracing::warn!(error = %e, "Failed to clean up stale daemons on startup"); + } + _ => {} + } + + // Spawn periodic cleanup task + tokio::spawn(async move { + let mut interval = tokio::time::interval( + std::time::Duration::from_secs(DAEMON_CLEANUP_INTERVAL_SECS) + ); + loop { + interval.tick().await; + match crate::db::repository::delete_stale_daemons( + &pool, + DAEMON_HEARTBEAT_TIMEOUT_SECS, + ).await { + Ok(deleted) if deleted > 0 => { + tracing::info!( + deleted = deleted, + timeout_secs = DAEMON_HEARTBEAT_TIMEOUT_SECS, + "Deleted stale daemons" + ); + } + Err(e) => { + tracing::warn!(error = %e, "Failed to delete stale daemons"); + } + _ => {} + } + } + }); + } + let app = make_router(state); let listener = tokio::net::TcpListener::bind(addr).await?; |
