diff options
Diffstat (limited to 'makima/src')
| -rw-r--r-- | makima/src/daemon/process/claude.rs | 2 | ||||
| -rw-r--r-- | makima/src/daemon/storage/patch.rs | 1 | ||||
| -rw-r--r-- | makima/src/daemon/task/manager.rs | 6 | ||||
| -rw-r--r-- | makima/src/daemon/tui/mod.rs | 1 | ||||
| -rw-r--r-- | makima/src/db/models.rs | 12 | ||||
| -rw-r--r-- | makima/src/db/repository.rs | 20 | ||||
| -rw-r--r-- | makima/src/listen.rs | 2 | ||||
| -rw-r--r-- | makima/src/llm/groq.rs | 1 | ||||
| -rw-r--r-- | makima/src/orchestration/directive.rs | 99 | ||||
| -rw-r--r-- | makima/src/server/handlers/listen.rs | 3 | ||||
| -rw-r--r-- | makima/src/server/handlers/mesh.rs | 2 | ||||
| -rw-r--r-- | makima/src/server/handlers/mesh_daemon.rs | 2 | ||||
| -rw-r--r-- | makima/src/server/handlers/mesh_supervisor.rs | 2 | ||||
| -rw-r--r-- | makima/src/server/handlers/mod.rs | 1 | ||||
| -rw-r--r-- | makima/src/server/handlers/settings.rs | 196 | ||||
| -rw-r--r-- | makima/src/server/mod.rs | 22 |
16 files changed, 90 insertions, 282 deletions
diff --git a/makima/src/daemon/process/claude.rs b/makima/src/daemon/process/claude.rs index aa18fab..57c8f77 100644 --- a/makima/src/daemon/process/claude.rs +++ b/makima/src/daemon/process/claude.rs @@ -577,7 +577,6 @@ impl ProcessManager { // On Unix, create a new process group so we can kill all child processes #[cfg(unix)] { - #[allow(unused_imports)] use std::os::unix::process::CommandExt; cmd.process_group(0); } @@ -763,7 +762,6 @@ impl ProcessManager { // On Unix, create a new process group so we can kill all child processes #[cfg(unix)] { - #[allow(unused_imports)] use std::os::unix::process::CommandExt; cmd.process_group(0); } diff --git a/makima/src/daemon/storage/patch.rs b/makima/src/daemon/storage/patch.rs index c9bc6f5..05c45a3 100644 --- a/makima/src/daemon/storage/patch.rs +++ b/makima/src/daemon/storage/patch.rs @@ -387,7 +387,6 @@ fn parse_diff_stat(stat_output: &str) -> (usize, usize) { } /// Checkout a specific commit in the worktree. -#[allow(dead_code)] pub async fn checkout_commit(worktree_path: &Path, sha: &str) -> Result<(), PatchError> { let output = Command::new("git") .current_dir(worktree_path) diff --git a/makima/src/daemon/task/manager.rs b/makima/src/daemon/task/manager.rs index f483218..ca97453 100644 --- a/makima/src/daemon/task/manager.rs +++ b/makima/src/daemon/task/manager.rs @@ -20,7 +20,7 @@ use crate::daemon::error::{DaemonError, TaskError, TaskResult}; use crate::daemon::process::{ClaudeInputMessage, ProcessManager}; use crate::daemon::storage; use crate::daemon::temp::TempManager; -use crate::daemon::worktree::{is_new_repo_request, ConflictResolution, WorktreeInfo, WorktreeManager}; +use crate::daemon::worktree::{is_new_repo_request, ConflictResolution, WorktreeError, WorktreeInfo, WorktreeManager}; use crate::daemon::db::local::LocalDb; use crate::daemon::ws::{BranchInfo, DaemonCommand, DaemonMessage}; @@ -3706,7 +3706,6 @@ impl TaskManager { } /// Handle GetWorktreeDiff command - get git diff for a task's worktree. - #[allow(dead_code)] async fn handle_get_worktree_diff( &self, task_id: Uuid, @@ -5623,7 +5622,7 @@ impl TaskManagerInner { let ws_tx = self.ws_tx.clone(); // For auth error detection - let _claude_command = self.process_manager.claude_command().to_string(); + let claude_command = self.process_manager.claude_command().to_string(); let daemon_hostname = hostname::get().ok().and_then(|h| h.into_string().ok()); let mut auth_error_handled = false; @@ -5631,7 +5630,6 @@ impl TaskManagerInner { let mut accumulated_output = String::new(); let mut circuit_breaker = CircuitBreaker::new(); let mut iteration_count = 0u32; - #[allow(unused_assignments)] let mut final_exit_code: i64 = -1; // Track the final exit code across iterations // Autonomous loop: we may run multiple iterations diff --git a/makima/src/daemon/tui/mod.rs b/makima/src/daemon/tui/mod.rs index 46652ec..e52b12a 100644 --- a/makima/src/daemon/tui/mod.rs +++ b/makima/src/daemon/tui/mod.rs @@ -26,6 +26,7 @@ use crossterm::{ execute, terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, }; +use ratatui::prelude::*; use ratatui::backend::CrosstermBackend; pub type Terminal = ratatui::Terminal<CrosstermBackend<io::Stdout>>; diff --git a/makima/src/db/models.rs b/makima/src/db/models.rs index 121897d..c11150f 100644 --- a/makima/src/db/models.rs +++ b/makima/src/db/models.rs @@ -7,7 +7,6 @@ use utoipa::ToSchema; use uuid::Uuid; /// Default max retries for task daemon failover (3 attempts) -#[allow(dead_code)] fn default_max_retries() -> i32 { 3 } @@ -3051,19 +3050,24 @@ pub struct DirectiveOrderGroupListResponse { pub total: i64, } -/// User setting record from the database (key-value per owner). +// ============================================================================= +// User Settings Types +// ============================================================================= + +/// A user setting (feature flag / preference) stored in the database. #[derive(Debug, Clone, FromRow, Serialize, Deserialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct UserSetting { pub id: Uuid, pub owner_id: Uuid, pub key: String, + #[sqlx(json)] pub value: serde_json::Value, pub created_at: DateTime<Utc>, pub updated_at: DateTime<Utc>, } -/// Request body for upserting a user setting. +/// Request to upsert (create or update) a user setting. #[derive(Debug, Deserialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct UpsertUserSettingRequest { @@ -3071,7 +3075,7 @@ pub struct UpsertUserSettingRequest { pub value: serde_json::Value, } -/// Response containing a list of user settings. +/// Response wrapping a list of user settings. #[derive(Debug, Serialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct UserSettingsResponse { diff --git a/makima/src/db/repository.rs b/makima/src/db/repository.rs index 10633d5..5a912e4 100644 --- a/makima/src/db/repository.rs +++ b/makima/src/db/repository.rs @@ -21,7 +21,6 @@ use super::models::{ PhaseDefinition, SupervisorHeartbeatRecord, SupervisorState, Task, TaskCheckpoint, TaskEvent, TaskSummary, UpdateContractRequest, UpdateFileRequest, UpdateTaskRequest, UpdateTemplateRequest, - UserSetting, }; /// Repository error types. @@ -4912,7 +4911,6 @@ pub async fn sync_supervisor_state( // ============================================================================= /// Helper to truncate string to max length -#[allow(dead_code)] fn truncate_string(s: &str, max_len: usize) -> String { if s.len() <= max_len { s.to_string() @@ -6700,9 +6698,11 @@ pub async fn get_available_orders_for_dog_pickup( .await } -// ─── User Settings ─────────────────────────────────────────────────────────── +// ============================================================================= +// User Settings +// ============================================================================= -/// Get all settings for a given owner. +/// Get all settings for a user. pub async fn get_user_settings( pool: &PgPool, owner_id: Uuid, @@ -6720,7 +6720,7 @@ pub async fn get_user_settings( .await } -/// Get a single setting by owner and key. +/// Get a specific setting by key for a user. pub async fn get_user_setting( pool: &PgPool, owner_id: Uuid, @@ -6739,7 +6739,7 @@ pub async fn get_user_setting( .await } -/// Upsert a user setting (insert or update on conflict). +/// Upsert (create or update) a user setting. pub async fn upsert_user_setting( pool: &PgPool, owner_id: Uuid, @@ -6750,8 +6750,9 @@ pub async fn upsert_user_setting( r#" INSERT INTO user_settings (owner_id, key, value) VALUES ($1, $2, $3) - ON CONFLICT (owner_id, key) DO UPDATE - SET value = EXCLUDED.value, updated_at = now() + ON CONFLICT (owner_id, key) DO UPDATE SET + value = EXCLUDED.value, + updated_at = NOW() RETURNING id, owner_id, key, value, created_at, updated_at "#, ) @@ -6762,7 +6763,7 @@ pub async fn upsert_user_setting( .await } -/// Delete a user setting. Returns true if a row was deleted. +/// Delete a user setting by key. Returns true if a row was deleted. pub async fn delete_user_setting( pool: &PgPool, owner_id: Uuid, @@ -6778,6 +6779,7 @@ pub async fn delete_user_setting( .bind(key) .execute(pool) .await?; + Ok(result.rows_affected() > 0) } diff --git a/makima/src/listen.rs b/makima/src/listen.rs index 6391453..91d616c 100644 --- a/makima/src/listen.rs +++ b/makima/src/listen.rs @@ -6,7 +6,6 @@ pub use parakeet_rs::{ParakeetEOU, ParakeetTDT, TimedToken, TimestampMode}; use crate::audio; -#[allow(dead_code)] const STREAM_CHUNK_MS: u32 = 5_000; /// A segment of dialogue with speaker identification and timing. @@ -18,7 +17,6 @@ pub struct DialogueSegment { pub text: String, } -#[allow(dead_code)] pub(crate) fn listen() -> Result<Vec<DialogueSegment>, Box<dyn std::error::Error>> { let audio_path = Path::new("audio-ftc.mp3"); diff --git a/makima/src/llm/groq.rs b/makima/src/llm/groq.rs index 8da9746..ee01fcf 100644 --- a/makima/src/llm/groq.rs +++ b/makima/src/llm/groq.rs @@ -83,7 +83,6 @@ struct Choice { #[derive(Debug, Deserialize)] struct MessageResponse { - #[allow(dead_code)] role: String, content: Option<String>, tool_calls: Option<Vec<ToolCallResponse>>, diff --git a/makima/src/orchestration/directive.rs b/makima/src/orchestration/directive.rs index b9ff3fe..1e025c8 100644 --- a/makima/src/orchestration/directive.rs +++ b/makima/src/orchestration/directive.rs @@ -442,59 +442,64 @@ impl DirectiveOrchestrator { let directives = repository::get_directives_needing_replanning(&self.pool).await?; for directive in directives { - tracing::info!( - directive_id = %directive.id, - title = %directive.title, - "Directive goal updated — spawning re-planning task" - ); + if let Err(e) = async { + tracing::info!( + directive_id = %directive.id, + title = %directive.title, + "Directive goal updated — spawning re-planning task" + ); - // If directive already has a PR, remove completed steps that were included in it - if directive.pr_url.is_some() || directive.pr_branch.is_some() { - match remove_already_merged_steps(&self.pool, directive.id).await { - Ok(count) if count > 0 => { - tracing::info!( - directive_id = %directive.id, - removed = count, - "Auto-removed completed steps already included in PR before replanning" - ); - } - Err(e) => { - tracing::warn!( - directive_id = %directive.id, - error = %e, - "Failed to auto-remove merged steps before replanning" - ); + // If directive already has a PR, remove completed steps that were included in it + if directive.pr_url.is_some() || directive.pr_branch.is_some() { + match remove_already_merged_steps(&self.pool, directive.id).await { + Ok(count) if count > 0 => { + tracing::info!( + directive_id = %directive.id, + removed = count, + "Auto-removed completed steps already included in PR before replanning" + ); + } + Err(e) => { + tracing::warn!( + directive_id = %directive.id, + error = %e, + "Failed to auto-remove merged steps before replanning" + ); + } + _ => {} } - _ => {} } - } - let existing_steps = - repository::list_directive_steps(&self.pool, directive.id).await?; - let generation = - repository::get_directive_max_generation(&self.pool, directive.id).await? + 1; - let goal_history = - repository::get_directive_goal_history(&self.pool, directive.id, 3).await?; + let existing_steps = + repository::list_directive_steps(&self.pool, directive.id).await?; + let generation = + repository::get_directive_max_generation(&self.pool, directive.id).await? + 1; + let goal_history = + repository::get_directive_goal_history(&self.pool, directive.id, 3).await?; - let plan = - build_planning_prompt(&directive, &existing_steps, generation, &goal_history); + let plan = + build_planning_prompt(&directive, &existing_steps, generation, &goal_history); - if let Err(e) = self - .spawn_orchestrator_task( - directive.id, - directive.owner_id, - format!("Re-plan: {}", directive.title), - plan, - directive.repository_url.as_deref(), - directive.base_branch.as_deref(), - ) - .await - { - tracing::warn!( - directive_id = %directive.id, - error = %e, - "Failed to spawn re-planning task" - ); + if let Err(e) = self + .spawn_orchestrator_task( + directive.id, + directive.owner_id, + format!("Re-plan: {}", directive.title), + plan, + directive.repository_url.as_deref(), + directive.base_branch.as_deref(), + ) + .await + { + tracing::warn!( + directive_id = %directive.id, + error = %e, + "Failed to spawn re-planning task" + ); + } + Ok::<(), anyhow::Error>(()) + }.await { + tracing::warn!(directive_id = %directive.id, error = %e, "Error in re-planning directive — continuing"); } } Ok(()) diff --git a/makima/src/server/handlers/listen.rs b/makima/src/server/handlers/listen.rs index 7edcdfc..e1bc30e 100644 --- a/makima/src/server/handlers/listen.rs +++ b/makima/src/server/handlers/listen.rs @@ -208,7 +208,8 @@ async fn handle_socket(socket: WebSocket, state: SharedState) { audio_offset = 0.0; finalized_segments.clear(); file_id = None; - // authenticated_owner_id and target_contract_id are kept from above + authenticated_owner_id = authenticated_owner_id; // Keep from above + target_contract_id = target_contract_id; // Keep from above // Reset models for new session let mut sortformer = ml_models.sortformer.lock().await; diff --git a/makima/src/server/handlers/mesh.rs b/makima/src/server/handlers/mesh.rs index d9d40d7..1a5b9c1 100644 --- a/makima/src/server/handlers/mesh.rs +++ b/makima/src/server/handlers/mesh.rs @@ -20,7 +20,7 @@ use crate::db::models::{ use crate::db::repository::{self, RepositoryError}; use crate::server::auth::Authenticated; use crate::server::messages::ApiError; -use crate::server::state::{DaemonCommand, SharedState, TaskUpdateNotification}; +use crate::server::state::{DaemonCommand, DaemonReauthStatus, SharedState, TaskUpdateNotification}; // ============================================================================= // Authentication Types diff --git a/makima/src/server/handlers/mesh_daemon.rs b/makima/src/server/handlers/mesh_daemon.rs index ed1b603..e5f0a81 100644 --- a/makima/src/server/handlers/mesh_daemon.rs +++ b/makima/src/server/handlers/mesh_daemon.rs @@ -1627,7 +1627,7 @@ async fn handle_daemon_connection(socket: WebSocket, state: SharedState, auth_re ); // Broadcast as task output with auth_required type so UI can display the login link - let _content = format!( + let content = format!( "🔐 Authentication required on daemon{}. Click to login: {}", hostname.as_ref().map(|h| format!(" ({})", h)).unwrap_or_default(), login_url diff --git a/makima/src/server/handlers/mesh_supervisor.rs b/makima/src/server/handlers/mesh_supervisor.rs index 3adb850..ebde52b 100644 --- a/makima/src/server/handlers/mesh_supervisor.rs +++ b/makima/src/server/handlers/mesh_supervisor.rs @@ -1272,7 +1272,7 @@ pub async fn create_branch( headers: HeaderMap, Json(request): Json<CreateBranchRequest>, ) -> impl IntoResponse { - let (supervisor_id, _owner_id) = match verify_supervisor_auth(&state, &headers, None).await { + let (supervisor_id, owner_id) = match verify_supervisor_auth(&state, &headers, None).await { Ok(ids) => ids, Err(e) => return e.into_response(), }; diff --git a/makima/src/server/handlers/mod.rs b/makima/src/server/handlers/mod.rs index b3c433b..4bdb424 100644 --- a/makima/src/server/handlers/mod.rs +++ b/makima/src/server/handlers/mod.rs @@ -20,7 +20,6 @@ pub mod mesh_merge; pub mod mesh_supervisor; pub mod mesh_ws; pub mod repository_history; -pub mod settings; pub mod speak; pub mod templates; pub mod voice; diff --git a/makima/src/server/handlers/settings.rs b/makima/src/server/handlers/settings.rs deleted file mode 100644 index ae52d5a..0000000 --- a/makima/src/server/handlers/settings.rs +++ /dev/null @@ -1,196 +0,0 @@ -//! HTTP handlers for user settings (feature flags / preferences). - -use axum::{ - extract::{Path, State}, - http::StatusCode, - response::IntoResponse, - Json, -}; - -use crate::db::models::{UpsertUserSettingRequest, UserSettingsResponse}; -use crate::db::repository; -use crate::server::auth::Authenticated; -use crate::server::messages::ApiError; -use crate::server::state::SharedState; - -/// List all settings for the authenticated user. -#[utoipa::path( - get, - path = "/api/v1/settings", - responses( - (status = 200, description = "User settings", body = UserSettingsResponse), - (status = 401, description = "Not authenticated", body = ApiError), - (status = 503, description = "Database not configured", body = ApiError), - (status = 500, description = "Internal server error", body = ApiError), - ), - security( - ("bearer_auth" = []) - ), - tag = "Settings" -)] -pub async fn list_settings( - State(state): State<SharedState>, - Authenticated(user): 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::get_user_settings(pool, user.owner_id).await { - Ok(settings) => Json(UserSettingsResponse { settings }).into_response(), - Err(e) => { - tracing::error!("Failed to list user settings: {}", e); - ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", e.to_string())), - ) - .into_response() - } - } -} - -/// Get a specific setting by key. -#[utoipa::path( - get, - path = "/api/v1/settings/{key}", - params( - ("key" = String, Path, description = "Setting key") - ), - responses( - (status = 200, description = "User setting", body = crate::db::models::UserSetting), - (status = 401, description = "Not authenticated", body = ApiError), - (status = 404, description = "Setting not found", body = ApiError), - (status = 503, description = "Database not configured", body = ApiError), - (status = 500, description = "Internal server error", body = ApiError), - ), - security( - ("bearer_auth" = []) - ), - tag = "Settings" -)] -pub async fn get_setting( - State(state): State<SharedState>, - Authenticated(user): Authenticated, - Path(key): Path<String>, -) -> 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::get_user_setting(pool, user.owner_id, &key).await { - Ok(Some(setting)) => Json(setting).into_response(), - Ok(None) => ( - StatusCode::NOT_FOUND, - Json(ApiError::new("NOT_FOUND", format!("Setting '{}' not found", key))), - ) - .into_response(), - Err(e) => { - tracing::error!("Failed to get user setting: {}", e); - ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", e.to_string())), - ) - .into_response() - } - } -} - -/// Upsert a user setting (create or update). -#[utoipa::path( - put, - path = "/api/v1/settings", - request_body = UpsertUserSettingRequest, - responses( - (status = 200, description = "Setting upserted", body = crate::db::models::UserSetting), - (status = 401, description = "Not authenticated", body = ApiError), - (status = 503, description = "Database not configured", body = ApiError), - (status = 500, description = "Internal server error", body = ApiError), - ), - security( - ("bearer_auth" = []) - ), - tag = "Settings" -)] -pub async fn upsert_setting( - State(state): State<SharedState>, - Authenticated(user): Authenticated, - Json(req): Json<UpsertUserSettingRequest>, -) -> 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::upsert_user_setting(pool, user.owner_id, &req.key, &req.value).await { - Ok(setting) => Json(setting).into_response(), - Err(e) => { - tracing::error!("Failed to upsert user setting: {}", e); - ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", e.to_string())), - ) - .into_response() - } - } -} - -/// Delete a user setting by key. -#[utoipa::path( - delete, - path = "/api/v1/settings/{key}", - params( - ("key" = String, Path, description = "Setting key") - ), - responses( - (status = 200, description = "Setting deleted"), - (status = 401, description = "Not authenticated", body = ApiError), - (status = 404, description = "Setting not found", body = ApiError), - (status = 503, description = "Database not configured", body = ApiError), - (status = 500, description = "Internal server error", body = ApiError), - ), - security( - ("bearer_auth" = []) - ), - tag = "Settings" -)] -pub async fn delete_setting( - State(state): State<SharedState>, - Authenticated(user): Authenticated, - Path(key): Path<String>, -) -> 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_user_setting(pool, user.owner_id, &key).await { - Ok(true) => StatusCode::OK.into_response(), - Ok(false) => ( - StatusCode::NOT_FOUND, - Json(ApiError::new("NOT_FOUND", format!("Setting '{}' not found", key))), - ) - .into_response(), - Err(e) => { - tracing::error!("Failed to delete user setting: {}", 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 025ec85..99dc1f3 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, contract_discuss, contracts, daemon_download, directives, file_ws, files, history, listen, mesh, mesh_chat, mesh_daemon, mesh_merge, mesh_supervisor, mesh_ws, orders, repository_history, settings, speak, templates, transcript_analysis, users, versions}; +use crate::server::handlers::{api_keys, chat, contract_chat, contract_daemon, contract_discuss, contracts, daemon_download, directives, file_ws, files, history, listen, mesh, mesh_chat, mesh_daemon, mesh_merge, mesh_supervisor, mesh_ws, orders, repository_history, speak, templates, transcript_analysis, users, versions}; use crate::server::openapi::ApiDoc; use crate::server::state::SharedState; @@ -281,16 +281,7 @@ pub fn make_router(state: SharedState) -> Router { .route("/timeline", get(history::get_timeline)) // Contract type templates (built-in only) .route("/contract-types", get(templates::list_contract_types)) - // User settings (feature flags) endpoints - .route( - "/user-settings", - get(settings::list_settings).put(settings::upsert_setting), - ) - .route( - "/user-settings/{key}", - get(settings::get_setting).delete(settings::delete_setting), - ) - // Settings endpoints + // Settings endpoints (static routes first to avoid conflict with /settings/{key}) .route( "/settings/repository-history", get(repository_history::list_repository_history), @@ -303,6 +294,15 @@ pub fn make_router(state: SharedState) -> Router { "/settings/repository-history/{id}", axum::routing::delete(repository_history::delete_repository_history), ) + // User settings (feature flags / preferences) + .route( + "/settings", + get(settings::list_settings).put(settings::upsert_setting), + ) + .route( + "/settings/{key}", + get(settings::get_setting).delete(settings::delete_setting), + ) .with_state(state); let swagger = SwaggerUi::new("/swagger-ui") |
