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
Routegains the prefix as its firstStaticSegment(path_of(&["commands"])inmain_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 noservesubcommand). - The Deployment template omits the
serve --bind …args and setsMOAIC_BIND_ADDR=0.0.0.0:<containerPort>; the emitted webmain.rsreads it (falling back to the leptos options' dev bind,127.0.0.1:8787) — only when the app declares auideploy, so dev and non-ui deploys are byte-identical. values.yamlgainsui: 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 thevprefix is the real Docker Hub tag (the bare1.12.5does not exist;latestwould 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.