Finish the panel: interface fixes, executable contracts, and the guest it runs on #1

Merged
JMR-dev merged 23 commits from feature/resume-build into main 2026-09-20 21:35:23 +00:00
JMR-dev commented 2026-09-20 20:54:43 +00:00 (Migrated from github.com)

Picks up from the implementation plan and carries it through the interface fixes the live data
called for, executable contracts for every HTTP surface, browser flows, CI, and the host this
runs on.

What changed

The data and the interface. The payload column held 182 MB of a 332 MB database and
contained nothing the typed columns did not already have, so it now keeps only attributes
outside the known schema. Every Tencode_Description Metro Nashville publishes is the ten-code
itself, so a numeric type reads "Code 43" rather than a bare number. The source publishes in
batches, which can leave the last 24 hours empty while ingestion is perfectly healthy: the feed
now says when the newest call arrived and offers the last 7 days.

Executable contracts. Bruno OpenCollection collections in api-tests/ cover the dashboard's
eight operations, queries over GET, health and readiness, and the limits that refuse a page, a
date range, a query cost, an unknown field, a mutation, and too many aliases. They run against a
fixture-backed API started by the same command (pnpm run test:api).

Browser flows. Playwright drives a real browser against the built dashboard and the real
event stream: the feed and its figures, filtering, a call's published record, the map, the
narrow-screen switch, the empty-period offer, and calls published while the reader watches.

CI. Four jobs run every gate the project is checked with. Actions are pinned to commits with
their versions beside them, and a test fails the build if one ever arrives unpinned or if the
workflow stops running a gate.

The deployment. Two Debian-slim images plus a Caddy built with exactly two modules, run by
Podman Quadlets as non-root users with a read-only root, no capabilities except
NET_BIND_SERVICE on the proxy, and ports bound to loopback so the proxy sees real client
addresses. Caddy runs the OWASP Core Rule Set, pinned by digest. nftables owns the host firewall
with firewalld masked; fail2ban bans what the web firewall refused. An Ansible role takes an
AlmaLinux 10 host from stock to configured, and docs/operations.md says how to run it.

Gates

Gate Result
pytest 186 passed, 100% lines and branches
pytest -m deployment 69 passed
Vitest 125 passed, 100% statements, branches, functions, lines
Bruno 22 requests, 21 tests, 67 assertions
Playwright 9 flows
ruff, ruff format, strict pyright, ESLint, both TypeScript projects clean

Decisions worth a second opinion

  • Two Core Rule Set exclusions, in containers/caddy/coraza/panel.conf: 942290 reads
    GraphQL's $variables as MongoDB operators and 932235 reads the source field as the shell
    builtin. Each is removed from ARGS:json.query on /graphql only, with its reason written
    beside it. Without them the firewall refuses every operation the dashboard sends.
  • The deployment scenarios are excluded from the default pytest run (-m 'not deployment')
    because they build images and configure a guest. They run in their own CI job.
  • The initial migration was edited in place: a database from an earlier run reports revision
    0001, passes /readyz, and then fails on the renamed column. Start from a fresh database.

What no container can prove

Reboot recovery, a certificate actually issued, SELinux labelling, and packets actually
filtered. The guest scenario also installs and generates the units without starting them, so
nothing has yet started the services through the role. Those want a run against a machine.

Bugs the scenarios caught, all fixed here

fail2ban will not start when the log it watches does not exist yet; logrotate rejects a numeric
id where a name belongs, so the proxy's logs were never rotating; Caddy's binary carries a file
capability and cannot start without it; the Core Rule Set blocked every GraphQL operation; and a
notification arriving before the opening query made the dashboard ask about the browser's last
24 hours rather than the server's.

🤖 Generated with Claude Code

Picks up from the implementation plan and carries it through the interface fixes the live data called for, executable contracts for every HTTP surface, browser flows, CI, and the host this runs on. ## What changed **The data and the interface.** The payload column held 182 MB of a 332 MB database and contained nothing the typed columns did not already have, so it now keeps only attributes outside the known schema. Every `Tencode_Description` Metro Nashville publishes is the ten-code itself, so a numeric type reads "Code 43" rather than a bare number. The source publishes in batches, which can leave the last 24 hours empty while ingestion is perfectly healthy: the feed now says when the newest call arrived and offers the last 7 days. **Executable contracts.** Bruno OpenCollection collections in `api-tests/` cover the dashboard's eight operations, queries over GET, health and readiness, and the limits that refuse a page, a date range, a query cost, an unknown field, a mutation, and too many aliases. They run against a fixture-backed API started by the same command (`pnpm run test:api`). **Browser flows.** Playwright drives a real browser against the built dashboard and the real event stream: the feed and its figures, filtering, a call's published record, the map, the narrow-screen switch, the empty-period offer, and calls published while the reader watches. **CI.** Four jobs run every gate the project is checked with. Actions are pinned to commits with their versions beside them, and a test fails the build if one ever arrives unpinned or if the workflow stops running a gate. **The deployment.** Two Debian-slim images plus a Caddy built with exactly two modules, run by Podman Quadlets as non-root users with a read-only root, no capabilities except `NET_BIND_SERVICE` on the proxy, and ports bound to loopback so the proxy sees real client addresses. Caddy runs the OWASP Core Rule Set, pinned by digest. nftables owns the host firewall with firewalld masked; fail2ban bans what the web firewall refused. An Ansible role takes an AlmaLinux 10 host from stock to configured, and `docs/operations.md` says how to run it. ## Gates | Gate | Result | | --- | --- | | pytest | 186 passed, 100% lines and branches | | pytest `-m deployment` | 69 passed | | Vitest | 125 passed, 100% statements, branches, functions, lines | | Bruno | 22 requests, 21 tests, 67 assertions | | Playwright | 9 flows | | ruff, ruff format, strict pyright, ESLint, both TypeScript projects | clean | ## Decisions worth a second opinion - **Two Core Rule Set exclusions**, in `containers/caddy/coraza/panel.conf`: 942290 reads GraphQL's `$variables` as MongoDB operators and 932235 reads the `source` field as the shell builtin. Each is removed from `ARGS:json.query` on `/graphql` only, with its reason written beside it. Without them the firewall refuses every operation the dashboard sends. - **The deployment scenarios are excluded from the default `pytest` run** (`-m 'not deployment'`) because they build images and configure a guest. They run in their own CI job. - **The initial migration was edited in place**: a database from an earlier run reports revision `0001`, passes `/readyz`, and then fails on the renamed column. Start from a fresh database. ## What no container can prove Reboot recovery, a certificate actually issued, SELinux labelling, and packets actually filtered. The guest scenario also installs and generates the units without starting them, so nothing has yet started the services through the role. Those want a run against a machine. ## Bugs the scenarios caught, all fixed here fail2ban will not start when the log it watches does not exist yet; logrotate rejects a numeric id where a name belongs, so the proxy's logs were never rotating; Caddy's binary carries a file capability and cannot start without it; the Core Rule Set blocked every GraphQL operation; and a notification arriving before the opening query made the dashboard ask about the browser's last 24 hours rather than the server's. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Sign in to join this conversation.