v0.254.0 — the saved notes follow the language, and the switch becomes a globe (R-557 slice 2 release C; SLICE 2 CLOSED)
gates / gates (push) Successful in 23s

The notes a background run SAVES — last night's backup line, the last error, the proof
result, the restore outcome — are written in the BOX's language at the moment they are
written. A household that switches sees the previous run's note in the old language until
the next run rewrites it: the operator's §16 option 1, stated rather than hidden.
EndRestoreOp no longer receives a Hungarian literal from anywhere.

The language switch is a globe. Two text links wrapped in the sidebar footer and asked the
reader to recognise "Magyar"/"English" as links; a globe is the one symbol every web user
already reads as "language", so nobody has to read Hungarian to escape Hungarian. It is
<details>/<summary> — a menu with no script, drawn inline because the icon sprite lives
only in layout.html and the visitor pages have their own shell.

Those visitor pages get the same globe, and a visitor's choice stays theirs: a display-only
felhom_lang cookie that langFor reads ONLY when there is no session. A signed-in household
can never inherit a language a previous visitor picked in the same browser. POST /lang is
CSRF-exempt for a narrow reason written at the exemption — its only achievable effect is the
language of the page the victim's own browser shows them — and safeBackPath refuses
//evil.example as well as https://, because "starts with /" alone is not the test. §16 taken:
a successful claim carries the cookie into the household's setting.

TWO PARITY EXCEPTIONS, MEASURED: 106 fixtures compared with a real diff — exactly two change
shapes (the dashboard footer, the globe in the shells) and 5 byte-identical, which are the
three pages that must not change.

I INTRODUCED A DEADLOCK AND THE SUITE CAUGHT IT BY HANGING. 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 the settings
lock. Fixed by resolving the language before the callback, and guarded by a test that names
the file and line in a second instead of hanging for 25 minutes.

MinAgent: 0.131.0 (unchanged). No hub release needed.

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:19:31 +02:00
parent eb6aa58aa7
commit 48f3336956
142 changed files with 2199 additions and 318 deletions
+81 -2
View File
@@ -1,7 +1,7 @@
# REPORT — localisation slice 2, releases A and B (controller v0.252.0 + v0.253.0, R-557)
# REPORT — localisation slice 2, COMPLETE (controller v0.252.0 + v0.253.0 + v0.254.0, R-557)
**2026-09-18 · base commit `736f54b49610` (v0.251.0) · MinAgent 0.131.0, unchanged · no hub release.**
**Two releases this session: A (the sentences shown now) and B (the 179 error messages).**
**Three releases: A (the sentences shown now), B (the 179 error messages), C (the saved notes + the globe).**
Architecture read first and named: `felhom.eu/documentation/architecture/10-localisation.md` §1
(parity), §2 (mechanism), §5 (gates), §9 (the R-553 signals), §10 (this slice); `07-backup-architecture.md`
for what the hub reads from a box; `05-hub-architecture.md` for what the hub composes itself.
@@ -193,3 +193,82 @@ are not red-proofs — nobody planted them.
translated on purpose), `handler_debug.go` (R-574), the notifier's 27 wire messages (R-558),
`funcmap.go`'s two un-overridden helpers (R-572), the text a background run PERSISTS (release C), and
the R-570 producer. Release C's default stands: written in the box's language at write time.
---
# Release C — v0.254.0: the saved notes, and a globe for the switch. SLICE 2 CLOSED.
## 1. Claims in the prompt that live source disproved — named first
| the prompt said | source says |
|---|---|
| "Release A's survey (persisted list, wire list)" is in `audits/i18n-slice2-2026-09-18/A/` | That folder holds only `live/`. The persisted list is in this REPORT's §6 for release A. Derived again from source here, which is the stronger reading — and it found the largest persisted group (`EndRestoreOp`'s outcome family, ~40 literals in two files) that a list would have had to remember. |
| "the three shells share no layout" | **True, and confirmed by reading**: `login.html`, `claim.html`, `recovery.html` each open their own `<html lang="{{T "layout.html_lang"}}">`. But the consequence was missed: the icon SPRITE lives only in `layout.html`, so a `<use href="#i-globe">` would render nothing on those three. The globe is drawn INLINE in the partial instead — one definition, four places, no sprite coupling. No dead symbol was added. |
| "no cookie is read anywhere for language" | Confirmed by grep: no language cookie existed. |
| the `<details>` menu "working without script in the browsers the guide names" | **Cannot be confirmed here.** `claude-in-chrome` is not available on DooPlex, so no browser rendered this page. `<details>`/`<summary>` is a plain HTML element with no script, and the markup is asserted; whether it LOOKS right is an operator click-through. Said plainly rather than implied. |
## 2. What shipped
**The saved notes.** ~70 producers across `internal/backup` (off-site classes and quota, tier-2
reasons and warnings, undo phrases, the reconstitute hold, the restore record) and `internal/web`
(the whole `EndRestoreOp` outcome family). Each renders through `util.Text(boxLang(), key, …)`.
**`EndRestoreOp` no longer receives a Hungarian literal from anywhere** (grep, with the negative
control). `s.noteErr`/`m.noteErr` render release B's errors into a note in the same language.
**The globe.** One partial (`lang_globe.html`), used by the dashboard footer and the three visitor
shells; drawn inline in the same stroke style as the sprite. `langFor` gained one step, and the order
is now fixed: `?lang=` → **household setting when a session exists** → visitor cookie when none →
setting → `hu`. A signed-in household never reads the cookie, which is the row that protects them.
**`POST /lang`** — CSRF-exempt for a narrow, checkable reason written at the exemption: the only
achievable effect is changing the language of the page the victim's own browser shows them. It writes
one cookie, reads nothing, and `safeBackPath` refuses `//evil.example` as well as `https://…` —
"starts with /" alone is not the test, and that is the mistake the function exists to not make.
**§16 taken (the operator's stated default): a claim carries the visitor's language**, on success
only, at the one moment an anonymous visitor becomes the household.
## 3. The two parity exceptions, measured
106 fixtures rendered and compared with a **real (LCS) diff**: exactly **two change shapes** — the
dashboard footer (89 fixtures) and the globe in the shells (12) — and **5 byte-identical**, which are
precisely the two guest share pages and the catch-all, the three that must not change. Full diff:
`audits/i18n-slice2-2026-09-18/C/parity-exception-diff.txt`.
**Deviation, named:** the prompt asked for one commit per re-capture. This is one commit, because
splitting them leaves an intermediate commit whose suite is red — and a commit that does not pass is
worse than a commit whose EVIDENCE separates the two blocks, which this one does explicitly.
## 4. Two defects release C found in its own work
**(a) A DEADLOCK I introduced, caught by the suite hanging.** `UpdateOffboxStatus` holds the settings
WRITE lock while it runs its callback; `boxLang()` reads the language through the settings READ lock.
`sync.RWMutex` is not reentrant. The first draft rendered a note inside that callback — on a real box
an off-site run would have hung **forever, holding the settings lock**, wedging everything else that
touches `settings.json`. The only symptom was `go test` timing out at 25 minutes instead of 8.
Fixed by resolving the language before the callback; `TestNoteHelpersAreNotCalledUnderTheSettingsLock`
now names the file and line in a second instead. **A hang is the worst kind of failure to diagnose,
and the lesson is: a helper that takes a lock must never be called from inside a callback that holds
one.**
**(b) The first "only two blocks changed?" measurement compared LINE BY INDEX.** An insertion shifts
every line below it, so it reported 60 520 changed lines and measured nothing. **A line-index compare
is not a diff.** Redone with difflib.
Also caught before it shipped: the parity HARNESS rendered the three shells through the DASHBOARD
path, which would have baked a form the real page never serves. Found by reading the diff before
re-capturing, and independently by `TestI18nDirectRenderPagesFollowLanguage`.
## 5. Green gate
`go build ./... && go vet ./... && go test ./...` green; all controller gates OK. Red-proofs 15–19 in
`audits/i18n-slice2-2026-09-18/redproofs.txt`, each seen to convict.
## 6. What is left after slice 2
**629 Hungarian literals**, none of them errors and none of them saved notes: the country TABLE (113,
not on the wire, not translated on purpose), the page-title literals slice 1 handles through
`TitleKey`, `handler_debug.go` (R-574), the notifier's 27 wire messages (R-558), `funcmap.go`'s two
un-overridden helpers (R-572), the R-570 producer, and the soft memory warning (R-575).
**R-557 is CLOSED.** New row: **R-577**, the guest-share visitor's language.