Files
brain-of-reese/.agent/phases/todo/23_containerfile_build/00_phase.md
T

6.1 KiB

Phase 23 — Containerfile: Build the Whole App Image Again

Source: TODO.md L6 — "Fix Containerfile build not working" Story: .agent/user_stories/containerfile-build.md (created by task 02) Context: Containerfile (3 stages: node:22-alpine + esbuild 0.25.5 frontend bundle → uv/python deps → slim runtime serving /app/static); frontend/ (4 pages: index.html, sources.html, document.html, login.html; assets: styles.css, markdown.js (classic script), header.js/app.js/sources.js/document.js/ login.js (ES modules)); scripts/entrypoint.sh.

Verified diagnosis (2026-08-24, this conversion — not a guess)

  1. Root cause of the build failure: phase 19 switched the page scripts to import … from "/assets/header.js" (an absolute URL). esbuild resolves that as the filesystem path /assets/header.js and the stage-1 bundle dies: ✘ [ERROR] Could not resolve "/assets/header.js" (reproduced with esbuild 0.25.5, the exact pinned version, on a copy of frontend/).
  2. Secondary gap (image would be broken even if it built): stage 1 bundles only app.js + sources.js and copies only index.html + sources.html. Missing from the image: document.html + login.html (phases 10/16), document.js + login.js, and markdown.js (classic script loaded by index.html + document.html).
  3. Verified fix: with relative imports (from "./header.js") all four page scripts bundle cleanly with esbuild 0.25.5.
  4. Latent double-evaluation trap: all four HTML pages also load <script type="module" src="/assets/header.js"> directly while the page script imports it. In dev the browser dedupes (same module URL) — but in the image the bundled page script already contains the header code, so shipping a raw header.js too would evaluate the module twice (duplicate sign-out listener, double init). The direct tags are redundant: the page script's import is hoisted and guarantees header.js evaluates before the page script's body calls initSharedHeader(), in dev and in the bundle alike.

Objective

podman build -f Containerfile . succeeds, and the resulting image serves the whole app — all four pages with their bundled, minified, local-only assets (No CDN rule) — with header.js evaluated exactly once per page.

Owner-confirmed (2026-08-24, roadmap A4)

  1. Relative imports (./header.js) over an esbuild alias — simpler, verified working, dev-server behavior unchanged (files are side-by-side).
  2. Remove the four redundant direct header.js script tags (the design above) rather than ship a raw header.js into the image — single module evaluation, no duplicate listeners.
  3. The image must cover all four pages + all local assets they reference — the integration test (task 02) enforces this coverage so the gap cannot silently reappear.

Dependencies

  • 19_shared_header (complete) — introduced the absolute imports (root cause) and the direct header.js tags.
  • 10_story_document_viewer / 16_admin_auth (complete) — the pages missing from the image.
  • 08_story_dark_tech_theme (complete) — No CDN rule the image must honor.

Tasks

  1. 01_fix_containerfile_build.md — relative imports, tag removal, stage-1 asset coverage, green podman build, image smoke test.
  2. 02_integration_test_commit.md — tests/integration/ test_containerfile_assets.py (hermetic coverage pin), regression suites, story file, final validation, the single atomic commit, phase move to complete/.

Locked decisions

  • A11 honored — vanilla JS, no CDN, static serving from FastAPI. A16 honored — integration test for the new build coverage; story file + report; no Playwright suite required (this phase is build/infrastructure — the phase gate is the hermetic integration test + the real podman build + image smoke recorded in the report, plus the dev-server E2E regressions). No anchor changed.

Testing & Quality

  • Integration (new tests/integration/test_containerfile_assets.py, hermetic — no podman, no network): every frontend/*.html is copied into stage 1's /out; every local src/href asset referenced by the four pages is produced by a stage-1 line (esbuild --outfile or cp); the four page module scripts are the exact set esbuild bundles; markdown.js is produced; no HTML references /assets/header.js directly (single-evaluation design pin); the esbuild version stays pinned.
  • Unit: none (no app/ changes).
  • Coverage: the >90% app/ gate is unaffected, re-run to prove it.
  • Build gate (manual, recorded in the report): podman build -f Containerfile . green; image smoke (task 01 step 6) results + log excerpt in .agent/reports/23_containerfile_build/.
  • Dev regressions (E2E, isolated): test_smoke.py, test_shared_header.py, test_chat_persistence.py (the HTML tag removal touches dev page load).
  • Lint/types: uv run ruff check . && uv run pyright clean.

Completion Criteria

  • Local esbuild 0.25.5 bundles all four page scripts cleanly.
  • podman build -f Containerfile . green (log excerpt in the report).
  • Image smoke: container runs (throwaway Postgres 17 + pgvector); GET /, /sources.html, /document.html, /login.html → 200; /assets/app.js minified and contains the header code; /assets/markdown.js 200; no http(s):// asset reference in any served page (No CDN rule).
  • Dev server unchanged in behavior: the three regression E2E suites green in isolation.
  • uv run pytest green; uv run pytest --cov=app --cov-report=term-missing ≥ today's number.
  • uv run ruff check . && uv run pyright clean.
  • .agent/user_stories/containerfile-build.md exists.
  • One --no-gpg-sign commit (below); .agent/phases/todo/23_containerfile_build/ moved to .agent/phases/complete/.

Commit

git add -A .agent/ Containerfile frontend/ tests/ && git commit --no-gpg-sign -m "fix(build): Containerfile builds again — relative module imports, all four pages and shared assets in the image"