Converts the 9 TODO items into an executable phase roadmap (Protocol B, appended after phase 39): - 40 tuning toggle anonymous flash (TODO L3) - 41 sync fail-fast + modal when a model is down (TODO L4) - 42 no reply autoscroll (TODO L5) - 43 thinking scroll back — user scroll + gated autoscroll (TODO L7) - 44 markdown tables (TODO L6) - 45 agent unlimited tool calls behind BOR_AGENT_MAX_ROUNDS (TODO L8) - 46 mobile hamburger nav (TODO L9) - 47 quadlet + jinja import formats, A9 revision (TODO L10–L11) Each phase carries a user story, a dedicated Playwright E2E suite plan, and owner-locked decisions (R1 A9 format extension, R2 phase-37 budget revision, A1–A5 scope decisions) confirmed 2026-08-27. Also records the completed phases 30–39 todo/ -> complete/ moves that were pending in the working tree. TODO.md is cleared (items now live in .agent/phases/todo/).
5.3 KiB
Phase 33 — Cache Busting (un-stick the pages)
Source: TODO.md L6 — "We need better cache busting, the pages are too sticky"
Story: .agent/user_stories/cache-busting.md
Context: app/main.py serves the whole frontend/ directory through one StaticFiles(html=True) catch-all mount; the five HTML pages reference assets without any version (href="/assets/styles.css", src="assets/markdown.js", src="/assets/app.js", …), so browsers happily keep stale CSS/JS/HTML after a deploy — the "too sticky" report. The no-CDN integration test (tests/integration/test_api.py::test_html_pages_served_locally_no_cdn) asserts no https:// references — appending ?v= keeps every reference same-origin, so it stays green. SSE/API live under /api/* and must be untouched (SSE already ships Cache-Control: no-cache itself).
Objective
A deploy must be visible without a hard refresh: HTML pages are always revalidated (Cache-Control: no-cache) and reference their assets with a version token (?v=<token>); assets are served immutable for 1 year (the token in the URL identifies the content, so long caching is safe). Zero new services, zero build-step changes, no CDN.
Dependencies
32_admin_sync_button(todo) — sequencing only; no shared code (this phase is transport-layer and independent of the RAG work).
Tasks
01_asset_version_token.md—app/core/caching.py::asset_version(): git short SHA (homelab checkouts have a.git), stable mtime+size content-hash fallback, computed once per process.02_caching_middleware.md— the response middleware (HTMLno-cache+?v=rewrite;/assets/*immutable) wired intocreate_app+ integration tests.03_e2e_and_docs.md—tests/e2e/test_cache_busting.py(Playwright header assertions), README, story file, commit.
Testing & Quality
- Unit: version token (git path, fallback path, failure path), the asset-reference rewrite (both
href/srcand leading-slash-lessassets/…refs, no double-?v=). - Integration: page headers + rewritten references; asset headers; no-CDN test green; SSE endpoint responses untouched (existing chat SSE tests green).
- Coverage: >90% on
app/(the newapp/core/caching.pyfully covered); TOTAL ≥ pre-change. - E2E (mandatory, A16):
tests/e2e/test_cache_busting.py— one story, run in isolation; real Chromium asserting the headers and the versioned request URLs the browser actually makes.
Completion Criteria
- Every HTML page (
/,/sources.html,/document.html,/login.html,/tuning.html) is served withCache-Control: no-cacheand its asset references carry?v=<token>(token non-empty, stable across requests, changes when the frontend content changes). /assets/*responses carryCache-Control: public, max-age=31536000, immutable./api/*(incl. the SSE chat stream) responses are byte-for-byte header-wise unaffected beyond what they already send; no-CDN integration test green.uv run pytestgreen;uv run pytest --cov=app --cov-report=term-missingTOTAL ≥ pre-change number (app/ >90%).uv run pytest tests/e2e/test_cache_busting.py -v --no-covgreen in isolation;test_smoke.py+ one RAG E2E stay green.uv run ruff check . && uv run pyrightclean..agent/user_stories/cache-busting.mdexists; README documents the caching behavior + how the token changes on deploy.- One
--no-gpg-signcommit staging only this phase's files (e.g.perf(ui): cache busting — HTML no-cache + versioned asset URLs (?v=) with immutable 1y asset caching);.agent/phases/todo/33_cache_busting/moved to.agent/phases/complete/.
Locked decisions
- Version token —
asset_version(): if the project checkout has a.git(the homelab reality), the token isgit rev-parse --short HEAD(a commit = a deploy, so the token flips on every deploy); otherwise a stable hash of the frontend tree (sortedrelpath + mtime_ns + size, first 12 hex chars) so dev checkouts still bust. Computed once per process (lru_cache) — zero per-request git/file cost. - Rewrite scope — only the five known HTML pages are rewritten (a small regex over
href="…assets/…"/src="…assets/…"appending?v=when absent). No templating layer, no build step, no changes to the static files themselves (theContainerfileesbuild stage is untouched). - Asset caching —
/assets/*are cachedimmutablefor 1 year because the URL carries the token; the unversioned path keeps working (StaticFiles ignores the query string), so old tabs and tests referencing/assets/x.jsdirectly still resolve. - Middleware boundary — the middleware touches exactly two shapes: the five page paths (body rewrite +
no-cache) and/assets/*(header only). Everything else — all/api/*including SSE — passes through byte-identical (SSE keeps its ownno-cache). Implemented as a Starlette middleware that only rewritestext/htmlresponses under the page paths; ifResponse.body()turns out to misbehave on theFileResponsestreaming path, the fallback is five explicit FastAPI routes that read + rewrite the files (identical observable behavior — the executor picks whichever passes the tests). - A11 untouched — no CDN, no new packages, no new services (A12 untouched).
- A16 / A17 honoured — one dedicated story E2E suite; one atomic
--no-gpg-signcommit.