"""Settings routes — admin-editable runtime configuration.""" from fastapi import APIRouter, Depends, HTTPException from pydantic import BaseModel from typing import Any from core import data_manager from core import settings_manager as settings from core.scheduling_engine import normalize_time_str from api.deps import require_admin_session router = APIRouter(prefix="/api/settings", tags=["settings"]) class SettingsUpdate(BaseModel): min_lead_time_hours: int | None = None buffer_minutes: int | None = None visit_levels: dict | None = None on_call_start_hour: str | None = None region_name: str | None = None timezone: str | None = None service_area: str | None = None openai_model: str | None = None def _audit(user: str, detail: str) -> None: data_manager.log_action(user, "UPDATE_SETTING", detail) @router.get("") def get_settings(session: dict = Depends(require_admin_session)): """Return current effective values plus the per-key defaults.""" return { "current": settings.get_all_settings(), "defaults": settings.get_schema_defaults(), } @router.put("") def update_settings(req: SettingsUpdate, session: dict = Depends(require_admin_session)): current = settings.get_all_settings() changes: list[str] = [] if req.min_lead_time_hours is not None and req.min_lead_time_hours != current["min_lead_time_hours"]: if req.min_lead_time_hours < 0 or req.min_lead_time_hours > 168: raise HTTPException(status_code=400, detail="min_lead_time_hours must be 0-168") settings.set_setting("min_lead_time_hours", int(req.min_lead_time_hours)) changes.append(f"min_lead_time_hours: {current['min_lead_time_hours']} → {req.min_lead_time_hours}") if req.buffer_minutes is not None and req.buffer_minutes != current["buffer_minutes"]: if req.buffer_minutes < 0 or req.buffer_minutes > 120: raise HTTPException(status_code=400, detail="buffer_minutes must be 0-120") settings.set_setting("buffer_minutes", int(req.buffer_minutes)) changes.append(f"buffer_minutes: {current['buffer_minutes']} → {req.buffer_minutes}") if req.on_call_start_hour is not None and req.on_call_start_hour != current["on_call_start_hour"]: normalized = normalize_time_str(req.on_call_start_hour) if normalized is None: raise HTTPException(status_code=400, detail="on_call_start_hour must be HH:MM (e.g. 07:00)") settings.set_setting("on_call_start_hour", normalized) changes.append(f"on_call_start_hour: {current['on_call_start_hour']} → {normalized}") if req.visit_levels is not None and req.visit_levels != current["visit_levels"]: # Validate shape: each value must have 'label' and integer 'duration_minutes'. for k, v in req.visit_levels.items(): if not isinstance(v, dict) or "duration_minutes" not in v: raise HTTPException(status_code=400, detail=f"visit_levels[{k}] missing duration_minutes") try: int(v["duration_minutes"]) except Exception: raise HTTPException(status_code=400, detail=f"visit_levels[{k}].duration_minutes not an integer") # Re-key: convert digit-string keys back to ints so the format matches the schema. normalized_levels = { (int(k) if isinstance(k, str) and k.isdigit() else k): { "label": v.get("label", str(k)), "duration_minutes": int(v["duration_minutes"]), } for k, v in req.visit_levels.items() } settings.set_setting("visit_levels", normalized_levels) changes.append("visit_levels updated") if req.region_name is not None and req.region_name != current["region_name"]: settings.set_setting("region_name", req.region_name) changes.append(f"region_name: {current['region_name']} → {req.region_name}") if req.timezone is not None and req.timezone != current["timezone"]: settings.set_setting("timezone", req.timezone) changes.append(f"timezone: {current['timezone']} → {req.timezone}") if req.service_area is not None and req.service_area != current["service_area"]: settings.set_setting("service_area", req.service_area) changes.append(f"service_area: {current['service_area']} → {req.service_area}") if req.openai_model is not None and req.openai_model != current["openai_model"]: settings.set_setting("openai_model", req.openai_model) changes.append(f"openai_model: {current['openai_model']} → {req.openai_model}") if changes: _audit(session["current_user"], "; ".join(changes)) return {"changes": changes, "current": settings.get_all_settings()} @router.post("/reset") def reset_to_defaults(session: dict = Depends(require_admin_session)): defaults = settings.get_schema_defaults() for key, default in defaults.items(): settings.set_setting(key, default) _audit(session["current_user"], "Reset all settings to defaults") return {"current": settings.get_all_settings()}