docs: localisation slice 2 CLOSED (controller v0.254.0) — R-577, R-578, and a probe rule
gates / gates (push) Successful in 23s

10-localisation.md §10.3: the saved notes follow the box language at write time, with the
one-night consequence stated rather than hidden; the globe, and the table of WHO reads which
page and where its globe posts — getting that wrong makes the button do nothing, which it did
on /recovery until the live probe found it. Decision 6 superseded a second time; decision 8
(a claim carries the visitor's language) recorded. Decision 5 of §11's anonymous-surface line:
changing what a VISITOR reads is within what an anonymous request may do; changing anything the
household owns is not, and POST /lang can do only the first.

R-578 — the deadlock, and why it is a row rather than a fixed bug: UpdateOffboxStatus holds the
settings write lock while running its callback, boxLang() wants the read lock, sync.RWMutex is
not reentrant. On a real box an off-site run would have hung FOREVER holding that lock. The
symptom was a test suite going from 8 minutes to a 25-minute timeout. Fixed and guarded, but the
guard covers one package and three helper names; the class needs a gate.

R-577 — a guest share visitor still has no way to pick a language, and the household's setting
is the wrong default for a stranger. Deliberately left, pinned by a test, and the operator's to
decide because it is a promise the share feature makes.

.claude/rules/live-probes.md, unconditional: never send a deploy request for an app that is not
installed, not even expecting a refusal — the endpoint accepts first and validates later. Two
sessions made that mistake in two days, the second WITH a prompt line forbidding it. A prompt is
read once; a rule file is loaded every session.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
2026-09-18 14:31:49 +02:00
parent ed0b0f5c92
commit 95c10954ae
10 changed files with 374 additions and 43 deletions
+68 -1
View File
@@ -279,7 +279,7 @@ session). Each slice ends with the parity gate green for every page it touched.
|---|---|---|---|---|
| 0 (done) | — | mechanism, 3 pages + layout, setting, report field, gates | Hungarian unchanged by measurement; English reachable | spent |
| 1 (done, v0.248.0–v0.250.0) | R-556 | the other 31 dashboard templates (~1 500 strings, 600 of them JS), parity fixtures per page; switch shown to everyone; English retrieval stems; the extractor's ASCII word list reviewed per page | the whole dashboard in English with Hungarian byte-identical | 12–16 h, three releases |
| 2 | R-557 (after R-553) | Go-side customer strings; flash-in-URL → keys; country names; alert texts; the 179 error messages | a page's server messages follow the language | 16–20 h, three releases · **A and B shipped 2026-09-18; C remains** |
| 2 (done, v0.252.0–v0.254.0) | R-557 | Go-side customer strings; flash-in-URL → keys; country names; alert texts; the 179 error messages; the saved notes; the globe | a page's server messages follow the language | **CLOSED 2026-09-18** |
| 3 | R-558 | hub: per-customer language at creation; `configgen` renders it; `customerMessages` (39), the 5 lifecycle mails and the self-bind page in English; dispatcher reads the reported language | a household's e-mails arrive in its language | 6–8 h, one hub + one controller release |
| 4 | R-559 | console banner (34 lines, console-font limits) and the download page | an English household meets English from the first boot screen (in scope — ruling 1b) | 4–6 h + an ISO train |
| 5 | R-560 | catalog: §7 format, controller reads it, 835 strings / ~5 000 words, catalog copy gates per language | an app card, its settings and its first steps in English | 12–16 h |
@@ -394,6 +394,68 @@ with both controls.
string with no error to carry a key and no language where it is built, so it renders Hungarian on an
English page. The copy is in the bundle; only the render is fixed. Named in the code, filed as a row.
### 10.3 Slice 2 release C — the saved notes, and a globe (v0.254.0). SLICE 2 CLOSED.
**[DESIGN] A note a background run SAVES is written in the BOX's language at write time** (operator
ruling, §16 option 1). ~70 producers. **The consequence, recorded because it is the cost of the
choice:** a household that switches language sees the previous run's note in the old language until
the next run rewrites it — usually the next night. The alternative (store a code, render live) needs a
dozen new persisted fields and a legacy path for each: the R-570 shape a dozen times over.
`EndRestoreOp` now receives no Hungarian literal from anywhere.
**[FACT] A deadlock, introduced and caught by the suite hanging (R-578).** `UpdateOffboxStatus` holds
the settings WRITE lock while running its callback; `boxLang()` reads the language through the READ
lock; `sync.RWMutex` is not reentrant. A note rendered inside that callback deadlocks **holding the
settings lock**, which wedges everything else on the box that touches `settings.json`. The only
symptom was `go test` going from 8 minutes to a 25-minute timeout. **The general rule this establishes:
a helper that takes a lock must never be called from inside a callback that holds one** — and a hang
is the worst symptom to diagnose, which is why R-578 asks for a gate rather than one test in one package.
**[DESIGN] Decision 6, superseded a second time: the switch is a GLOBE, and a visitor's language is
their own.** One answerable sentence: *how does someone who cannot read Hungarian find the way out of
Hungarian?* Options: (1) keep two text links („Magyar"/„English") in the sidebar footer — cost: they
wrap at the sidebar's width, and finding them means recognising two words as links; (2) one globe, the
symbol every web user already reads as "language". **Chosen (2)**, `<details>`/`<summary>` so the menu
needs no script and a screen reader announces it. Language names inside are shown in their own
language and are never translated. Drawn inline rather than in the icon sprite, because the sprite
lives only in `layout.html` and the pages outside the dashboard chrome have their own shell.
**[DESIGN] Who the page is FOR decides where its globe posts, and getting that wrong makes the button
do nothing.**
| page | reader | globe posts to | their choice lives in |
|---|---|---|---|
| every dashboard page | the household, signed in | `/settings/language` (session CSRF) | `settings.json` |
| `/recovery` — an AUTHENTICATED route | the household, signed in | `/settings/language` | `settings.json` |
| `/login`, `/claim` | a visitor, no session | `/lang` (no CSRF) | the `felhom_lang` cookie, their browser |
| the two guest share pages, the catch-all | a stranger / nobody | **no globe** | — (R-577, the operator's) |
`langFor`'s order is fixed: `?lang=` → **the household's setting when a session exists** → the cookie
when there is none → the setting → `hu`. **A signed-in household never reads the cookie**, so they
cannot inherit a language a previous visitor picked in the same browser. The recovery row above was
measured live before it was right: an anonymous form there sets a cookie that `langFor` then ignores,
and the button appears to do nothing.
**[DESIGN] `POST /lang` is CSRF-exempt, for a reason narrow enough to check.** The only achievable
effect of a forged request is to change the language of the page the victim's own browser shows them.
It writes one display-only cookie, reads nothing, touches no setting, and `safeBackPath` refuses a
protocol-relative `//host` as well as an absolute URL — *"starts with `/`"* alone is not the test,
because a browser reads `//evil.example` as another origin. **If that handler ever gains a second
effect it needs CSRF that day.** That is the anonymous surface `04-control-plane-authorization.md`
governs: changing what a visitor reads is within it, changing anything the household owns is not.
**[DESIGN] §16, the operator's default, taken: a successful CLAIM carries the visitor's language into
the household's setting.** Someone who switched the claim page to English and then claimed the box
chose English. Only on success, and only there — the one moment an anonymous visitor becomes the
household.
**[FACT] The parity exceptions, measured rather than asserted.** 106 fixtures, a real (LCS) diff
against the fixtures as they stood at v0.253.0: **3 change shapes** (the footer; the recovery globe;
the login/claim globe) and **5 byte-identical**, which are exactly the pages that must not change.
`audits/i18n-slice2-2026-09-18/C/parity-exception-diff.txt`. **A second measurement error worth
keeping: the first attempt compared LINE BY INDEX, and an insertion shifts every line below it — it
reported 60 520 changed lines and measured nothing. A line-index compare is not a diff.**
---
## 11. Operator decisions
@@ -414,6 +476,11 @@ These are rulings, not proposals. Anything specced against a different assumptio
5. **Mechanism (b2)** — §2.1.
6. ~~**Switch hidden while only three pages are English** — §3.~~ **Superseded 2026-09-17** by slice 1
release C (v0.250.0): every template converted, switch shown to every household (§3).
**Superseded again 2026-09-18** by slice 2 release C (v0.254.0): the switch is a GLOBE, it is on the
sign-in and claim pages too, and a visitor's choice lives in their own browser (§10.3).
8. **A successful claim carries the visitor's language into the household's setting** (§16 default,
taken 2026-09-18). Only on success; every other anonymous request leaves `settings.json` alone.
### 2026-09-17 (evening) — the two open questions, ruled