"""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, prompts, settings, swipe # --------------------------------------------------------------------------- # 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) app.include_router(prompts.router) app.include_router(swipe.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")