6.1 KiB
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)
- 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.jsand 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 offrontend/). - Secondary gap (image would be broken even if it built): stage 1
bundles only
app.js+sources.jsand copies onlyindex.html+sources.html. Missing from the image:document.html+login.html(phases 10/16),document.js+login.js, andmarkdown.js(classic script loaded byindex.html+document.html). - Verified fix: with relative imports (
from "./header.js") all four page scripts bundle cleanly with esbuild 0.25.5. - 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 rawheader.jstoo would evaluate the module twice (duplicate sign-out listener, double init). The direct tags are redundant: the page script'simportis hoisted and guaranteesheader.jsevaluates before the page script's body callsinitSharedHeader(), 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)
- Relative imports (
./header.js) over an esbuild alias — simpler, verified working, dev-server behavior unchanged (files are side-by-side). - Remove the four redundant direct
header.jsscript tags (the design above) rather than ship a rawheader.jsinto the image — single module evaluation, no duplicate listeners. - 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 directheader.jstags.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
01_fix_containerfile_build.md— relative imports, tag removal, stage-1 asset coverage, greenpodman build, image smoke test.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 tocomplete/.
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): everyfrontend/*.htmlis copied into stage 1's/out; every localsrc/hrefasset referenced by the four pages is produced by a stage-1 line (esbuild--outfileorcp); the four page module scripts are the exact set esbuild bundles;markdown.jsis produced; no HTML references/assets/header.jsdirectly (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 pyrightclean.
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.jsminified and contains the header code;/assets/markdown.js200; nohttp(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 pytestgreen;uv run pytest --cov=app --cov-report=term-missing≥ today's number.uv run ruff check . && uv run pyrightclean..agent/user_stories/containerfile-build.mdexists.- One
--no-gpg-signcommit (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"