Mosaic one model, many lenses

ADR 0025 — Admin-UI routing: ui_prefix, the ui web-binary deploy, embedding LRU, Qdrant gRPC

Follow-up to ADR 0020 (content root) and ADR 0024 (embedded storage), from reviewing the latest EE mono commits (8944e7a5b, e3dc06369, d2bc23620, 0a7089324, 480c9a3cc, b76d626f3).

1. app.ui_prefix — nest the admin UI under a hidden route

EE learn (8944e7a5b feat(ddd): ui_prefix app field): when an app also serves content at / (content root), the Leptos admin UI is nested under a hidden route prefix (EE deploys use "/_"): /_/commands, /_/aggregates, … while / and the REST bridge (/api/...) are unchanged. It is an APP field, not a deploy field (the web shell is one binary, the prefix is part of the UI's identity).

Mosaic implementation.

  • Tessera: app: { ui_prefix: "/_" } (YAML + AppDecl.ui_prefix). Validated fail-closed (bad-app-ui-prefix): must be a non-empty path segment — leading /, no trailing /, length > 1. Unset = today's routes (byte-identical).
  • Web lens: every Leptos Route gains the prefix as its first StaticSegment (path_of(&["commands"]) in main_src); the root / route (content) and the axum REST routes are untouched.
  • The scene pages' internal links keep their absolute paths (they navigate within the UI; the browser URL bar shows the prefixed route).

2. deploy: { ui: true } — ship the web shell, not just the API binary

A k8s spec with ui: true deploys the web crate's binary ({app-name}-web: the full app — API + Leptos admin UI + shell fallback) instead of the API-only binary:

  • The k8s Dockerfile builds --bin {app-name}-web; the entrypoint takes no args (the web shell has no serve subcommand).
  • The Deployment template omits the serve --bind … args and sets MOAIC_BIND_ADDR=0.0.0.0:<containerPort>; the emitted web main.rs reads it (falling back to the leptos options' dev bind, 127.0.0.1:8787) — only when the app declares a ui deploy, so dev and non-ui deploys are byte-identical.
  • values.yaml gains ui: true (only for ui specs) and the template nil-checks .Values.ui.

3. Embedding LRU cache (learned e3dc06369)

The vectordb lens' embed_text is wrapped by a process-local LRU (4096 entries, key = exact text, emitted for every vectordb app): repeated texts — re-ingests of unchanged files, repeated query chunks, the boot-time embed-what-is-missing pass — skip the HTTP round-trip. Fail-soft and side-effect-free (pure cache), so it is emitted unconditionally for vectordb apps rather than behind an opt-in.

4. Qdrant template fixes (learned d2bc23620 + 0a7089324)

  • Image tag is qdrant/qdrant:v1.12.5 — the tag with the v prefix is the real Docker Hub tag (the bare 1.12.5 does not exist; latest would also break the chart's semver check).
  • The Service + container expose gRPC 6334 alongside REST 6333: gRPC clients (tonic) connect on 6334 — 6333 alone never serves them.
  • (P25, deferred: the qdrant client itself — including EE's arbitrary-doc-id → valid-point-id mapping from 480c9a3cc.)

5. Knowledge data-seam runtime-context fix (found while verifying)

The embedded tier's smoke test exposed a pre-existing crash: the knowledge data seam (mosaic-knowledge/src/data.rs) drives OpenDAL's async API with its own dedicated tokio runtime via block_on. Runtime::block_on panics ("Cannot start a runtime from within a runtime") when the calling thread is already driving a runtime — which is exactly where a host runs the engine's synchronous boot (the app's worker / spawn_blocking threads carry the host runtime's context). Symptom: cold boot died right after ingest (or hung on a worker), in any persist backend — the file/JSON seam included, since plain paths go through the same OpenDAL seam.

Fix: run_op — when Handle::try_current() is Ok (caller inside a runtime), the future is handed to a dedicated context-free worker thread (same mpsc pattern the host uses for its LanceDB ops); otherwise block_on directly (the cheap path: plain threads, tests).

Consequences

  • Small/medium-tier apps and any spec can now serve the admin UI on the same host as their content (/_), no extra ingress or path rewrite.
  • The data seam is context-safe in every calling context (the claim its old doc-comment made but did not keep).
  • The qdrant store template matches what a gRPC-speaking client actually needs.