import json from typing import Any from fastapi import APIRouter, Request, Response from fastapi.responses import FileResponse, HTMLResponse, RedirectResponse from resonance_engine.api import security from resonance_engine.config import settings router = APIRouter(include_in_schema=False) def read_manifest() -> dict[str, Any]: """Return the Vite manifest, or {} if the dashboard hasn't been built yet.""" for candidate in [ settings.dashboard_dir / ".vite" / "manifest.json", # Vite 5+ settings.dashboard_dir / "manifest.json", # Vite 4 ]: if candidate.exists(): return json.loads(candidate.read_text()) return {} def _not_built() -> HTMLResponse: return HTMLResponse( "

503 — Dashboard not built

" "

Run make build-dashboard and restart the server.

", status_code=503, ) def _serve_dashboard(full_path: str, request: Request) -> Response: if not settings.dashboard_dir.exists(): return _not_built() if full_path: candidate = settings.dashboard_dir / full_path if candidate.is_file(): return FileResponse(candidate) if not security.is_authenticated(request) and full_path != "login": return RedirectResponse("/dashboard/login") index = settings.dashboard_dir / "index.html" if not index.exists(): return _not_built() # index.html must not be cached — it contains hashed asset URLs that change # on every build. Hashed assets themselves are immutably cacheable by default. return FileResponse( index, headers={"Cache-Control": "no-cache, no-store, must-revalidate"}, ) # response_model=None: return type is a Starlette Response subclass, not a Pydantic model @router.get("/dashboard", response_model=None) async def serve_dashboard_root(request: Request) -> Response: return _serve_dashboard("", request) @router.get("/dashboard/{full_path:path}", response_model=None) async def serve_dashboard_spa(request: Request, full_path: str) -> Response: return _serve_dashboard(full_path, request)