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:
+31
-13
@@ -28,19 +28,16 @@ _As of 2026-09-21:_
|
||||
re-exports are asserted by a test. 192 tests green, `0.1.15`.
|
||||
- **U2 (marks) has landed** — `booth/marks.py`, contract at
|
||||
`docs/contracts/u2_marks.contract.md`, 242 tests green. Not yet deployed.
|
||||
- **Two things are outstanding on U2 and both need the operator:**
|
||||
1. **Deploy + migrate, in that order.** The live service still runs the old
|
||||
code, and four unanswered `*.ask.json` sidecars are live
|
||||
(`dfa-concepts`, `sc-iso-spread`, `sindra-voice-1`, `run07-decisions`).
|
||||
Migrating BEFORE deploying is the hazard: the old code would keep serving
|
||||
the sidecar, an answer written there would land in the sidecar, and
|
||||
`import_legacy_asks` skips a stem it has already imported — so that answer
|
||||
would be lost. Restart the service first, then `booth marks-import <name>`
|
||||
on each of the four.
|
||||
2. **The release tier.** U2 changes the CLI surface for 17 consuming handles
|
||||
(`booth asks` → `booth marks`, new `marks-import`) and is a v1 unit, so it
|
||||
reads minor-worthy — which needs explicit operator approval per the SemVer
|
||||
rule. Nothing is bumped or tagged; the work is committed as SHAs.
|
||||
- **U2 is DEPLOYED and the migration is done.** The service was restarted
|
||||
2026-09-21 23:41 and again after the `auto_reload` fix; all four legacy
|
||||
sidecars imported (`dfa-concepts/dfa`, `run07-decisions/decisions`,
|
||||
`sc-iso-spread/spread`, `sindra-voice-1/anchor`, all still open) with the
|
||||
sidecars left on disk. Verified live: index + 25 booths x {booth page, marks
|
||||
page, marks.json} all 200, plus zoom views on five booths.
|
||||
- **Still needs the operator: the release tier.** U2 changes the CLI surface for
|
||||
17 consuming handles (`booth asks` -> `booth marks`, new `marks-import`) and is
|
||||
a v1 unit, so it reads minor-worthy — which needs explicit approval per the
|
||||
SemVer rule. Nothing is bumped or tagged; the work is committed as SHAs.
|
||||
- **`/heid-contract-review` on the U2 contract is still in flight** (panel mode,
|
||||
posted 2026-09-21, redacted copy at
|
||||
`/tmp/heid-contract-review/booth-20260922-061015/`). Triage it when it lands —
|
||||
@@ -152,6 +149,27 @@ _As of 2026-09-21:_
|
||||
|
||||
## Tried and abandoned
|
||||
|
||||
- `[2026-09-21]` **Letting Jinja hot-reload templates while the repo is the
|
||||
deployment root** — the cause of a live outage the same day U2 landed, and the
|
||||
sharpest foot-gun in the repo. `booth.service` sets `WorkingDirectory` to this
|
||||
repo, so the running service imports these files with no build step and no
|
||||
staging copy. Python is read once at process start; Jinja's `FileSystemLoader`
|
||||
re-reads a template **on every render**. Editing `booth.html` therefore
|
||||
deployed it instantly against Python from 22:03 that knew nothing about
|
||||
`item_marks`, and **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.
|
||||
**The lesson that generalises:** a skew between a process and the disk under it
|
||||
is invisible to the test suite by construction, so no amount of green tests
|
||||
would have caught it; the operator found it. Fixed at the source rather than
|
||||
with a reminder — the `Environment` is hand-built with `auto_reload=False`, so
|
||||
there is now ONE staleness rule (nothing takes effect until you restart) and
|
||||
the running process is always a coherent snapshot of one commit. Asserted by
|
||||
`test_templates_do_not_hot_reload_from_disk`. Watch the second-order risk the
|
||||
fix introduces: a hand-built `Environment` does not inherit `autoescape` from
|
||||
the `Jinja2Templates` constructor, and booth names, item names and mark text
|
||||
are all agent-authored strings landing in HTML.
|
||||
|
||||
- `[2026-09-21]` **Five separate mechanisms to get one question next to one
|
||||
artifact** — `.forever`, the link board, `inline.py`'s placeholder DSL,
|
||||
`wrap_verbatim_html`'s six regexes, and the floating amber asks chip plus
|
||||
|
||||
Reference in New Issue
Block a user