"""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). """ 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 # --------------------------------------------------------------------------- # 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 # --------------------------------------------------------------------------- @asynccontextmanager async def lifespan(app: FastAPI): """Create required data directories on startup if they do not exist.""" for directory in (PROMPTS_PATH, OUTPUTS_PATH, CAMPAIGNS_PATH, CONFIG_PATH): directory.mkdir(parents=True, exist_ok=True) 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"} # --------------------------------------------------------------------------- # 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")