# #region SettingsRouter [C:5] [TYPE Module] [SEMANTICS fastapi, api, superset, logging-config-response] # @defgroup Api Module group. # # @BRIEF Provides API endpoints for managing application settings and Superset environments. # @LAYER API # @RELATION DEPENDS_ON -> [ConfigManager] # @RELATION DEPENDS_ON -> [get_config_manager] # @RELATION DEPENDS_ON -> [has_permission] # # @PRE ConfigManager is initialized and accessible. # @POST Settings are read or written via ConfigManager. # @SIDE_EFFECT Persists config changes to disk via ConfigManager. # @DATA_CONTRACT Input -> ConfigUpdateRequest, Output -> AppConfig, LoggingConfigResponse # @INVARIANT All settings changes must be persisted via ConfigManager. # @PUBLIC_API router from fastapi import APIRouter, Depends, HTTPException from pydantic import BaseModel from sqlalchemy.orm import Session from ...core.config_manager import ConfigManager from ...core.config_models import AppConfig, Environment, GlobalSettings, LoggingConfig from ...core.database import get_db from ...core.logger import belief_scope, logger from ...core.async_superset_client import AsyncSupersetClient from ...dependencies import get_config_manager, has_permission from ...models.config import AppConfigRecord from ...models.llm import ValidationPolicy from ...models.storage import StorageConfig from ...models.translate import TranslationJob, TranslationSchedule from ...schemas.settings import ( TranslationScheduleItem, ValidationPolicyCreate, ValidationPolicyResponse, ValidationPolicyUpdate, ) from ...core.connection_service import ConnectionService from ...services.llm_prompt_templates import normalize_llm_settings # #region LoggingConfigResponse [C:1] [TYPE Class] [SEMANTICS logging, config, response] # @BRIEF Response model for logging configuration with current task log level. class LoggingConfigResponse(BaseModel): level: str task_log_level: str enable_belief_state: bool # #endregion LoggingConfigResponse router = APIRouter() # #region _normalize_superset_env_url [C:1] [TYPE Function] # @BRIEF Canonicalize Superset environment URL to base host/path without trailing /api/v1. # Auto-prepends https:// if no scheme is present. # @PRE raw_url can be empty. # @POST Returns normalized base URL with scheme. def _normalize_superset_env_url(raw_url: str) -> str: normalized = str(raw_url or "").strip().rstrip("/") if normalized.lower().endswith("/api/v1"): normalized = normalized[: -len("/api/v1")] normalized = normalized.rstrip("/") # Auto-prepend https:// if no scheme is present if normalized and not normalized.startswith("http://") and not normalized.startswith("https://"): normalized = f"https://{normalized}" return normalized # #endregion _normalize_superset_env_url # #region _validate_superset_connection_fast [C:2] [TYPE Function] # @BRIEF Run lightweight Superset connectivity validation without full pagination scan. # @PRE env contains valid URL and credentials. # @POST Raises on auth/API failures; returns None on success. async def _validate_superset_connection_fast(env: Environment) -> None: client = AsyncSupersetClient(env) # 1) Explicit auth check await client.authenticate() # 2) Single lightweight API call to ensure read access _, _ = await client.get_dashboards_page( query={ "page": 0, "page_size": 1, "columns": ["id"], } ) # #endregion _validate_superset_connection_fast # #region get_settings [C:2] [TYPE Function] # @ingroup Api # @BRIEF Retrieves all application settings. # @PRE Config manager is available. # @POST Returns masked AppConfig. @router.get("", response_model=AppConfig) async def get_settings( config_manager: ConfigManager = Depends(get_config_manager), _=Depends(has_permission("admin:settings", "READ")), ): with belief_scope("get_settings"): logger.reason("Fetching all settings", extra={"src": "get_settings"}) config = config_manager.get_config().copy(deep=True) config.settings.llm = normalize_llm_settings(config.settings.llm) # Mask passwords for env in config.environments: if env.password: env.password = "********" return config # #endregion get_settings # #region get_features [C:1] [TYPE Function] # @BRIEF Public endpoint returning feature flags for frontend sidebar filtering. # @RATIONALE Unauthenticated because sidebar filtering must work for all users, not just admins. # @PRE Config manager is available. # @POST Returns dict with dataset_review and health_monitor booleans. @router.get("/features") async def get_features( config_manager: ConfigManager = Depends(get_config_manager), ): return config_manager.get_config().settings.features.model_dump() # #endregion get_features # #region get_allowed_languages [C:1] [TYPE Function] # @BRIEF Public endpoint returning allowed translation languages for language dropdowns. # @RATIONALE No auth required — needed by dictionary page, job config, etc. everywhere # language selects appear. Non-admin users still need the list. # @PRE Config manager is available. # @POST Returns list of BCP-47 language codes. @router.get("/allowed-languages") async def get_allowed_languages( config_manager: ConfigManager = Depends(get_config_manager), ): return config_manager.get_config().settings.allowed_languages # #endregion get_allowed_languages # #region update_global_settings [C:2] [TYPE Function] # @ingroup Api # @BRIEF Updates global application settings. # @PRE New settings are provided. # @POST Global settings are updated. @router.patch("/global", response_model=GlobalSettings) async def update_global_settings( settings: GlobalSettings, config_manager: ConfigManager = Depends(get_config_manager), _=Depends(has_permission("admin:settings", "WRITE")), ): with belief_scope("update_global_settings"): logger.reason("Updating global settings", extra={"src": "update_global_settings"}) config_manager.update_global_settings(settings) return settings # #endregion update_global_settings # #region get_storage_settings [C:2] [TYPE Function] # @ingroup Api # @BRIEF Retrieves storage-specific settings. @router.get("/storage", response_model=StorageConfig) async def get_storage_settings( config_manager: ConfigManager = Depends(get_config_manager), _=Depends(has_permission("admin:settings", "READ")), ): with belief_scope("get_storage_settings"): return config_manager.get_config().settings.storage # #endregion get_storage_settings # #region update_storage_settings [C:2] [TYPE Function] # @ingroup Api # @BRIEF Updates storage-specific settings. # @POST Storage settings are updated and saved. @router.put("/storage", response_model=StorageConfig) async def update_storage_settings( storage: StorageConfig, config_manager: ConfigManager = Depends(get_config_manager), _=Depends(has_permission("admin:settings", "WRITE")), ): with belief_scope("update_storage_settings"): is_valid, message = config_manager.validate_path(storage.root_path) if not is_valid: raise HTTPException(status_code=400, detail=message) settings = config_manager.get_config().settings settings.storage = storage config_manager.update_global_settings(settings) return config_manager.get_config().settings.storage # #endregion update_storage_settings # #region get_environments [C:2] [TYPE Function] # @ingroup Api # @BRIEF Lists all configured Superset environments. # @PRE Config manager is available. # @POST Returns list of environments. @router.get("/environments", response_model=list[Environment]) async def get_environments( config_manager: ConfigManager = Depends(get_config_manager), _=Depends(has_permission("admin:settings", "READ")), ): with belief_scope("get_environments"): logger.reason("Fetching environments", extra={"src": "get_environments"}) environments = config_manager.get_environments() return [ env.copy(update={"url": _normalize_superset_env_url(env.url)}) for env in environments ] # #endregion get_environments # #region add_environment [C:2] [TYPE Function] # @ingroup Api # @BRIEF Adds a new Superset environment. # @PRE Environment data is valid and reachable. # @POST Environment is added to config. @router.post("/environments", response_model=Environment) async def add_environment( env: Environment, config_manager: ConfigManager = Depends(get_config_manager), _=Depends(has_permission("admin:settings", "WRITE")), ): with belief_scope("add_environment"): logger.reason(f"Adding environment {env.id}", extra={"src": "add_environment"}) env = env.copy(update={"url": _normalize_superset_env_url(env.url)}) # Validate connection before adding (fast path) try: await _validate_superset_connection_fast(env) except Exception as e: logger.error( f"[add_environment][Coherence:Failed] Connection validation failed: {e}" ) raise HTTPException( status_code=400, detail=f"Connection validation failed: {e}" ) config_manager.add_environment(env) return env # #endregion add_environment # #region update_environment [C:2] [TYPE Function] # @ingroup Api # @BRIEF Updates an existing Superset environment. # @PRE ID and valid environment data are provided. # @POST Environment is updated in config. @router.put("/environments/{id}", response_model=Environment) async def update_environment( id: str, env: Environment, config_manager: ConfigManager = Depends(get_config_manager), ): with belief_scope("update_environment"): logger.reason(f"Updating environment {id}", extra={"src": "update_environment"}) env = env.copy(update={"url": _normalize_superset_env_url(env.url)}) # If password is masked, we need the real one for validation env_to_validate = env.copy(deep=True) if env_to_validate.password == "********": old_env = next( (e for e in config_manager.get_environments() if e.id == id), None ) if old_env: env_to_validate.password = old_env.password # Validate connection before updating (fast path) try: await _validate_superset_connection_fast(env_to_validate) except Exception as e: logger.error( f"[update_environment][Coherence:Failed] Connection validation failed: {e}" ) raise HTTPException( status_code=400, detail=f"Connection validation failed: {e}" ) if config_manager.update_environment(id, env): return env raise HTTPException(status_code=404, detail=f"Environment {id} not found") # #endregion update_environment # #region delete_environment [C:2] [TYPE Function] # @ingroup Api # @BRIEF Deletes a Superset environment. # @PRE ID is provided. # @POST Environment is removed from config. @router.delete("/environments/{id}") async def delete_environment( id: str, config_manager: ConfigManager = Depends(get_config_manager) ): with belief_scope("delete_environment"): logger.reason(f"Deleting environment {id}", extra={"src": "delete_environment"}) config_manager.delete_environment(id) return {"message": f"Environment {id} deleted"} # #endregion delete_environment # #region test_environment_connection [C:2] [TYPE Function] # @ingroup Api # @BRIEF Tests the connection to a Superset environment. # @PRE ID is provided. # @POST Returns success or error status. @router.post("/environments/{id}/test") async def test_environment_connection( id: str, config_manager: ConfigManager = Depends(get_config_manager) ): with belief_scope("test_environment_connection"): logger.reason(f"Testing environment {id}", extra={"src": "test_environment_connection"}) # Find environment env = next((e for e in config_manager.get_environments() if e.id == id), None) if not env: raise HTTPException(status_code=404, detail=f"Environment {id} not found") try: await _validate_superset_connection_fast(env) logger.info( f"[test_environment_connection][Coherence:OK] Connection successful for {id}" ) return {"status": "success", "message": "Connection successful"} except Exception as e: logger.error( f"[test_environment_connection][Coherence:Failed] Connection failed for {id}: {e}" ) return {"status": "error", "message": str(e)} # #endregion test_environment_connection # #region get_logging_config [C:2] [TYPE Function] # @ingroup Api # @BRIEF Retrieves current logging configuration. # @PRE Config manager is available. # @POST Returns logging configuration. @router.get("/logging", response_model=LoggingConfigResponse) async def get_logging_config( config_manager: ConfigManager = Depends(get_config_manager), _=Depends(has_permission("admin:settings", "READ")), ): with belief_scope("get_logging_config"): logging_config = config_manager.get_config().settings.logging return LoggingConfigResponse( level=logging_config.level, task_log_level=logging_config.task_log_level, enable_belief_state=logging_config.enable_belief_state, ) # #endregion get_logging_config # #region update_logging_config [C:2] [TYPE Function] # @ingroup Api # @BRIEF Updates logging configuration. # @PRE New logging config is provided. # @POST Logging configuration is updated and saved. @router.patch("/logging", response_model=LoggingConfigResponse) async def update_logging_config( config: LoggingConfig, config_manager: ConfigManager = Depends(get_config_manager), _=Depends(has_permission("admin:settings", "WRITE")), ): with belief_scope("update_logging_config"): logger.reason( f"Updating logging config: level={config.level}, task_log_level={config.task_log_level}", extra={"src": "update_logging_config"} ) # Get current settings and update logging config settings = config_manager.get_config().settings settings.logging = config config_manager.update_global_settings(settings) return LoggingConfigResponse( level=config.level, task_log_level=config.task_log_level, enable_belief_state=config.enable_belief_state, ) # #endregion update_logging_config # #region ConsolidatedSettingsResponse [C:1] [TYPE Class] # @BRIEF Response model for consolidated application settings. class ConsolidatedSettingsResponse(BaseModel): environments: list[dict] connections: list[dict] llm: dict llm_providers: list[dict] logging: dict storage: dict notifications: dict = {} features: dict = {} app_timezone: str = "Europe/Moscow" allowed_languages: list[str] = [] # #endregion ConsolidatedSettingsResponse # #region get_consolidated_settings [C:4] [TYPE Function] # @ingroup Api # @BRIEF Retrieves all settings categories in a single call. # @PRE Config manager is available and the caller holds admin settings read permission. # @POST Returns consolidated settings, provider metadata, and persisted notification payload in one stable response. # @SIDE_EFFECT Opens one database session to read LLM providers and config-backed notification payload, then closes it. # @DATA_CONTRACT Input[ConfigManager] -> Output[ConsolidatedSettingsResponse] # @RELATION DEPENDS_ON -> [ConfigManager] # @RELATION DEPENDS_ON -> [LLMProviderService] # @RELATION DEPENDS_ON -> [AppConfigRecord] # @RELATION DEPENDS_ON -> [SessionLocal] # @RELATION DEPENDS_ON -> [has_permission] # @RELATION DEPENDS_ON -> [normalize_llm_settings] @router.get("/consolidated", response_model=ConsolidatedSettingsResponse) async def get_consolidated_settings( config_manager: ConfigManager = Depends(get_config_manager), _=Depends(has_permission("admin:settings", "READ")), ): with belief_scope("get_consolidated_settings"): logger.reason("Fetching consolidated settings payload") config = config_manager.get_config() from ...core.database import SessionLocal from ...services.llm_provider import LLMProviderService db = SessionLocal() notifications_payload = {} try: llm_service = LLMProviderService(db) providers = llm_service.get_all_providers() llm_providers_list = [ { "id": p.id, "provider_type": p.provider_type, "name": p.name, "base_url": p.base_url, "api_key": "********", "default_model": p.default_model, "is_active": p.is_active, "is_multimodal": bool(p.is_multimodal) if p.is_multimodal is not None else False, "context_window": p.context_window, "max_output_tokens": p.max_output_tokens, } for p in providers ] config_record = ( db.query(AppConfigRecord).filter(AppConfigRecord.id == "global").first() ) if config_record and isinstance(config_record.payload, dict): notifications_payload = ( config_record.payload.get("notifications", {}) or {} ) finally: db.close() normalized_llm = normalize_llm_settings(config.settings.llm) response_payload = ConsolidatedSettingsResponse( environments=[env.model_dump() for env in config.environments], connections=[{**c.model_dump(), "password": "********"} for c in config.settings.connections], llm=normalized_llm, llm_providers=llm_providers_list, logging=config.settings.logging.model_dump(), storage=config.settings.storage.model_dump(), notifications=notifications_payload, features=config.settings.features.model_dump(), app_timezone=config.settings.app_timezone, allowed_languages=config.settings.allowed_languages, ) logger.reflect( "Consolidated settings payload assembled", extra={ "environment_count": len(response_payload.environments), "provider_count": len(response_payload.llm_providers), }, ) return response_payload # #endregion get_consolidated_settings # #region update_consolidated_settings [C:2] [TYPE Function] # @ingroup Api # @BRIEF Bulk update application settings from the consolidated view. # @PRE User has admin permissions, config is valid. # @POST Settings are updated and saved via ConfigManager. @router.patch("/consolidated") async def update_consolidated_settings( settings_patch: dict, config_manager: ConfigManager = Depends(get_config_manager), _=Depends(has_permission("admin:settings", "WRITE")), ): with belief_scope("update_consolidated_settings"): logger.reason( "Applying consolidated settings patch", extra={"src": "update_consolidated_settings"} ) current_config = config_manager.get_config() current_settings = current_config.settings # Update connections if provided if "connections" in settings_patch: cs = ConnectionService(config_manager) # Build lookup of existing connections by id to preserve encrypted passwords existing_by_id = { c.id: c.password for c in current_settings.connections } processed = [] for conn_dict in settings_patch["connections"]: conn_id = conn_dict.get("id", "") raw_pwd = conn_dict.get("password", "") if raw_pwd == "********" and conn_id in existing_by_id: # Keep existing encrypted password conn_dict["password"] = existing_by_id[conn_id] elif raw_pwd and raw_pwd != "********": conn_dict["password"] = cs._encrypt_password(raw_pwd) processed.append(conn_dict) current_settings.connections = processed # Update LLM if provided if "llm" in settings_patch: current_settings.llm = normalize_llm_settings(settings_patch["llm"]) # Update Logging if provided if "logging" in settings_patch: current_settings.logging = LoggingConfig(**settings_patch["logging"]) # Update Storage if provided if "storage" in settings_patch: new_storage = StorageConfig(**settings_patch["storage"]) is_valid, message = config_manager.validate_path(new_storage.root_path) if not is_valid: raise HTTPException(status_code=400, detail=message) current_settings.storage = new_storage # Update Features if provided if "features" in settings_patch: from ...core.config_models import FeaturesConfig current_settings.features = FeaturesConfig(**settings_patch["features"]) if "notifications" in settings_patch: payload = config_manager.get_payload() payload["notifications"] = settings_patch["notifications"] config_manager.save_config(payload) # Update app_timezone if provided if "app_timezone" in settings_patch: new_tz = settings_patch["app_timezone"] from ...core.timezone import invalidate_timezone_cache, validate_timezone if not validate_timezone(new_tz): raise HTTPException(status_code=422, detail=f"Invalid IANA timezone: {new_tz}") current_settings.app_timezone = new_tz invalidate_timezone_cache() # Update allowed_languages if provided if "allowed_languages" in settings_patch: current_settings.allowed_languages = settings_patch["allowed_languages"] config_manager.update_global_settings(current_settings) return {"status": "success", "message": "Settings updated"} # #endregion update_consolidated_settings # #region get_validation_policies [C:2] [TYPE Function] # @ingroup Api # @BRIEF Lists all validation policies. @router.get("/automation/policies", response_model=list[ValidationPolicyResponse]) async def get_validation_policies( db: Session = Depends(get_db), _=Depends(has_permission("admin:settings", "READ")) ): with belief_scope("get_validation_policies"): return db.query(ValidationPolicy).all() # #endregion get_validation_policies # #region create_validation_policy [C:2] [TYPE Function] # @ingroup Api # @BRIEF Creates a new validation policy. @router.post("/automation/policies", response_model=ValidationPolicyResponse) async def create_validation_policy( policy: ValidationPolicyCreate, db: Session = Depends(get_db), _=Depends(has_permission("admin:settings", "WRITE")), ): with belief_scope("create_validation_policy"): db_policy = ValidationPolicy(**policy.model_dump()) db.add(db_policy) db.commit() db.refresh(db_policy) return db_policy # #endregion create_validation_policy # #region update_validation_policy [C:2] [TYPE Function] # @ingroup Api # @BRIEF Updates an existing validation policy. @router.patch("/automation/policies/{id}", response_model=ValidationPolicyResponse) async def update_validation_policy( id: str, policy: ValidationPolicyUpdate, db: Session = Depends(get_db), _=Depends(has_permission("admin:settings", "WRITE")), ): with belief_scope("update_validation_policy"): db_policy = db.query(ValidationPolicy).filter(ValidationPolicy.id == id).first() if not db_policy: raise HTTPException(status_code=404, detail="Policy not found") update_data = policy.dict(exclude_unset=True) for key, value in update_data.items(): setattr(db_policy, key, value) db.commit() db.refresh(db_policy) return db_policy # #endregion update_validation_policy # #region delete_validation_policy [C:2] [TYPE Function] # @ingroup Api # @BRIEF Deletes a validation policy. @router.delete("/automation/policies/{id}") async def delete_validation_policy( id: str, db: Session = Depends(get_db), _=Depends(has_permission("admin:settings", "WRITE")), ): with belief_scope("delete_validation_policy"): db_policy = db.query(ValidationPolicy).filter(ValidationPolicy.id == id).first() if not db_policy: raise HTTPException(status_code=404, detail="Policy not found") db.delete(db_policy) db.commit() return {"message": "Policy deleted"} # #endregion delete_validation_policy # #region get_translation_schedules [C:2] [TYPE Function] # @ingroup Api # @BRIEF Lists all translation schedules joined with translation job names. @router.get("/automation/translation-schedules", response_model=list[TranslationScheduleItem]) async def get_translation_schedules( db: Session = Depends(get_db), _=Depends(has_permission("admin:settings", "READ")), ): with belief_scope("get_translation_schedules"): results = ( db.query( TranslationSchedule, TranslationJob.name.label("job_name"), ) .outerjoin(TranslationJob, TranslationSchedule.job_id == TranslationJob.id) .all() ) return [ TranslationScheduleItem( schedule_id=ts.id, job_id=ts.job_id, job_name=job_name, cron_expression=ts.cron_expression, timezone=ts.timezone, is_active=ts.is_active, last_run_at=ts.last_run_at, next_run_at=ts.next_run_at, created_by=ts.created_by, created_at=ts.created_at, ) for ts, job_name in results ] # #endregion get_translation_schedules # #region Connection CRUD Endpoints 🆕 [C:3] [SEMANTICS settings,connections,crud] # @ingroup Api # @BRIEF CRUD and test endpoints for DatabaseConnection configurations. # @PRE ConnectionService is initialized with ConfigManager. # @POST Connection CRUD reflected in GlobalSettings.connections. Passwords encrypted at rest. # @SIDE_EFFECT Reads/writes GlobalSettings via ConnectionService. Opens/closes DB connections during test. # @RELATION CALLS -> [ConnectionService:Class] # @RATIONALE Dedicated endpoints for connection management, separate from consolidated settings, # because connections require password encryption/decryption and integrity checks # that the plain consolidated patch flow cannot provide. # # @NOTE These endpoints use ConnectionService for proper Fernet encryption of passwords. # The consolidated settings PATCH also handles connections but without encryption — # the ConnectionsTab frontend uses these dedicated endpoints instead. # #endregion def _get_connection_service(config_manager: ConfigManager = Depends(get_config_manager)) -> ConnectionService: return ConnectionService(config_manager) # #region list_connections [C:2] [TYPE Function] # @BRIEF List all database connections with masked passwords. @router.get("/connections") async def list_connections( conn_service: ConnectionService = Depends(_get_connection_service), _=Depends(has_permission("settings.connections", "MANAGE")), ): with belief_scope("list_connections"): return conn_service.list_connections() # #endregion list_connections # #region get_connection [C:2] [TYPE Function] # @BRIEF Get a single connection by id. Password is masked in response. @router.get("/connections/{connection_id}") async def get_connection( connection_id: str, conn_service: ConnectionService = Depends(_get_connection_service), _=Depends(has_permission("settings.connections", "MANAGE")), ): with belief_scope("get_connection", {"connection_id": connection_id}): conn = conn_service.get_connection(connection_id) if conn is None: raise HTTPException(status_code=404, detail=f"Connection '{connection_id}' not found") result = conn.model_dump() result["password"] = "********" result["used_by"] = conn_service._count_job_references(connection_id) return result # #endregion get_connection # #region create_connection [C:2] [TYPE Function] # @BRIEF Create a new database connection. Password is encrypted before storage. @router.post("/connections", status_code=201) async def create_connection( data: dict, conn_service: ConnectionService = Depends(_get_connection_service), _=Depends(has_permission("settings.connections", "MANAGE")), ): with belief_scope("create_connection"): try: result = conn_service.create_connection(data) logger.reflect("Connection created", {"name": data.get("name")}) return result except ValueError as e: raise HTTPException(status_code=422, detail=str(e)) # #endregion create_connection # #region update_connection [C:2] [TYPE Function] # @BRIEF Update an existing connection. Empty/masked password = keep existing. @router.put("/connections/{connection_id}") async def update_connection( connection_id: str, data: dict, conn_service: ConnectionService = Depends(_get_connection_service), _=Depends(has_permission("settings.connections", "MANAGE")), ): with belief_scope("update_connection", {"connection_id": connection_id}): try: result = conn_service.update_connection(connection_id, data) logger.reflect("Connection updated", {"connection_id": connection_id}) return result except ValueError as e: raise HTTPException(status_code=422, detail=str(e)) # #endregion update_connection # #region delete_connection [C:2] [TYPE Function] # @BRIEF Delete a connection. Blocked if referenced by active TranslationJobs. @router.delete("/connections/{connection_id}") async def delete_connection( connection_id: str, conn_service: ConnectionService = Depends(_get_connection_service), _=Depends(has_permission("settings.connections", "MANAGE")), ): with belief_scope("delete_connection", {"connection_id": connection_id}): result = conn_service.delete_connection(connection_id) if not result["success"]: raise HTTPException( status_code=409, detail={ "message": "Connection is in use by active jobs", "blocking_jobs": result.get("blocking_jobs", []), }, ) logger.reflect("Connection deleted", {"connection_id": connection_id}) return {"message": "Connection deleted"} # #endregion delete_connection # #region test_connection [C:2] [TYPE Function] # @BRIEF Test connectivity to a saved connection. Runs SELECT 1 and returns latency + version or error. @router.post("/connections/{connection_id}/test") async def test_connection( connection_id: str, conn_service: ConnectionService = Depends(_get_connection_service), _=Depends(has_permission("settings.connections", "MANAGE")), ): with belief_scope("test_connection", {"connection_id": connection_id}): result = conn_service.test_connection(connection_id) logger.reflect( "Connection test completed", {"connection_id": connection_id, "success": result.get("success")}, ) return result # #endregion test_connection # #endregion SettingsRouter