fix(booth): templates were hot-reloading into a live service running older Python

19 of 25 live booths returned 500 with `UndefinedError: 'item_marks' is
undefined`. Neither the old code nor the new code was broken — the service was
running both at once.

`booth.service` sets WorkingDirectory to this repo, so the repo IS the
deployment root: no build step, no staging copy, the live service imports these
files. Python is read once when the process starts. Jinja's FileSystemLoader
re-reads a template on EVERY render. So the two halves of the service had
different staleness rules, and editing booth.html deployed it instantly against
Python from 22:03 that had never heard of the context the new markup wanted.

The failure mode is worth naming precisely, because it is invisible to the
suite by construction: the skew exists between a running process and the disk
underneath it, so every test can pass against a tree that is simultaneously
serving 500s. No amount of green catches this. The operator found it.

Fixed at the source rather than with a reminder to restart. The template
Environment is built here with auto_reload=False, so templates are cached at
startup exactly like the Python, and there is ONE rule: nothing takes effect
until you restart. The price is that template work needs a restart to see —
that price is the entire point, and it is cheaper than a page of 500s while
someone is reviewing.

Building the Environment by hand means autoescape no longer comes from the
Jinja2Templates constructor, so it is explicit and load-bearing: booth names,
item names and mark text are all agent- or operator-authored strings that land
in HTML. Verified escaped, not merely configured.

Two tests hold the line — one on the snapshot property, one on the `dur` filter
that is no longer incidental to the constructor. The environment is reachable at
app.state.templates because a promise about the deployed service needs an
assertion, and an assertion needs the env the app actually renders with.

Also records the foot-gun in CLAUDE.md and persistent-memory: anyone editing
this repo while the operator may be using the service is editing production.

244 tests. No version bump — the release tier for U2 is still the operator's
call, and this rides with it.
This commit is contained in:
vh
2026-09-21 23:44:37 -07:00
parent c7f9437a64
commit bb1e3cfcd7
4 changed files with 141 additions and 15 deletions
+36
View File
@@ -152,6 +152,42 @@ Ask which one you have before choosing a storage shape.
- **Nothing deletes the operator's data on a timer** beyond the documented 24h
TTL. Liveness is *flagged*, not enforced.
## ⚠ The repo IS the deployment root
`booth.service` runs uvicorn with `WorkingDirectory=/home/lkraven/development/booth`.
There is no build step, no staging copy, and no separate deploy artifact: the
live service on `:8090` imports **these files**. Two consequences, and the second
one caused an outage.
1. **A Python edit does nothing until you restart.** Expected, and documented
below.
2. **A template edit used to take effect INSTANTLY.** Jinja's `FileSystemLoader`
re-reads a template from disk on every render. So the two halves of the
service had different staleness rules, and editing `booth.html` deployed it
immediately against Python that had never heard of the context it wanted.
On 2026-09-21 that put **19 of 25 live booths at 500** —
`UndefinedError: 'item_marks' is undefined` — with the Python from 22:03 and
the templates from 23:40. Neither version was broken; the service was running
both. The operator found it, not the suite, because no test can see a skew
that only exists between a process and the disk under it.
Fixed at the source: the template `Environment` is now built with
`auto_reload=False`, so templates are cached at startup exactly like the
Python. **One rule now — nothing takes effect until you restart.** The price
is that template work needs a restart to see, and that price is the point.
`test_templates_do_not_hot_reload_from_disk` holds the line.
**So: after ANY edit here — Python or template — the live service is stale until
you restart it.** If you are touching this repo while the operator may be using
the service, either restart promptly or expect him to be looking at the old
version. Never leave the tree in a state where a restart would 500.
⚠ The `Environment` is hand-built now, which means `autoescape` is explicit
rather than inherited from the `Jinja2Templates` constructor. It is on
(`select_autoescape(["html", "xml"])`) and it is load-bearing: booth names, item
names and mark text are all agent- or operator-authored and land in HTML.
## Working in here
```sh