Skip to content

fix(trusty-search,trusty-memory,trusty-analyze): derive UI API base from baseURI pathname, not the raw href - #4985

Open
mac-duetto wants to merge 1 commit into
mainfrom
fix/4980-ui-base-hash-routing-v2
Open

fix(trusty-search,trusty-memory,trusty-analyze): derive UI API base from baseURI pathname, not the raw href#4985
mac-duetto wants to merge 1 commit into
mainfrom
fix/4980-ui-base-hash-routing-v2

Conversation

@mac-duetto

Copy link
Copy Markdown
Collaborator

Closes #4980

Problem

computeBase() derived the SPA's API base by running two $-anchored regex strips against the raw document.baseURI href. baseURI carries the URL fragment, so on a hash-routed load ending in ui/#/ the mount-segment strip silently no-ops. Every API call is then misrouted under /ui/, hits the SPA catch-all, and returns index.html as 200 text/html instead of JSON/SSE. Nothing fails at the HTTP layer, which is why this went unnoticed.

API_BASE is snapshotted once at module load, so the wrong base is fixed for the life of the page.

Not a recent regression. Introduced in d087b88, first shipped in trusty-search-v0.24.10 — roughly 18 minor versions ago. Reported after a v0.42.2 upgrade only because the upgrade restarted the daemon and the tab reloaded at /ui/#/.

Wider than it looks. router.svelte.js never writes location.hash at init, so a first navigation to bare /ui/ works. But navigate() writes the hash on the first sidebar click and it stays in the URL. Every reload, restored tab, or bookmarked visit after that is broken — the steady state after a minute of normal use.

Fix

Run the same strips against new URL(document.baseURI).pathname, which never carries the fragment or query, then return origin + strippedPath. The window.__SEARCH_BASE__ override branch and the typeof document === 'undefined' guard are untouched.

Applied to all three KEEP IN SYNC copies: trusty-search, trusty-memory, trusty-analyze.

trusty-memory was not user-visibly broken — its SPA is root-mounted, so the strip is a no-op and apiUrl() stayed correct; only apiBase() returned a fragment-bearing string and nothing calls it. Fixed for the sync contract, stated as such in its changelog rather than overclaimed.

Rejected alternative

base.js honours a window.__SEARCH_BASE__ override and base.test.js tests it, but no Rust code ever injects it. Wiring it up server-side looks like the obvious fix and is wrong: serve_index has only state.daemon_port (a bare u16) — no scheme, no host, no proxy prefix. Any absolute base it builds is a guess, and the override is checked first, so it wins. Behind the trusty-console proxy the browser would bypass the proxy entirely; under ssh -L, Docker port mapping, or a non-loopback bind the injected origin is simply wrong. It would trade a reload bug for a topology bug in exactly the deployments the current code handles correctly. This was caught by an adversarial review pass, not by the first-pass diagnosis.

Test ladder — rung 6 (UI / API surface)

cd crates/trusty-search/ui && pnpm run test
cd crates/trusty-memory/ui  && pnpm run build
cd crates/trusty-analyze/ui && pnpm run build
SKIP_UI_BUILD=1 cargo build  -p trusty-search -p trusty-memory -p trusty-analyze
SKIP_UI_BUILD=1 cargo clippy -p trusty-search --all-targets -- -D warnings
SKIP_UI_BUILD=1 cargo clippy -p trusty-analyze --all-targets -- -D warnings

Built with the pinned toolchain — Node 20.20.2 + pnpm 9.15.9, matching the ui-checks CI job.

Red first. The three new cases fail against unfixed code on this base:

× ignores the #/ fragment → expected 'http://127.0.0.1:7878/ui/#/' to be 'http://127.0.0.1:7878/'
× ignores the fragment behind the console proxy
× ignores a query string → expected '.../ui/health' to be '.../health'
 Test Files  1 failed | 1 passed (2)      Tests  3 failed | 10 passed (13)

Green after: Test Files 2 passed (2) / Tests 13 passed (13). All pre-existing cases pass unmodified.

Binary smoke run — before/after against real shipped bundles

A binary built from this branch was run isolated on port 17878. The bundle was fetched over the wire and its real computeBase/apiUrl executed in Node against stubbed baseURI values, contrasted against the pre-fix bundle from dd712709:

document.baseURI old apiUrl('/health') new
/ui/ /health /health
/ui/#/ /ui/health /health
/ui/#/indexes /ui/health /health
/ui/?tab=1 /ui/health /health
https://console.local/proxy/search/ui/#/ .../ui/health .../search/health

Served index.html references the new hash index-D8eMcIuN.js; the old index-DHZwUpbb.js is absent from the binary.

Bundle diff isolation

trusty-memory and trusty-analyze isolate cleanly — 110,013/110,171 and 139,723/139,883 bytes identical, differing region is computeBase only.

trusty-search did not: only 1,826 of 106,144 bytes match, because adding two local bindings shifted esbuild's identifier-frequency ranking and re-mapped names bundle-wide. Proven benign rather than assumed: the build is deterministic (same source twice → identical md5), and reverting base.js and rebuilding reproduces the exact committed baseline index-DHZwUpbb.js, while restoring the fix gives index-D8eMcIuN.js. base.js is the only input delta. Zero .rs files and no lockfile or dependency changed.

Not in scope

ui_asset_handler returns 200 + index.html for any unmatched /ui/* path, which is why this failure produced HTTP 200s instead of surfacing in the network tab. /ui/health still returns 200 text/html after this change — expected, unchanged, and tracked as the separable follow-up in #4980.

⚠️ Pre-existing failure on main, not introduced here

cargo clippy -p trusty-memory fails on origin/main:

error: this boolean expression can be simplified
  --> crates/trusty-common/src/memory_core/retrieval/embed_repair.rs:139:25
  | .filter(|d| !(d.is_expired_at(now) && !d.is_tier_c()))
  | help: try: `!d.is_expired_at(now) || d.is_tier_c()`

This PR changes zero .rs files, that file is byte-identical to origin/main, and it reproduces under cargo clippy -p trusty-common alone with trusty-memory's feature set — nothing from this branch involved. It surfaces only via that feature unification; trusty-common's defaults pass, which is why -p trusty-search and -p trusty-analyze are clean. CI's --workspace --all-targets -- -D warnings unifies features, so the clippy job on main is likely red independently of this PR. Deliberately not fixed here — burying an unrelated trusty-common edit in a UI fix would hide a main-is-red signal.

🤖🤖🤖 Generated with trusty-mpm — https://github.com/bobmatnyc/trusty-tools

…rom baseURI pathname, not the raw href

computeBase() ran its `$`-anchored `index.html` / `ui/` strips against the raw
`document.baseURI`, which includes the URL fragment. On a hash-routed load
ending in `ui/#/` the mount-segment strip silently no-opped, so every API call
resolved under `/ui/` and hit the SPA catch-all, which answers 200 text/html
with index.html. Nothing failed at the HTTP layer: request() fell back to
res.text() and handed callers an HTML string, while EventSource hard-failed on
the wrong Content-Type. The router writes the hash on the first sidebar click,
so every reload, restored tab, or bookmark after that was broken. A query
string (`/ui/?tab=1`) triggered the same misrouting.

The strips now run against `new URL(document.baseURI).pathname`, which carries
neither the fragment nor the query string, re-joined to the origin. The
`window.__*_BASE__` override branch and the non-browser guard are unchanged.

Applied to all three KEEP IN SYNC copies. trusty-search and trusty-analyze
mount their SPA at `/ui/` and were both broken; trusty-memory is root-mounted,
so its `ui/` strip is a no-op and apiUrl() stayed correct via relative
resolution, but apiBase() returned a fragment-bearing string.

Three regression cases added to trusty-search's base.test.js, the only UI
package with a test harness. Verified red against the unfixed code (3 failed,
10 passed) and green after (13 passed); no existing case was modified.

The committed ui-dist / ui/dist bundles are regenerated with the pinned
toolchain, since CI and release always set SKIP_UI_BUILD=1 and ship whatever is
committed. Attribution was verified by round-trip: rebuilding from the unfixed
base.js reproduces the committed baseline bundle byte-for-byte, and the build
is deterministic across runs, so the entire bundle delta is downstream of the
computeBase change alone.

Closes #4980

🤖🤖🤖 Generated with trusty-mpm — https://github.com/bobmatnyc/trusty-tools
@mac-duetto mac-duetto added trusty-mpm trusty-mpm platform and related work ws/tm-trusty-tools-20 trusty-mpm workstream tm-trusty-tools-20 labels Aug 6, 2026
@mac-duetto mac-duetto self-assigned this Aug 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

trusty-mpm trusty-mpm platform and related work ws/tm-trusty-tools-20 trusty-mpm workstream tm-trusty-tools-20

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(trusty-search): dashboard API base breaks under hash routing — computeBase regexes document.baseURI including the fragment

1 participant