fix(dashboard): revalidate HTML app shell so obsidian card shows /obsidian/ not :3000#79
Merged
Merged
Conversation
…obsidian :3000) The operator saw the obsidian Services card showing a raw `:3000` instead of the clean `/obsidian/` SSO link, even though PR #78 shipped correctly: /api/services returns the obsidian entry with NO `url`/`check` echoed (only id/name/port/hint/ok), and serviceOpenHref takes the `APP_PREFIX && SSO_ROUTES['obsidian']` branch -> `/obsidian/`. The card never renders `port` as text. So current code cannot produce `:3000` — the browser was serving a CACHED pre-#78 index.html. Root cause: Starlette's StaticFiles sends ETag + Last-Modified but NO Cache-Control on index.html, so browsers apply heuristic freshness and serve the stale app shell without revalidating. Every dashboard rebuild (new SSO routes / service cards) can therefore keep showing the old shell until a hard refresh — a recurring drift, not a one-off. Fixing per-incident ("tell the operator to hard-refresh") would repeat forever; the real fix is upstream. Fix: subclass StaticFiles to add `Cache-Control: no-cache` to HTML responses only. The shell stays cached but MUST revalidate its ETag every load, so new builds appear immediately; hashed static assets keep long-cache behavior. Tests: index.html sends Cache-Control: no-cache (+ retains ETag); obsidian entry has the SSO-routed shape (no `url`, port for probe only, hint uses the same-origin `/obsidian/` path, never `:3000`) — regression-locking the shape against codebase-memory-ui. Validated live on ordo-v2 (only dashboard rebuilt/recreated): served shell now carries `cache-control: no-cache` + the obsidian SSO route; /api/services obsidian entry unchanged & clean; /api/hardware still reports disk (1999.8GB) + both GPUs (1070+5090); dashboard healthy; obsidian/llamacpp/agent container IDs unchanged. Root ruff (dashboard/) + tests green (118 passed). Co-Authored-By: Claude Opus 4.8 <[email protected]>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What the operator saw
The new obsidian row in the dashboard Services tab showed a raw :3000 instead of the clean
/obsidian/SSO link.Discriminating evidence — it was NOT #78, it was browser cache
Live
/api/servicesobsidian entry (container-to-container via caddy +X-Forwarded-Email) is structurally identical to codebase-memory-ui — the working precedent:urlkey and thecheckURL is NOT echoed into the payload.serviceOpenHreftherefore takes theAPP_PREFIX && SSO_ROUTES['obsidian']branch →location.origin + '/obsidian/'.Running/Offline, and (only when offline) a hint that sayshttps://<host>/obsidian/—:3000appears nowhere.So current code cannot produce
:3000. The servedindex.htmlfrom #78 already containsobsidian: '/obsidian/'inSSO_ROUTES. The operator's browser was serving a cached pre-#78 shell (dashboard recreated ~21:25Z; the old shell had aurl/no-obsidian-route and fell through toserviceUrl(host, s.port)→:3000).Root cause (upstream, recurring — not a one-off)
Starlette's
StaticFilessendsETag+Last-Modifiedbut noCache-Controlonindex.html. Browsers then apply heuristic freshness and serve the stale app shell without revalidating. Every future dashboard rebuild (new SSO routes / service cards) risks the same stale-shell drift. Telling the operator to hard-refresh would be a per-incident bandaid that repeats forever.Fix
Subclass
StaticFilesto addCache-Control: no-cacheto HTML responses only. The shell stays cached but MUST revalidate its ETag every load, so new builds are picked up immediately; hashed static assets keep their long-cache behavior.Tests (root
tests/test_services_and_throughput.py)test_index_html_sends_no_cache— HTML shell returnsCache-Control: no-cacheand still carries an ETag.test_obsidian_entry_is_sso_routed_like_codebase_memory— locks the obsidian entry to the SSO-routed shape (nourl, port for the probe only, hint uses/obsidian/, never:3000), mirroring codebase-memory-ui.Acceptance (validated live on ordo-v2, only
dashboardrebuilt/recreated)cache-control: no-cache(ETag retained) + theobsidian: '/obsidian/'route./api/servicesobsidian entry clean; render branch →/obsidian/, no:3000in any visible field./api/hardwarestill returns disk (1999.8 GB total) + both GPUs (GTX 1070 + RTX 5090) — env overrides persist via the manifest.healthy; obsidian / llamacpp / agent container IDs unchanged (only dashboard recreated).dashboard/) clean;118 passed, 4 skipped.🤖 Generated with Claude Code