haiku.rag/haiku_rag_slim/haiku/rag/ingester/api/routes/dashboard.py
Tres Seaver b78f0ae9ed
feat(ingester): serve control plane under a configurable base path
Add `ingester.api.root_path` so the HTTP control plane (dashboard + API)
can be reverse-proxied behind a sub-path (e.g. /ingester/) on a shared
origin, instead of needing nginx sub_filter URL-rewriting.

- APIConfig.root_path: normalized ('', or single leading slash, no trailing
  slash) via a field_validator; validate_assignment so CLI overrides
  normalize the same way as config-file values.
- Forwarded to FastAPI(root_path=) and uvicorn.Config(root_path=) so
  OpenAPI/docs links are prefix-aware.
- Dashboard route injects a <base href> matching root_path; all dashboard
  fetches are now base-relative, so they resolve under the prefix while
  staying identical at the root.
- `serve --root-path` CLI flag.
- Docs: "Behind a reverse proxy" section with an nginx example.

Closes #431

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 23:02:43 -04:00

26 lines
1.1 KiB
Python

from pathlib import Path
from fastapi import APIRouter, Request
from fastapi.responses import HTMLResponse
router = APIRouter(tags=["dashboard"])
_INDEX_HTML_PATH = Path(__file__).resolve().parent.parent / "static" / "index.html"
_INDEX_HTML = _INDEX_HTML_PATH.read_text(encoding="utf-8")
@router.get("/", include_in_schema=False)
async def dashboard(request: Request) -> HTMLResponse:
"""Serve the operator dashboard. Static page that polls /health, /sources,
/stats and /jobs from the browser. Auth happens via the bearer header the
JS attaches to its fetches — the dashboard route itself is unauthenticated
so an operator can open it in a browser and paste the token on demand.
A ``<base href>`` matching the server's ``root_path`` is injected so the
page's relative fetches resolve correctly whether the control plane is
served at the root or reverse-proxied under a sub-path (e.g. ``/ingester/``).
"""
root_path = request.scope.get("root_path", "")
base_href = f"{root_path}/" if root_path else "/"
html = _INDEX_HTML.replace("<head>", f'<head>\n <base href="{base_href}" />', 1)
return HTMLResponse(html)