from contextlib import asynccontextmanager
from pathlib import Path
from fastapi import FastAPI
from fastapi.responses import FileResponse
from fastapi.staticfiles import StaticFiles
from app.api import api_router
from app.core.config import load_dotenv_file
from app.core.database import init_database
from app.core.middleware import (
ApiVersionAliasMiddleware,
PasswordChangeGateMiddleware,
RequestContextMiddleware,
)
from app.services.chat_runtime import chat_runtime
from app.version import __version__
load_dotenv_file()
BASE_DIR = Path(__file__).resolve().parent.parent
DIST_DIR = BASE_DIR / "dist"
@asynccontextmanager
async def lifespan(_app: FastAPI):
init_database()
await chat_runtime.start()
try:
yield
finally:
await chat_runtime.stop()
def create_app() -> FastAPI:
app = FastAPI(title="ComPilot Scan", version=__version__, lifespan=lifespan)
app.add_middleware(PasswordChangeGateMiddleware)
app.add_middleware(RequestContextMiddleware)
app.add_middleware(ApiVersionAliasMiddleware)
app.include_router(api_router)
if (DIST_DIR / "assets").is_dir():
app.mount("/assets", StaticFiles(directory=DIST_DIR / "assets"), name="assets")
@app.get("/")
async def serve_index():
index = DIST_DIR / "index.html"
if index.is_file():
return FileResponse(index)
return {
"message": "ComPilot Scan API",
"docs": "/docs",
"hint": "cd web && npm run build",
}
@app.get("/{path:path}")
async def spa_fallback(path: str):
if path.startswith("api") or path in ("docs", "openapi.json", "redoc"):
return {"detail": "Not Found"}
file_path = (DIST_DIR / path).resolve()
if file_path.is_file() and DIST_DIR in file_path.parents:
return FileResponse(file_path)
index = DIST_DIR / "index.html"
if index.is_file():
return FileResponse(index)
return {"detail": "Not Found"}
return app
app = create_app()