docs: localisation slice 2 CLOSED (controller v0.254.0) — R-577, R-578, and a probe rule
gates / gates (push) Successful in 23s
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:
@@ -0,0 +1,74 @@
|
||||
# Live validation — controller v0.254.0 on demo-hp guest 9201 (2026-09-18, release C)
|
||||
|
||||
**Method: endpoint-level, stated as such.** No browser on DooPlex. Nothing was installed, created,
|
||||
formatted or deleted, and **the deploy endpoint was not touched at all** — the new rule
|
||||
(`.claude/rules/live-probes.md`) written after yesterday's mistake. The password was passed as a file
|
||||
and deleted from host and guest afterwards.
|
||||
|
||||
## 1. A visitor's language is their own (no session) — before and after
|
||||
|
||||
| probe | 0.253.0 | 0.254.0 |
|
||||
|---|---|---|
|
||||
| globe on `/login` | **0** | **1** |
|
||||
| `POST /lang lang=en back=/login` | 302 (no such route → the login redirect) | **303, `felhom_lang=en` set** |
|
||||
| `/login` with that cookie | `<html lang="hu">` | **`<html lang="en">`**, and "Forgot password" present |
|
||||
| **`settings.json` `language` after it** | `"hu"` | **`"hu"` — unchanged.** An anonymous request cannot write what the household owns |
|
||||
| `POST /lang lang=xx` | 302 | **400**, no cookie |
|
||||
| `POST /lang back=//evil.example/x` | the login redirect | **`/`** — a protocol-relative URL is another origin |
|
||||
| globe on `/claim` | 0 | **1** |
|
||||
|
||||
## 2. A signed-in household reads their own setting, never the cookie
|
||||
|
||||
`/launcher` with a session **and** the `felhom_lang=en` cookie → **`<html lang="hu">`**. The cookie is
|
||||
not read once there is a session; that is the row that keeps a household from inheriting a language a
|
||||
previous visitor picked in the same browser.
|
||||
|
||||
## 3. The dashboard globe, end to end
|
||||
|
||||
`POST /settings/language lang=en` (session CSRF) → 302 → `settings.json` `"language": "en"` →
|
||||
`/launcher`, `/backups`, `/backups/remote` all `<html lang="en">` → switched back → `"hu"`.
|
||||
|
||||
The footer renders in order — version, globe, sign-out — and the old two-text-link switch is gone
|
||||
(`lang-switch-btn` count 1 → **0**):
|
||||
|
||||
```
|
||||
<div class="sidebar-footer">
|
||||
<span class="version">0.254.0</span><details class="lang-globe">
|
||||
<summary class="lang-globe-btn" aria-label="Nyelv" title="Nyelv"><svg class="ico" …globe…/></summary>
|
||||
<ul class="lang-globe-menu">
|
||||
<li><form method="POST" action="/settings/language">…<button … class="lang-globe-item current" aria-current="true">Magyar</button></form></li>
|
||||
<li><form method="POST" action="/settings/language">…<button … class="lang-globe-item">English</button></form></li>
|
||||
</ul>
|
||||
</details>
|
||||
<a href="/logout" class="logout-link">Kijelentkezés ↗</a>
|
||||
```
|
||||
|
||||
## 4. The saved note, and the §16 consequence, seen live
|
||||
|
||||
With the box switched to English, the stored tier-2 note stayed as it was written:
|
||||
`"last_warning": "A belső SSD-n csak a konfiguráció, adatbázis és a köte…"`. **That is the operator's
|
||||
choice working, not a bug:** a saved note is written in the box's language at write time and shown
|
||||
verbatim afterwards, so a household that switches sees the previous run's note in the old language
|
||||
until the next run rewrites it.
|
||||
|
||||
## 5. What could NOT be proven live, and why
|
||||
|
||||
**The `/recovery` globe.** `recoveryPageHandler` redirects to `/backups/remote` unless
|
||||
`recoveryOffer()` is true — the page exists only while a box is in a lost-machine situation, and
|
||||
demo-hp is not. It 302s with or without a session. So its globe is proven by the render tests
|
||||
(`TestGlobeOnAnonymousShells`, `TestI18nDirectRenderPagesFollowLanguage`) and by its 7 parity
|
||||
fixtures, **not live**. Said plainly rather than left to be assumed from the other rows.
|
||||
|
||||
That page is also where release C's own defect was: it is an AUTHENTICATED route, so its globe must
|
||||
write the household's SETTING; the first draft gave it the anonymous form, which sets a cookie
|
||||
`langFor` ignores once there is a session — the button would have done nothing. Found by this probe
|
||||
reporting no globe on `/recovery` and then reading the route table.
|
||||
|
||||
**The `<details>` menu opening in a browser.** `claude-in-chrome` is not available on DooPlex. The
|
||||
markup is asserted; whether it looks and behaves right on screen is a manual click-through.
|
||||
|
||||
## 6. State left behind
|
||||
|
||||
Controller **0.254.0**; saved language **`hu`**; the visitor cookie was set only inside the probe's
|
||||
own curl jar and is gone with it. 23 standing containers up, unchanged. Nothing installed, nothing
|
||||
removed, no drive touched, no floor raised, no golden baked. **Provisioned nothing.**
|
||||
@@ -0,0 +1,17 @@
|
||||
##### A — NO SESSION: the visitor's own language
|
||||
settings.json language before: "language": "hu"
|
||||
/login with no cookie -> html lang=<html lang="hu"
|
||||
globe present on /login -> 1
|
||||
POST /lang lang=en back=/login -> 303 felhom_lang en
|
||||
/login WITH the cookie -> html lang=<html lang="en"
|
||||
and an English phrase -> 1
|
||||
settings.json language after: "language": "hu"
|
||||
POST /lang lang=xx -> 400
|
||||
POST /lang back=//evil -> /
|
||||
/claim and /recovery globes: /claim=1 /recovery=0
|
||||
##### B — WITH A SESSION: the household's own setting, cookie NOT read
|
||||
/launcher with session + the en cookie -> html lang=<html lang="hu"
|
||||
the footer: version, globe, sign-out in order ->
|
||||
the OLD text links are gone -> lang-switch-btn count = 0
|
||||
##### C — the saved notes, as they stand on this box
|
||||
"last_warning": "A belső SSD-n csak a konfiguráció, adatbázis és a kötelező adatok férnek
|
||||
@@ -0,0 +1,15 @@
|
||||
##### A — NO SESSION: the visitor's own language
|
||||
settings.json language before: (unreadable)
|
||||
/login with no cookie -> html lang=<html lang="hu"
|
||||
globe present on /login -> 0
|
||||
POST /lang lang=en back=/login -> 302 /login WITH the cookie -> html lang=<html lang="hu"
|
||||
and an English phrase -> 0
|
||||
settings.json language after: (unreadable)
|
||||
POST /lang lang=xx -> 302
|
||||
POST /lang back=//evil -> /login?next=%2Flang
|
||||
/claim and /recovery globes: /claim=0 /recovery=0
|
||||
##### B — WITH A SESSION: the household's own setting, cookie NOT read
|
||||
/launcher with session + the en cookie -> html lang=<html lang="hu"
|
||||
the footer: version, globe, sign-out in order ->
|
||||
the OLD text links are gone -> lang-switch-btn count = 1
|
||||
##### C — the saved notes, as they stand on this box
|
||||
@@ -0,0 +1,31 @@
|
||||
#!/bin/bash
|
||||
# Release C probe. Every request below either reads a page or sets a display cookie. Nothing is
|
||||
# installed, created, formatted or deleted; no deploy endpoint is touched at all (.claude/rules/live-probes.md).
|
||||
IP=172.17.0.2:8080
|
||||
H="Host: felhom.enkisfelhom.hu"
|
||||
PW=$(cat /tmp/.felhompw); J=/tmp/pj.txt; rm -f $J /tmp/vj.txt
|
||||
echo "##### A — NO SESSION: the visitor's own language"
|
||||
echo -n " settings.json language before: "; grep -o '"language": *"[a-z]*"' /var/lib/felhom/docker/volumes/felhom-controller-data/_data/data/settings.json 2>/dev/null || echo "(no language key yet = hu)"
|
||||
echo -n " /login with no cookie -> html lang="; curl -s -H "$H" "http://$IP/login" | grep -o '<html lang="[a-z]*"' | head -1
|
||||
echo -n " globe present on /login -> "; curl -s -H "$H" "http://$IP/login" | grep -c 'class="shell-lang"'
|
||||
echo -n " POST /lang lang=en back=/login -> "; curl -s -c /tmp/vj.txt -H "$H" -o /dev/null -w "%{http_code} " -X POST "http://$IP/lang" --data-urlencode "lang=en" --data-urlencode "back=/login"; grep -o 'felhom_lang[[:space:]]*[a-z]*' /tmp/vj.txt | head -1
|
||||
echo -n " /login WITH the cookie -> html lang="; curl -s -b /tmp/vj.txt -H "$H" "http://$IP/login" | grep -o '<html lang="[a-z]*"' | head -1
|
||||
echo -n " and an English phrase -> "; curl -s -b /tmp/vj.txt -H "$H" "http://$IP/login" | grep -c 'Forgot password'
|
||||
echo -n " settings.json language after: "; grep -o '"language": *"[a-z]*"' /var/lib/felhom/docker/volumes/felhom-controller-data/_data/data/settings.json 2>/dev/null || echo "(no language key yet = hu)"
|
||||
echo -n " POST /lang lang=xx -> "; curl -s -H "$H" -o /dev/null -w "%{http_code}\n" -X POST "http://$IP/lang" --data-urlencode "lang=xx" --data-urlencode "back=/login"
|
||||
echo -n " POST /lang back=//evil -> "; curl -s -H "$H" -o /dev/null -w "%{redirect_url}\n" -X POST "http://$IP/lang" --data-urlencode "lang=en" --data-urlencode "back=//evil.example/x" 2>/dev/null | sed 's|http://[^/]*||'
|
||||
echo -n " /claim and /recovery globes: "; for P in /claim /recovery; do echo -n "$P=$(curl -s -H "$H" "http://$IP$P" | grep -c 'class=\"shell-lang\"') "; done; echo
|
||||
echo "##### B — WITH A SESSION: the household's own setting, cookie NOT read"
|
||||
curl -s -c $J -H "$H" "http://$IP/login" -o /tmp/lg.html
|
||||
CSRF=$(grep -o 'name="_csrf" value="[^"]*"' /tmp/lg.html | head -1 | sed 's/.*value="//;s/"//')
|
||||
SESS=$(curl -s -b $J -H "$H" -D - -o /dev/null -X POST "http://$IP/login" --data-urlencode "password=$PW" --data-urlencode "_csrf=$CSRF" | grep -i '^set-cookie: felhom_session' | head -1 | sed 's/[Ss]et-[Cc]ookie: //;s/;.*//')
|
||||
[ -z "$SESS" ] && { echo "LOGIN FAILED"; exit 1; }
|
||||
LANGC=$(grep -o 'felhom_lang[[:space:]]*[a-z]*' /tmp/vj.txt | head -1 | awk '{print $2}')
|
||||
echo -n " /launcher with session + the en cookie -> html lang="
|
||||
curl -s -H "$H" -H "Cookie: $SESS; felhom_lang=$LANGC" "http://$IP/launcher" | grep -o '<html lang="[a-z]*"' | head -1
|
||||
echo -n " the footer: version, globe, sign-out in order -> "
|
||||
curl -s -H "$H" -H "Cookie: $SESS" "http://$IP/launcher" | grep -o 'class="sidebar-footer".*logout-link' | grep -o 'class="version"\|class="lang-globe"\|class="logout-link"' | tr '\n' ' '; echo
|
||||
echo -n " the OLD text links are gone -> lang-switch-btn count = "
|
||||
curl -s -H "$H" -H "Cookie: $SESS" "http://$IP/launcher" | grep -c 'lang-switch-btn'
|
||||
echo "##### C — the saved notes, as they stand on this box"
|
||||
grep -o '"last_warning": *"[^"]\{0,80\}' /var/lib/felhom/docker/volumes/felhom-controller-data/_data/data/settings.json 2>/dev/null | head -2; grep -o '"last_error": *"[^"]\{0,80\}' /var/lib/felhom/docker/volumes/felhom-controller-data/_data/data/settings.json 2>/dev/null | head -2
|
||||
@@ -0,0 +1,46 @@
|
||||
PARITY EXCEPTIONS — controller v0.254.0 (R-557 slice 2 release C)
|
||||
|
||||
Every Hungarian fixture rendered NOW, against the fixture as it stood at eb6aa58 (v0.253.0) —
|
||||
itself captured from the UNCONVERTED templates and never regenerated to make a conversion pass.
|
||||
A real diff (LCS), not a line-index compare: the globe ADDS lines, and a line-index compare calls
|
||||
every line below an insertion changed, which measures nothing (redproofs.txt, catch D).
|
||||
The per-session CSRF token is blanked on both sides — it is random per session and can never be
|
||||
a fixture value; what is still pinned is that the field is THERE and WHICH form it sits in.
|
||||
|
||||
[89 fixtures] e.g. app_export.html, app_import_bundles.html, app_import_empty.html
|
||||
- <span class="version">0.247.0</span><form method="POST" action="/settings/language" class="lang-switch"><input type="hidden" name="back" value="/i18n-fixture"><button type="submit" name="lang" value="hu" class="lang-sw
|
||||
+ <span class="version">0.247.0</span><details class="lang-globe">
|
||||
+ <summary class="lang-globe-btn" aria-label="Nyelv" title="Nyelv"><svg class="ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx=
|
||||
+ <ul class="lang-globe-menu">
|
||||
+ <li><form method="POST" action="/settings/language"><input type="hidden" name="_csrf" value="CSRF"><input type="hidden" name="back" value="/i18n-fixture"><button type="submit" name="lang" value="hu" lang="hu" class="lang-globe
|
||||
+ <li><form method="POST" action="/settings/language"><input type="hidden" name="_csrf" value="CSRF"><input type="hidden" name="back" value="/i18n-fixture"><button type="submit" name="lang" value="en" lang="en" class="lang-globe
|
||||
+ </ul>
|
||||
+</details>
|
||||
|
||||
[7 fixtures] e.g. recovery_locked_can.html, recovery_locked_cannot.html, recovery_locked_confirm.html
|
||||
+ <div class="shell-lang"><details class="lang-globe">
|
||||
+ <summary class="lang-globe-btn" aria-label="Nyelv" title="Nyelv"><svg class="ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx=
|
||||
+ <ul class="lang-globe-menu">
|
||||
+ <li><form method="POST" action="/settings/language"><input type="hidden" name="_csrf" value="CSRF"><input type="hidden" name="back" value="/i18n-fixture"><button type="submit" name="lang" value="hu" lang="hu" class="lang-globe
|
||||
+ <li><form method="POST" action="/settings/language"><input type="hidden" name="_csrf" value="CSRF"><input type="hidden" name="back" value="/i18n-fixture"><button type="submit" name="lang" value="en" lang="en" class="lang-globe
|
||||
+ </ul>
|
||||
+</details></div>
|
||||
|
||||
[5 fixtures] e.g. claim_reset_code.html, claim_reset_nocode.html, claim_setup_code.html
|
||||
+ <div class="shell-lang"><details class="lang-globe">
|
||||
+ <summary class="lang-globe-btn" aria-label="Nyelv" title="Nyelv"><svg class="ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx=
|
||||
+ <ul class="lang-globe-menu">
|
||||
+ <li><form method="POST" action="/lang"><input type="hidden" name="back" value="/i18n-fixture"><button type="submit" name="lang" value="hu" lang="hu" class="lang-globe-item current" aria-current="true">Magyar</button></form></l
|
||||
+ <li><form method="POST" action="/lang"><input type="hidden" name="back" value="/i18n-fixture"><button type="submit" name="lang" value="en" lang="en" class="lang-globe-item">English</button></form></li>
|
||||
+ </ul>
|
||||
+</details></div>
|
||||
|
||||
[5 fixtures] IDENTICAL — not one byte moved: catchall_app.html, catchall_unknown.html, launcher_share_password.html, launcher_shared_apps.html, launcher_shared_empty.html
|
||||
|
||||
DISTINCT CHANGE SHAPES: 3.
|
||||
The release declares TWO blocks — the dashboard footer, and the globe on the pages outside the
|
||||
dashboard chrome. The second has two forms, because WHO reads the page decides where its globe
|
||||
posts: /recovery is an AUTHENTICATED route so its reader is the household and writes their
|
||||
setting; /login and /claim are met with no session and write the visitor's own cookie. The five
|
||||
untouched fixtures are exactly the pages that must not change: both guest share pages and the
|
||||
catch-all (R-577).
|
||||
@@ -52,3 +52,43 @@ A. The bulk converter SILENTLY DROPPED the continuation of a multi-line concaten
|
||||
B. My own counting script was CASE-SENSITIVE, so it reported "0 error literals left" while five
|
||||
remained ("occ parancs sikertelen", "hub hiba", "OnlyOffice aldomain nem ismert" x2). The
|
||||
R-565 shape, in the instrument. Re-measured with re.I; the five are converted.
|
||||
|
||||
RELEASE C (v0.254.0), 2026-09-18 — saved notes, and a globe for the language switch
|
||||
|
||||
15. langFor drops the `!s.hasSession(r)` guard (a signed-in household inherits a visitor's cookie)
|
||||
CONVICTS: TestLangForPrecedence/SESSION_→_the_household's_setting,_cookie_NOT_read
|
||||
— langFor = "en", want "hu"
|
||||
16. safeBackPath allows a protocol-relative URL (an open redirect off the box)
|
||||
CONVICTS: TestLangCookieHandler/back_may_only_be_a_same-origin_path
|
||||
— back="//evil.example/x" → Location "//evil.example/x", want "/"
|
||||
17. the R-570 producer is translated (the one saved sentence release C may not touch)
|
||||
CONVICTS: TestR570SentenceStaysHungarian, BOTH arms — the literal is gone AND a bundle key
|
||||
appeared, each named separately
|
||||
18. one byte changed inside the FOOTER parity-exception block (lang-globe-menu → -menu2)
|
||||
CONVICTS: TestI18nParity on all 101 re-captured fixtures
|
||||
19. one byte changed inside the SHELL parity-exception block (shell-lang → shell-lang2)
|
||||
CONVICTS: TestI18nParity naming login at line 11
|
||||
|
||||
TWO LIVE CATCHES IN RELEASE C — neither planted:
|
||||
C. The parity HARNESS rendered the three visitor shells through addLanguageData, the DASHBOARD path.
|
||||
The fixture would have baked a globe posting to /settings/language with a CSRF field — a form the
|
||||
real page never serves. Caught by reading the diff before re-capturing, and independently by
|
||||
TestI18nDirectRenderPagesFollowLanguage, which renders through the REAL executeTemplateLang.
|
||||
The harness now branches on i18nDirectTemplates.
|
||||
D. The first "is the change only the two declared blocks?" measurement compared LINE BY INDEX, and an
|
||||
insertion shifts every line below it — so it reported 60 520 changed lines and measured nothing.
|
||||
Redone as a real (LCS) diff: exactly TWO change shapes across 106 fixtures, and 5 fixtures
|
||||
byte-identical (the two guest share pages and the catch-all — the three that must not change).
|
||||
|
||||
RELEASE C, second round — after the recovery-page finding
|
||||
|
||||
E. LIVE CATCH, not planted: /recovery is in the AUTHENTICATED route table, so its reader is the
|
||||
HOUSEHOLD — but release C's first draft gave it the anonymous globe, which sets a cookie langFor
|
||||
deliberately ignores once there is a session. The button would have done NOTHING. Found by the
|
||||
live probe reporting `/recovery globes: 0` (302 to /login) and then reading the route table.
|
||||
Fixed: executeTemplateLang branches on hasSession — household form with CSRF, or visitor form
|
||||
without. Two tests and the parity harness now carry the same branch.
|
||||
F. The per-session CSRF token cannot be a fixture value. Blanked on both sides of every parity
|
||||
comparison, exactly as relative ages already were; what stays pinned is that the field is THERE
|
||||
and WHICH form it sits in — which is the half that says whether the globe writes the household's
|
||||
setting or the visitor's cookie.
|
||||
|
||||
Reference in New Issue
Block a user