"""HTTP and WebSocket routes for the embedded radar web UI. Every handler is a thin shell over the :class:`WebController` (which forwards to the AppWindow's existing buttons) and the :class:`RingBroadcaster` (the single frame source). There is no ownership gating: the web UI lives inside the process that already owns the hardware, so its controls are simply that process's buttons. """ from __future__ import annotations import asyncio import contextlib import logging from fastapi import APIRouter, Body, HTTPException, Request, WebSocket, WebSocketDisconnect from python_app.webui.controller import WebController from python_app.webui.streaming import RingBroadcaster logger = logging.getLogger(__name__) router = APIRouter() def _controller(request: Request) -> WebController: return request.app.state.controller @router.get("/api/status") async def get_status(request: Request) -> dict: return _controller(request).status() @router.post("/api/start") async def post_start(request: Request) -> dict: logger.info("Web UI request: start") controller = _controller(request) controller.start() return controller.status() @router.post("/api/single_capture") async def post_single_capture(request: Request) -> dict: logger.info("Web UI request: single capture") controller = _controller(request) controller.single_capture() return controller.status() @router.post("/api/stop") async def post_stop(request: Request) -> dict: logger.info("Web UI request: stop") controller = _controller(request) controller.stop() return controller.status() @router.post("/api/tmp_reference") async def post_tmp_reference(request: Request) -> dict: logger.info("Web UI request: capture temporary reference") controller = _controller(request) controller.capture_tmp_reference() return controller.status() @router.get("/api/live_settings") async def get_live_settings(request: Request) -> list: return _controller(request).current_live_settings() @router.post("/api/live_settings") async def post_live_settings(request: Request, fields: dict = Body(default={})) -> list: try: return _controller(request).apply_live_settings(fields) except ValueError as exc: logger.warning("Web UI rejected live settings update: %s", exc) raise HTTPException(status_code=400, detail=str(exc)) from exc @router.websocket("/ws") async def ws(websocket: WebSocket) -> None: """Stream frames and status to one client until it disconnects.""" await websocket.accept() broadcaster: RingBroadcaster = websocket.app.state.broadcaster queue: asyncio.Queue[dict] = asyncio.Queue(maxsize=1) broadcaster.register(queue) logger.info("Web UI client connected") try: while True: await websocket.send_json(await queue.get()) except WebSocketDisconnect: logger.info("Web UI client disconnected") finally: broadcaster.unregister(queue) with contextlib.suppress(Exception): await websocket.close()