Files
Cloud-Tour-to-Libo/app/main.py
T

101 lines
3.2 KiB
Python

"""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, openapi_router
from app.api.mcp_server import router as mcp_router
from app.db import init_pool, close_pool
from app.data_platform.schema import ensure_platform_registry
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.
"""
@staticmethod
def _without_conditional_cache_headers(scope):
"""Force SPA entry points to return the current index.html body.
StaticFiles normally honours If-None-Match/If-Modified-Since. That is
useful for content-hashed assets, but a cached index.html can otherwise
keep pointing at an obsolete JavaScript bundle after a deployment.
"""
fresh_scope = dict(scope)
fresh_scope["headers"] = [
(name, value)
for name, value in scope.get("headers", [])
if name.lower() not in {b"if-none-match", b"if-modified-since"}
]
return fresh_scope
@staticmethod
def _set_cache_policy(response, *, spa_entry: bool):
if spa_entry:
response.headers["Cache-Control"] = "no-cache, no-store, must-revalidate"
response.headers["Pragma"] = "no-cache"
response.headers["Expires"] = "0"
else:
response.headers["Cache-Control"] = "public, max-age=31536000, immutable"
return response
async def get_response(self, path: str, scope):
spa_entry = path in {"", ".", "index.html"}
request_scope = (
self._without_conditional_cache_headers(scope) if spa_entry else scope
)
try:
response = await super().get_response(path, request_scope)
except StarletteHTTPException as exc:
if exc.status_code == 404:
spa_entry = True
response = await super().get_response(
"index.html", self._without_conditional_cache_headers(scope)
)
else:
raise
return self._set_cache_policy(response, spa_entry=spa_entry)
@asynccontextmanager
async def lifespan(_app: FastAPI):
await init_pool()
await ensure_platform_registry()
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)
app.include_router(openapi_router)
app.include_router(mcp_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