Files
brain-of-reese/.agent/user_stories/containerfile-build.md
T

6.8 KiB

Story: Containerfile Builds the Whole App Again

Phase: 23_containerfile_build · Gate: hermetic integration tests/integration/test_containerfile_assets.py + real podman build

  • image smoke (no Playwright suite — build/infrastructure phase, per the phase plan's A16 note)

Narrative

As the owner, I reported (2026-08-24, TODO.md L6) that the Containerfile build "is not working". I want podman build -f Containerfile . to succeed again — and, more importantly, for the resulting image to serve the whole app: all four pages (chat, sources, document viewer, login) with their bundled, minified, local-only assets (No CDN rule, A11), the shared header module evaluated exactly once per page, and a build contract that fails loudly when a future page/script/asset outgrows the Containerfile.

  • Given the 3-stage Containerfile (esbuild frontend bundle → uv python deps → slim runtime serving /app/static)
  • When podman build -f Containerfile . runs
  • Then the build is green and the image serves /, /sources.html, /document.html, /login.html with 200 and every referenced asset local and minified — with header.js inlined into each page bundle exactly once.

Bug report (verbatim, TODO.md L6)

Fix Containerfile build not working

Verified diagnosis (2026-08-24, phase 23 — 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/ — repro_esbuild_0.25.5.log).
  2. Secondary gap (image would be broken even if it built): stage 1 bundled only app.js + sources.js and copied 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. 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.
  4. Second build blocker (found in task 01): a local npm install of esbuild left the binary in node_modules/.bin, invisible to the next RUN — esbuild: not found. The stage must install globally.

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

  1. Relative imports (from "./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.

Fix

piece before after
page-script header import from "/assets/header.js" (absolute — esbuild fatal) from "./header.js" (relative, bundles cleanly)
direct header.js <script> tags 4 (one per page — double-eval trap) 0 (the hoisted import covers evaluation order)
esbuild install local (binary off the next RUN's PATH) npm install -g esbuild@0.25.5 (pinned, on PATH)
stage-1 bundles app.js, sources.js app.js, sources.js, document.js, login.js
stage-1 minifies styles.css styles.css, markdown.js (no --bundle — classic script)
stage-1 copies index.html, sources.html all four pages into /out

Acceptance criteria

  1. Local esbuild 0.25.5 bundles all four page scripts cleanly.
  2. podman build -f Containerfile . green (log excerpt in .agent/reports/23_containerfile_build/).
  3. 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).
  4. Dev server unchanged in behavior: test_smoke.py, test_shared_header.py, test_chat_persistence.py green in isolation.
  5. test_containerfile_assets.py 6/6 within the full suite; uv run pytest green; app/ coverage ≥ today's number (>90% gate); ruff + pyright clean.

Test mapping table

Test Scenario → tests/integration/test_containerfile_assets.py (hermetic — parses Containerfile + frontend/ as text; no podman, no network, no DB):

  1. test_every_html_page_is_copied_into_stage1 — every frontend/*.html is copied by a stage-1 cp into /out, and the copied set is exactly the pages on disk (new page without cp entry → fail; cp of a deleted page → fail).
  2. test_every_local_asset_reference_is_produced — every local assets///assets/ src=/href= in the pages is produced by a stage-1 line (esbuild --outfile=/out/assets/<name> or a cp) — the missing-markdown.js-style gap cannot reappear.
  3. test_page_module_scripts_are_bundled — the type="module" page scripts referenced by the HTML (basenames) equal the esbuild --bundle inputs in stage 1 (today: app.js, sources.js, document.js, login.js).
  4. test_header_module_is_imported_not_directly_loaded — no HTML carries a direct <script … src="…/header.js"> tag (single-evaluation design pin, owner-confirmed A4-2) and every page script imports it relatively (from "./header.js" — no absolute form).
  5. test_markdown_js_is_a_produced_classic_script — markdown.js has a stage-1 minify line without --bundle and no top-level import/export in the source (both sides of the classic-script assumption pinned).
  6. test_esbuild_stays_pinned — the frontend stage pins a concrete esbuild@X.Y.Z (no latest/^/~/bare spec — the exact pinned 0.25.5 is what the diagnosis reproduced against).

Complementary source pins live in tests/unit/test_shared_header.py:: test_header_module_loads_before_the_page_script (unit) and the real build + image smoke are recorded in .agent/reports/23_containerfile_build/ (task 01).