# Developer Homepage A static portfolio site (vanilla HTML/CSS/JS + a fake terminal easter egg) packaged as an nginx container and deployed with Podman Quadlet. ## Build & Run with Podman ### Build the image ```bash podman build -t homepage:latest . ``` ### Run the container The image runs `build.sh` itself and copies the hashed `dist/` output into the nginx html root, so the built site is already inside the image: ```bash podman run --rm -p 0.0.0.0:8080:8080 homepage:latest ``` To iterate on `src/` without rebuilding the image, build locally and bind-mount the **built** output over the html root — mount `dist/`, never `src/`, because `index.html` requests content-hashed filenames: ```bash ./build.sh podman run --rm -p 0.0.0.0:8080:8080 -v ./dist:/usr/share/nginx/html:z homepage:latest ``` Visit `http://localhost:8080` in your browser. ## Serving & configuration `nginx.conf` is copied into the image as the site config. It listens on **8080 over plain HTTP**, serves the hashed build, serves `index.html` with `no-cache`, and caches the hashed assets as `immutable` for a year. The four hardening headers (`X-Frame-Options`, `X-Content-Type-Options`, `Referrer-Policy`, `Content-Security-Policy`) live in `nginx-security-headers.inc`, which the Dockerfile copies to `/etc/nginx/snippets/security-headers.inc`. nginx does **not** merge `add_header` across configuration levels, so every `location` block `include`s that snippet instead of inheriting from the `server` block — otherwise the locations that set their own cache headers would silently drop the security headers (audit finding M-4, Phase 03). `server_tokens off;` hides the nginx version from the `Server` header (audit finding L-2). When adding a new `location`, `include` the snippet there too and keep the security headers out of `nginx.conf` itself. TLS and HSTS are the job of the reverse proxy in front of the container (`reeseapps.com` terminates at the Caddy edge). Do **not** add `Strict-Transport-Security` or a `listen 443` block to `nginx.conf`. ### Verify the headers ```bash ./scripts/check-headers.sh ``` This is the integration test for the header fix: it builds the **real** image with podman (or docker), runs it on the scratch address `127.0.0.1:18099`, and asserts all four security headers on each response class — `/`, `/index.html` and the first hashed asset found in `index.html`. On success it prints one `OK:` line per path and `HEADER CHECK PASSED` (exit 0). A missing header prints `FAIL:
missing on `; all three paths are checked before the script prints `HEADER CHECK FAILED` and exits non-zero, so one run reports every violation (once a header has failed somewhere, the later `OK:` lines are suppressed). No local `./build.sh` is needed — the image builds itself. The container is removed by the script; `.agents/validate.sh` removes the leftover `homepage-headercheck-*` image tag. Run it after **any** change to `nginx.conf`, `nginx-security-headers.inc` or the `Dockerfile`. ## Deploy with Quadlet The `homepage.container` Quadlet unit is **not** tracked in this repository — it lives with the deployment host's config. Install it under `~/.config/containers/systemd/` (copy it from wherever the host's unit files are kept): ```bash mkdir -p ~/.config/containers/systemd cp /homepage.container ~/.config/containers/systemd/ ``` It must publish host port 8080 and use the image CI publishes: `gitea.reeseapps.com/services/homepage` (`main` on every push to `main`, plus one tag per release). Quadlet is a systemd generator: it turns `homepage.container` into a `homepage.service` unit when systemd regenerates units, so manage it under the `.service` name: ```bash systemctl --user daemon-reload systemctl --user start homepage.service systemctl --user status homepage.service ``` Auto-start on boot comes from the unit's `[Install]` section (e.g. `WantedBy=default.target`), which the generator applies while generating — the Quadlet services are transient, so `systemctl enable` has no effect on them; re-run `systemctl --user daemon-reload` after editing the unit. For a user-mode unit that must come up without a login session, enable lingering once: `loginctl enable-linger $USER`. ## Development Source files live in `src/`: `index.html`, `style.css`, `script.js` and the flat terminal modules (`terminal.js`, `terminal-commands.js`, `terminal-vim.js`, `terminal-achievements.js`). Keep every `.js` file flat in `src/` — `build.sh` only hashes root-level files. ```bash npm install # installs the pinned @playwright/test dev dependency ./build.sh # src/ -> dist/ (content-hashed asset names) npm test # Playwright E2E suite against dist/ on 127.0.0.1:8123 npm run test:headed # same suite, visible browser ``` `playwright.config.js` points `baseURL` at `http://127.0.0.1:8123` and starts the static server with `python3 -m http.server 8123 --bind 0.0.0.0 --directory dist` — the port is bound on all interfaces while the suite runs, so use it on a trusted network. The fake terminal performs **no real network I/O**. `curl` and `wget` render deterministic simulated output built by `simulatedCurlOutput()` / `simulatedWgetOutput()` in `src/terminal-commands.js`, which echo the requested URL and say they are simulated (audit finding M-2, Phase 02) — add new command output the same canned way. The E2E suite guards this: one test records every request outside the test origin while running `curl` and `wget`, so a real network call reintroduced into `src/` fails the suite. That test derives the allowed origin from `baseURL` in `playwright.config.js`, so changing the port there needs no test edit. The suite (`tests/e2e/01-page.spec.js` … `05-menu.spec.js`) covers page structure — sections, nav targets, the experience timeline and the six featured project cards in their fixed order — the fake terminal's commands, history, tab-completion and pranks, the vim simulator, the achievement unlocks and their `localStorage` persistence, the mobile menu, and the fact that no request ever leaves the test origin. Content changes to `src/index.html` (e.g. the featured projects) are asserted there, so update the matching test with the content. Always build before testing: the suite exercises the built artifact in `dist/`, never `src/`. `npm test` starts (or reuses) its own static server, so no manual server is needed. Rebuild the image (`podman build`) after changing `src/`. Requires Node.js 20+ (the `@playwright/test` engine floor) and the Chromium revision cached for the pinned Playwright version — run `npx playwright install chromium` if it is missing, and see `AGENTS.md` before bumping the pin. The validation gate the phased pipeline runs per task is `.agents/validate.sh` (full E2E suite, the container security-header check from `scripts/check-headers.sh`, plus repo-hygiene checks): `bash .agents/validate.sh`. The header check is skipped with a loud `WARN:` line on a host with no container runtime; everything else still runs.