import asyncio import logging import sys from dotenv import load_dotenv load_dotenv() logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(name)s] %(levelname)s: %(message)s") log = logging.getLogger("app") # psycopg (used by LangGraph checkpointer) requires SelectorEventLoop on Windows if sys.platform == "win32": asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) from contextlib import asynccontextmanager from fastapi import FastAPI from backend.checkpointer import init_checkpointer from backend.db.connection import init_db_pool from backend.graph.graph import build_graph from backend.routers import auth, batches, instructor, interview, sessions, student, topics, upload @asynccontextmanager async def lifespan(app: FastAPI): log.info("Starting up — connecting to DB...") await init_db_pool() log.info("DB pool ready") checkpointer = await init_checkpointer() log.info("LangGraph checkpointer ready") app.state.graph = build_graph(checkpointer) log.info("Interview graph compiled") yield log.info("Shutting down") app = FastAPI(lifespan=lifespan) app.include_router(auth.router, prefix="/api/auth") app.include_router(batches.router, prefix="/api/batches") app.include_router(topics.router, prefix="/api/topics") app.include_router(upload.router, prefix="/api/upload") app.include_router(student.router, prefix="/api/student") app.include_router(sessions.router, prefix="/api/sessions") app.include_router(instructor.router, prefix="/api/instructor") app.include_router(interview.router, prefix="/interview") # React static build — MUST be last (serves the SPA for all non-API routes) from pathlib import Path from fastapi.staticfiles import StaticFiles from fastapi.responses import FileResponse _dist = Path(__file__).resolve().parent.parent / "frontend" / "dist" if _dist.is_dir(): # Catch-all for React Router — serve index.html for any path that isn't a real file @app.get("/{full_path:path}", include_in_schema=False) async def spa_fallback(full_path: str): file = _dist / full_path if file.is_file(): return FileResponse(str(file)) return FileResponse(str(_dist / "index.html")) app.mount("/", StaticFiles(directory=str(_dist)), name="static") log.info("Static files mounted from %s", _dist) else: log.warning("frontend/dist not found at %s — SPA will not be served", _dist)