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)