"""City Knowledge Graph Admin — FastAPI entry point.""" from __future__ import annotations from contextlib import asynccontextmanager from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from fastapi.staticfiles import StaticFiles from starlette.exceptions import HTTPException as StarletteHTTPException from app.api import api_router from app.db import init_pool, close_pool class SPAStaticFiles(StaticFiles): """Serve the admin SPA with client-side-routing fallback. Any unknown path under /admin (e.g. /admin/login-v2, /admin/plaza/graph) falls back to index.html so React Router can handle it on a hard navigation or page refresh — instead of returning a 404. """ async def get_response(self, path: str, scope): try: return await super().get_response(path, scope) except StarletteHTTPException as exc: if exc.status_code == 404: return await super().get_response("index.html", scope) raise @asynccontextmanager async def lifespan(_app: FastAPI): await init_pool() yield await close_pool() app = FastAPI( title="ZN-KG Admin", version="0.1.0", lifespan=lifespan, ) app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) app.include_router(api_router) # Serve built admin-web SPA (when available) try: app.mount("/admin", SPAStaticFiles(directory="app/static/admin", html=True), name="admin-web") except Exception: pass