# Container Packaging Task 4 Report ## Status Implemented and verified runtime-configured frontend packaging. ## Changes - Added the browser runtime contract `window.__THOTHII_CONFIG__.backendBaseUrl`. - Loaded `/config.js` before the Vite module entrypoint. - Made runtime configuration take precedence while preserving `VITE_BACKEND_URL` and the existing `http://localhost:8787` client default for development and tests. - Added a multi-stage frontend image that builds with Node and serves static assets as unprivileged UID/GID `101:101` with nginx on port 8080. - Added startup-time `BACKEND_BASE_URL` substitution (default `/api`). - Added `/api/` reverse proxying to `core:8787`, SPA fallback, no-cache runtime config, and SSE-safe proxy settings (`proxy_buffering off`, `proxy_cache off`, one-hour read timeout). ## TDD evidence - RED: `npx vitest run src/api/runtime-config.test.ts` failed because `./runtime-config` did not exist. - GREEN: targeted runtime config suite passed (3 tests after preserving the legacy client default). ## Verification - `cd frontend && npx vitest run --reporter=dot && npx tsc -b && npm run build` — exit 0 (40 test files, 185 tests; TypeScript and Vite production build passed). - `docker build -f docker/frontend.Dockerfile -t thothii-frontend:test .` — success. - Image metadata reports `USER 101:101`. - Two-container isolated-network smoke: - `/config.js` returned `window.__THOTHII_CONFIG__ = { backendBaseUrl: "/api" };` - `/api/health` proxied to the core image and returned `{"status":"ok"}`. - an unknown nested route returned the SPA `index.html`. - active nginx config contained `proxy_buffering off`, `proxy_cache off`, and `proxy_read_timeout 1h`. - `/config.js` returned `Cache-Control: no-store`. - `sh -n docker/frontend-entrypoint.sh` and `git diff --check` — exit 0. ## Secret-leakage inspection - `.dockerignore` excludes `.env*` (except examples), credentials/key formats, dependency trees, build outputs, backend data, and deployment data. - The runtime web root contained no `.env*`, `.pem`, `.key`, `.p12`, or `.pfx` files. - Image history contained build/package instructions only; no secret build arguments or credential values were introduced by this task. ## Self-review / concerns - nginx resolves the `core` hostname at startup, matching the planned Compose service name; standalone runs therefore need a reachable network alias named `core`. - Existing frontend test warnings (React refs/act, MSW unmatched incidental requests, Vite chunk-size warnings) remain; they did not fail the requested gates and are unrelated to this task. - `.superpowers/sdd/progress.md` was already modified by the orchestrator and was intentionally excluded from this task's commit. ## P1 review fixes Follow-up commit work addressed both review findings: - Runtime configuration is now produced with `jq -cn --arg`, so `BACKEND_BASE_URL` is encoded by a real JSON serializer rather than interpolated into JavaScript by `sed`. - The image includes `frontend-config-smoke`, which strips only the fixed assignment wrapper, parses the remaining JSON with `jq`, requires exactly the `backendBaseUrl` key, and compares the decoded value to the environment input. - The hostile smoke passed with quotes, backslashes, a literal newline, ampersand, pipe, and `"; globalThis.PWNED=true; //` in the value. A breakout would leave non-JSON trailing input and fail parsing. - Added `joinBackendPath`, shared by API fetch and EventSource creation. It removes duplicate boundary slashes for relative and absolute bases while keeping empty and `/` bases rooted. Follow-up verification: - RED: six join cases failed with `joinBackendPath is not a function` before implementation. - Targeted: runtime config, API client, and EventSource suites — 14 tests passed. - Full frontend gate — exit 0 (40 test files, 191 tests, TypeScript, Vite build). - Rebuilt `thothii-frontend:test` successfully. - Hostile config image smoke — `frontend runtime config smoke: ok`. - Rebuilt two-container smoke — default `/api` config, proxied `/api/health`, SPA fallback, and SSE-safe nginx directives all passed.