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.
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.
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)
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
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_DescriptionMetro Nashville publishes is the ten-codeitself, 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'seight 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_SERVICEon the proxy, and ports bound to loopback so the proxy sees real clientaddresses. 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.mdsays how to run it.Gates
-m deploymentDecisions worth a second opinion
containers/caddy/coraza/panel.conf: 942290 readsGraphQL's
$variablesas MongoDB operators and 932235 reads thesourcefield as the shellbuiltin. Each is removed from
ARGS:json.queryon/graphqlonly, with its reason writtenbeside it. Without them the firewall refuses every operation the dashboard sends.
pytestrun (-m 'not deployment')because they build images and configure a guest. They run in their own CI job.
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