from __future__ import annotations import logging from typing import Literal from fastapi import APIRouter, Depends, HTTPException, Request, status from pydantic import BaseModel, Field from redis.asyncio import Redis from app.config import RUNTIME_SETTINGS_REDIS_KEY, RuntimeSettings, Settings from app.core.ip_utils import extract_client_ip from app.dependencies import get_redis, get_runtime_settings, get_settings, require_admin logger = logging.getLogger(__name__) router = APIRouter(prefix="/admin/api/settings", tags=["settings"], dependencies=[Depends(require_admin)]) class SettingsResponse(BaseModel): alert_webhook_url: str | None = None alert_threshold_count: int = Field(ge=1) alert_threshold_seconds: int = Field(ge=1) archive_days: int = Field(ge=1) failsafe_mode: Literal["open", "closed"] class SettingsUpdateRequest(SettingsResponse): pass def serialize_runtime_settings(runtime_settings: RuntimeSettings) -> dict[str, str]: return { "alert_webhook_url": runtime_settings.alert_webhook_url or "", "alert_threshold_count": str(runtime_settings.alert_threshold_count), "alert_threshold_seconds": str(runtime_settings.alert_threshold_seconds), "archive_days": str(runtime_settings.archive_days), "failsafe_mode": runtime_settings.failsafe_mode, } @router.get("", response_model=SettingsResponse) async def get_runtime_config( runtime_settings: RuntimeSettings = Depends(get_runtime_settings), ) -> SettingsResponse: return SettingsResponse(**runtime_settings.model_dump()) @router.put("", response_model=SettingsResponse) async def update_runtime_config( payload: SettingsUpdateRequest, request: Request, settings: Settings = Depends(get_settings), redis: Redis | None = Depends(get_redis), ): if redis is None: raise HTTPException( status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail="Settings persistence is unavailable because Redis is offline.", ) updated = RuntimeSettings(**payload.model_dump()) try: await redis.hset(RUNTIME_SETTINGS_REDIS_KEY, mapping=serialize_runtime_settings(updated)) except Exception as exc: raise HTTPException( status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail="Failed to persist runtime settings.", ) from exc async with request.app.state.runtime_settings_lock: request.app.state.runtime_settings = updated logger.info( "Runtime settings updated.", extra={"client_ip": extract_client_ip(request, settings)}, ) return SettingsResponse(**updated.model_dump())