Files
brain-of-reese/.agent/phases/complete/33_cache_busting/00_phase.md
T
ducoterra 02c76ad328 chore(agent): phase roadmap from TODO.md — 8 phases (40–47), 24 tasks
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/).
2026-08-27 18:25:53 -04:00

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

  1. 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.
  2. 02_caching_middleware.md — the response middleware (HTML no-cache + ?v= rewrite; /assets/* immutable) wired into create_app + integration tests.
  3. 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/src and leading-slash-less assets/… 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 new app/core/caching.py fully 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 with Cache-Control: no-cache and its asset references carry ?v=<token> (token non-empty, stable across requests, changes when the frontend content changes).
  • /assets/* responses carry Cache-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 pytest green; uv run pytest --cov=app --cov-report=term-missing TOTAL ≥ pre-change number (app/ >90%).
  • uv run pytest tests/e2e/test_cache_busting.py -v --no-cov green in isolation; test_smoke.py + one RAG E2E stay green.
  • uv run ruff check . && uv run pyright clean.
  • .agent/user_stories/cache-busting.md exists; README documents the caching behavior + how the token changes on deploy.
  • One --no-gpg-sign commit 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 is git rev-parse --short HEAD (a commit = a deploy, so the token flips on every deploy); otherwise a stable hash of the frontend tree (sorted relpath + 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 (the Containerfile esbuild stage is untouched).
  • Asset caching — /assets/* are cached immutable for 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.js directly 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 own no-cache). Implemented as a Starlette middleware that only rewrites text/html responses under the page paths; if Response.body() turns out to misbehave on the FileResponse streaming 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-sign commit.