"""FastAPI application factory for the embedded radar web UI. The controller (the Qt bridge that forwards to the AppWindow) is created and owned by the host process and injected here. The app's only owned resource is the :class:`RingBroadcaster` polling task, created and torn down by the lifespan. The static single-page frontend is mounted at ``/`` and the JSON/WS API under ``/api`` and ``/ws``. """ from __future__ import annotations from contextlib import asynccontextmanager from pathlib import Path from fastapi import FastAPI from fastapi.staticfiles import StaticFiles from python_app.webui.controller import WebController from python_app.webui.routes import router from python_app.webui.streaming import RingBroadcaster _STATIC_DIR = Path(__file__).resolve().parent / "static" def create_app(controller: WebController) -> FastAPI: """Build the FastAPI app that serves and streams for ``controller``.""" @asynccontextmanager async def lifespan(app: FastAPI): broadcaster = RingBroadcaster(controller) app.state.controller = controller app.state.broadcaster = broadcaster broadcaster.start() try: yield finally: await broadcaster.stop() app = FastAPI(title="Radar Web UI", lifespan=lifespan) app.include_router(router) # Mount the SPA last so the API routes above always take precedence. app.mount("/", StaticFiles(directory=_STATIC_DIR, html=True), name="static") return app