- schemas/settings.py: Settings pydantic model con api_key, llm_model, nicchie_attive, tono
- routers/calendar.py: POST /api/calendar/generate, GET /api/calendar/formats
- routers/generate.py: POST /api/generate/bulk (202 + job_id), GET /job/{job_id}/status (polling), GET /job/{job_id}, POST /single
- routers/export.py: GET /api/export/{job_id}/csv (originale), POST /api/export/{job_id}/csv (modifiche inline)
- routers/settings.py: GET /api/settings/status (api_key_configured), GET /api/settings, PUT /api/settings
- main.py: include_router x4 PRIMA di SPAStaticFiles, copia prompt default al primo avvio
164 lines
5.2 KiB
Python
164 lines
5.2 KiB
Python
"""Router per la gestione della configurazione applicativa.
|
|
|
|
Endpoint:
|
|
- GET /api/settings — ritorna la configurazione corrente (api_key mascherata)
|
|
- PUT /api/settings — salva la configurazione aggiornata
|
|
- GET /api/settings/status — ritorna se l'api_key è configurata (per abilitare/disabilitare UI)
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import logging
|
|
from typing import Optional
|
|
|
|
from fastapi import APIRouter
|
|
from pydantic import BaseModel
|
|
|
|
from backend.config import CONFIG_PATH
|
|
from backend.schemas.settings import Settings
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
router = APIRouter(prefix="/api/settings", tags=["settings"])
|
|
|
|
# Percorso al file di configurazione
|
|
_SETTINGS_FILE = CONFIG_PATH / "settings.json"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Response schemas
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class SettingsStatusResponse(BaseModel):
|
|
"""Risposta per GET /status — usata dal frontend per abilitare/disabilitare il pulsante genera."""
|
|
api_key_configured: bool
|
|
llm_model: str
|
|
|
|
|
|
class SettingsResponse(BaseModel):
|
|
"""Risposta per GET / — api_key mascherata per sicurezza."""
|
|
api_key_masked: Optional[str] # Solo ultimi 4 caratteri o None
|
|
llm_model: str
|
|
nicchie_attive: list[str]
|
|
lingua: str
|
|
frequenza_post: int
|
|
brand_name: Optional[str]
|
|
tono: Optional[str]
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def _load_settings() -> Settings:
|
|
"""Carica le impostazioni da disco. Ritorna i default se il file non esiste."""
|
|
if not _SETTINGS_FILE.exists():
|
|
return Settings()
|
|
|
|
try:
|
|
data = json.loads(_SETTINGS_FILE.read_text(encoding="utf-8"))
|
|
return Settings.model_validate(data)
|
|
except Exception as e:
|
|
logger.warning("Errore caricamento settings: %s — uso default", str(e))
|
|
return Settings()
|
|
|
|
|
|
def _save_settings(settings: Settings) -> None:
|
|
"""Salva le impostazioni su disco."""
|
|
_SETTINGS_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
_SETTINGS_FILE.write_text(
|
|
settings.model_dump_json(indent=2),
|
|
encoding="utf-8",
|
|
)
|
|
|
|
|
|
def _mask_api_key(api_key: Optional[str]) -> Optional[str]:
|
|
"""Maschera la chiave API mostrando solo gli ultimi 4 caratteri.
|
|
|
|
Es: "sk-ant-api03-abc...xyz1234" -> "...1234"
|
|
"""
|
|
if not api_key:
|
|
return None
|
|
if len(api_key) <= 4:
|
|
return "****"
|
|
return f"...{api_key[-4:]}"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Endpoint
|
|
# ---------------------------------------------------------------------------
|
|
|
|
@router.get("/status", response_model=SettingsStatusResponse)
|
|
async def get_settings_status() -> SettingsStatusResponse:
|
|
"""Ritorna lo stato della configurazione essenziale.
|
|
|
|
Usato dal frontend per abilitare/disabilitare il pulsante "Genera".
|
|
|
|
Returns:
|
|
SettingsStatusResponse con api_key_configured e llm_model.
|
|
"""
|
|
settings = _load_settings()
|
|
return SettingsStatusResponse(
|
|
api_key_configured=bool(settings.api_key),
|
|
llm_model=settings.llm_model,
|
|
)
|
|
|
|
|
|
@router.get("/", response_model=SettingsResponse)
|
|
async def get_settings() -> SettingsResponse:
|
|
"""Ritorna la configurazione corrente con api_key mascherata.
|
|
|
|
La chiave API non viene mai inviata al frontend in chiaro —
|
|
solo gli ultimi 4 caratteri vengono mostrati per conferma.
|
|
|
|
Returns:
|
|
SettingsResponse con tutti i parametri configurabili.
|
|
"""
|
|
settings = _load_settings()
|
|
return SettingsResponse(
|
|
api_key_masked=_mask_api_key(settings.api_key),
|
|
llm_model=settings.llm_model,
|
|
nicchie_attive=settings.nicchie_attive,
|
|
lingua=settings.lingua,
|
|
frequenza_post=settings.frequenza_post,
|
|
brand_name=settings.brand_name,
|
|
tono=settings.tono,
|
|
)
|
|
|
|
|
|
@router.put("/", response_model=SettingsResponse)
|
|
async def update_settings(new_settings: Settings) -> SettingsResponse:
|
|
"""Aggiorna e salva la configurazione.
|
|
|
|
Nota: se api_key è None nel body, la chiave esistente viene mantenuta
|
|
(evita di sovrascrivere accidentalmente la chiave quando si aggiornano
|
|
altri parametri senza inviare la chiave).
|
|
|
|
Args:
|
|
new_settings: Nuova configurazione da salvare.
|
|
|
|
Returns:
|
|
SettingsResponse aggiornata con api_key mascherata.
|
|
"""
|
|
# Carica settings esistenti per merger (non sovrascrive api_key con None)
|
|
existing = _load_settings()
|
|
|
|
# Se la nuova api_key è None, mantieni quella esistente
|
|
if new_settings.api_key is None:
|
|
new_settings = new_settings.model_copy(update={"api_key": existing.api_key})
|
|
|
|
_save_settings(new_settings)
|
|
logger.info("Settings aggiornate | model=%s | brand=%s", new_settings.llm_model, new_settings.brand_name)
|
|
|
|
return SettingsResponse(
|
|
api_key_masked=_mask_api_key(new_settings.api_key),
|
|
llm_model=new_settings.llm_model,
|
|
nicchie_attive=new_settings.nicchie_attive,
|
|
lingua=new_settings.lingua,
|
|
frequenza_post=new_settings.frequenza_post,
|
|
brand_name=new_settings.brand_name,
|
|
tono=new_settings.tono,
|
|
)
|