perf(ui): cache busting — HTML no-cache + versioned asset URLs (?v=) with immutable 1y asset caching
Phase 33 (story: .agent/user_stories/cache-busting.md). - app/core/caching.py: asset_version() — git short SHA (a commit is a deploy), stable content-hash fallback for non-git checkouts, "dev" for a missing static dir; computed once per process. CachingMiddleware — the five HTML pages revalidate (no-cache) with ?v=<token> asset refs rewritten in flight; /assets/* is public, max-age=31536000, immutable; everything else (all /api/*, the SSE chat stream in particular) passes through byte-identical. - tests/e2e/test_cache_busting.py: fresh-Chromium wire assertions — document no-cache, versioned CSS/JS request URLs sharing one token, immutable asset headers, /api/health baseline headers, SSE chat to done (mock LLM). - README 'Caching / deploys' section + story file. Also fixed two prod-image defects surfaced by this phase's podman smoke (the full app would not boot): - Containerfile: ship the scripts/ package — app/api/sync.py (phase 32) imports scripts.git_sync / scripts.import_docs at module level, so the container crashed on boot (ModuleNotFoundError: No module named 'scripts'). - compose.yaml: pass BOR_ADMIN_PASSWORD / BOR_SESSION_SECRET through to the app service (:- defaults keep 'podman compose up -d db' working; the app's own fail-loud gate still names missing admin auth). Smoke: podman compose --profile prod up -d on a fresh image + a fresh Chromium profile — /, /sources.html and /login.html all served Cache-Control: no-cache; all 8 asset requests versioned with one shared token (content-hash fallback inside the image — no .git there); /assets/* immutable for a year.
This commit is contained in:
@@ -325,6 +325,44 @@ git-source refresh in one click, in-process:
|
||||
pull, hash skip, and the overview is left alone (its regeneration is
|
||||
change-gated).
|
||||
|
||||
## Caching / deploys
|
||||
|
||||
A deploy is a commit — and the browser must see it **without a hard
|
||||
refresh** (the "the pages are too sticky" problem, phase 33). One
|
||||
Starlette middleware (`app/core/caching.py`) applies the rule at the
|
||||
transport layer:
|
||||
|
||||
- **HTML pages always revalidate.** Every page (`/`, `/sources.html`,
|
||||
`/document.html`, `/login.html`, `/tuning.html`) ships
|
||||
`Cache-Control: no-cache`, so each visit re-checks the page with the
|
||||
server — a page never lingers in the browser's cache unchecked.
|
||||
- **Assets are versioned and cached for a year.** The pages reference
|
||||
their CSS/JS with a token (`/assets/styles.css?v=<token>`), and every
|
||||
`/assets/*` response ships
|
||||
`Cache-Control: public, max-age=31536000, immutable`. The token is what
|
||||
identifies the content, so long-term caching is safe: a new token means
|
||||
a new URL, which the browser fetches fresh.
|
||||
- **The token is the deploy.** In a git checkout (the normal case) it is
|
||||
the short SHA of `HEAD` (`git rev-parse --short HEAD`), computed once
|
||||
per process start — so **every commit/deploy flips the token** and the
|
||||
versioned asset URLs change with it. A checkout without `.git` (or a git
|
||||
failure) falls back to a stable content hash of the `frontend/` tree
|
||||
(sorted path + mtime + size), so dev checkouts still bust; a missing
|
||||
static dir gets the placeholder token `dev`.
|
||||
- **The API is untouched.** Nothing under `/api/*` — the SSE chat stream
|
||||
in particular — gains or loses a header or has its body read; the SSE
|
||||
endpoint's own `Cache-Control: no-cache` is set by the endpoint itself.
|
||||
|
||||
No CDN, no new services, no build-step change: the middleware rewrites
|
||||
the asset references of the five known pages in flight. The unversioned
|
||||
asset paths keep working too (the static mount ignores the query string),
|
||||
so old tabs and direct links to `/assets/…` still resolve.
|
||||
|
||||
> **Deploy note:** the very first deploy onto this scheme needs one
|
||||
> normal page visit, so the browser revalidates the HTML once and starts
|
||||
> requesting the versioned assets; every commit after that is picked up
|
||||
> automatically.
|
||||
|
||||
## Checking retrieval quality
|
||||
|
||||
Ask the *real* pipeline (live aipi embeddings + the current KB) whether a
|
||||
|
||||
Reference in New Issue
Block a user