Files
homepage/README.md
T
ducoterra 1fe932cf39
Build and Push Container / build-and-push (push) Successful in 16s
phase: 05_update_projects_section
**Phase 05 final pass — all 4 tasks already complete; verified phase-level criteria + docs. Repo 100% green.**

- Verified: 6 `project-card fade-in` cards; order Brain of Reese → Reverse Proxy → Dynamic DNS → Local AI Stack → Deployment Automation → ServWaf; `src/index.html` diff hunks (lines 293–538) all inside `#projects` (286–542).
- `./build.sh` → `Build complete: dist/` (exit 0); `npm test` → **48 passed, 0 failed**; `bash .agents/validate.sh` → **exit 0** (48 E2E + `HEADER CHECK PASSED` on `/`, `/index.html`, hashed CSS via podman + repo-hygiene gates). No linter exists in this repo (no `lint` script/eslint config). Coverage: no JS/CSS touched — vacuous per phase spec; E2E is the binding gate.
- Completion criteria: card count 6 in `src/` **and** `dist/` ✅ · first card "Brain of Reese" with `…/Experimental/brain-of-reese` href in `dist/index.html` ✅ · `grep -c "Self-Hosted Services\|Kubernetes Cluster" src/index.html` = **0** ✅ · AI card has vLLM/Gufo/llama.cpp + LocalAI/LiteLLM/Langfuse, no "Ollama" ✅ · regression test present in `tests/e2e/01-page.spec.js` and passing ✅ · `./build.sh && npm test` green (48 ≥ 45) ✅ · no edits outside `#projects` ✅ · README/AGENTS consistent ✅.
- Docs: README **updated** (new paragraph stating what the E2E suite covers, incl. the six featured cards, so future content edits update the matching test). AGENTS.md **updated** (convention: the `#projects` grid is test-locked by `01-page.spec.js`). All other README claims re-checked against reality (Dockerfile builds `dist/` internally, `listen 8080`, snippet `include`s, `check-headers.sh` port/paths/output, `--bind 0.0.0.0` config, Node ≥20 engine floor, CI `main`+tag publishing) — consistent.
- Notable: `.agents/validate.sh` is still **untracked** though AGENTS.md/README reference it — harness should `git add` it (I ran no git index commands). `.agents/PLAN.md` §4/§5 still say "44 tests" (suite is 48 since Phases 04/05); PLAN.md is off-limits, so this drift needs an explicit user-approved edit. No live-infrastructure touched.
- Next pending phase: none in `.agents/phases/todo/` (05 was last). Remaining unphased audit items (L-3 CSP `unsafe-inline`, L-5 SSH-key labelling, L-7 CI pinning, L-8 build.sh CI check) would need new phases via `phase-authoring`.
2026-10-23 01:32:52 -04:00

159 lines
6.9 KiB
Markdown

# 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: <header> missing on <path>`; 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 <deploy-config>/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.