- 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
104 lines
3.7 KiB
Python
104 lines
3.7 KiB
Python
"""PostGenerator FastAPI application.
|
|
|
|
IMPORTANT: root_path is intentionally NOT set in the FastAPI() constructor.
|
|
It is passed only via Uvicorn's --root-path flag at runtime to avoid the
|
|
double-path bug (Pitfall #4).
|
|
"""
|
|
|
|
import shutil
|
|
from contextlib import asynccontextmanager
|
|
from pathlib import Path
|
|
|
|
from fastapi import FastAPI
|
|
from fastapi.responses import FileResponse
|
|
from fastapi.staticfiles import StaticFiles
|
|
|
|
from backend.config import CAMPAIGNS_PATH, CONFIG_PATH, OUTPUTS_PATH, PROMPTS_PATH
|
|
from backend.routers import calendar, export, generate, settings
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# SPA static files with catch-all fallback
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class SPAStaticFiles(StaticFiles):
|
|
"""Serve a React SPA: return index.html for any 404 so the client-side
|
|
router handles unknown paths instead of returning a 404 from the server."""
|
|
|
|
async def get_response(self, path: str, scope):
|
|
try:
|
|
return await super().get_response(path, scope)
|
|
except Exception:
|
|
# Fall back to index.html for client-side routing
|
|
return await super().get_response("index.html", scope)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Startup / shutdown lifecycle
|
|
# ---------------------------------------------------------------------------
|
|
|
|
# Directory dei prompt di default (inclusa nel source)
|
|
_DEFAULT_PROMPTS_DIR = Path(__file__).parent / "data" / "prompts"
|
|
|
|
|
|
@asynccontextmanager
|
|
async def lifespan(app: FastAPI):
|
|
"""Create required data directories on startup if they do not exist.
|
|
|
|
Also copies default prompts to PROMPTS_PATH on first run (when empty).
|
|
"""
|
|
# Crea directory dati
|
|
for directory in (PROMPTS_PATH, OUTPUTS_PATH, CAMPAIGNS_PATH, CONFIG_PATH):
|
|
directory.mkdir(parents=True, exist_ok=True)
|
|
|
|
# Copia prompt default al primo avvio (se PROMPTS_PATH è vuota)
|
|
if _DEFAULT_PROMPTS_DIR.exists() and not any(PROMPTS_PATH.glob("*.txt")):
|
|
for prompt_file in _DEFAULT_PROMPTS_DIR.glob("*.txt"):
|
|
dest = PROMPTS_PATH / prompt_file.name
|
|
if not dest.exists():
|
|
shutil.copy2(prompt_file, dest)
|
|
|
|
yield
|
|
# Nothing to clean up on shutdown
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Application
|
|
# ---------------------------------------------------------------------------
|
|
|
|
# CRITICAL: Do NOT pass root_path here — use Uvicorn --root-path instead.
|
|
app = FastAPI(
|
|
title="PostGenerator",
|
|
description="Instagram carousel post generator powered by Claude",
|
|
version="0.1.0",
|
|
lifespan=lifespan,
|
|
# root_path is intentionally absent — set via Uvicorn --root-path at runtime
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# API routes (must be registered BEFORE the SPA catch-all mount)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
@app.get("/api/health")
|
|
async def health() -> dict:
|
|
"""Health check endpoint."""
|
|
return {"status": "ok"}
|
|
|
|
|
|
# Include all API routers — ORDER MATTERS (registered before SPA catch-all)
|
|
app.include_router(calendar.router)
|
|
app.include_router(generate.router)
|
|
app.include_router(export.router)
|
|
app.include_router(settings.router)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# SPA catch-all mount (MUST be last — catches everything not matched above)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
_STATIC_DIR = Path(__file__).parent.parent / "static"
|
|
|
|
if _STATIC_DIR.exists():
|
|
app.mount("/", SPAStaticFiles(directory=str(_STATIC_DIR), html=True), name="spa")
|