docs: localisation slice 2 release B (controller v0.253.0) — R-557 progress, R-575, R-576
gates / gates (push) Successful in 25s

All 179 Hungarian error messages carry a key; zero remain. 10-localisation.md gains §10.2: the
four properties util.MsgError had to have at once and the failure each one prevents, and the
plural rule as a BUNDLE rule rather than a per-call-site flag, with the answerable sentence and
what the other option would have cost.

Two instrument defects recorded rather than tidied away, because both shapes recur:

R-576 — the parity gate has a measured blind spot. The bulk converter dropped the continuation
of multi-line concatenations, damaging 7 producers, and the gate stayed GREEN: every surviving
fragment WAS a byte-equal base-commit literal, so its question ("is this text real?") was
answered yes while the CALL had lost half its sentence. Two behaviour tests caught it. The
general form: a structural gate over the TEXT cannot see a defect in the CALL.

And the script counting what was left was case-sensitive, so it said "0 remain" while five did —
R-565's shape inside the measurement. Every "no Hungarian left" claim in this slice is now made
case-insensitively and with both controls.

R-575 — the soft memory-overcommit warning has no error to carry a key and no language where it
is built, so it renders Hungarian on an English page. Named in the code, not hidden.

Live evidence includes a mistake I made and corrected: a probe of the deploy refusal INSTALLED
vaultwarden on demo-hp (the endpoint accepts before it validates), the same mistake the previous
session recorded. Removed through the product's own path with its data; verified gone.

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 11:51:06 +02:00
parent 92ee0f9881
commit 08e9ddbd5c
9 changed files with 261 additions and 32 deletions
@@ -0,0 +1,58 @@
# Live validation — controller v0.253.0 on demo-hp guest 9201 (2026-09-18, release B)
**Method: endpoint-level, stated as such.** No browser on DooPlex. Session from a real `POST /login`;
`X-CSRF-Token` from the page meta for API calls; the password passed as a file and deleted from host
and guest afterwards.
## 0. A mistake I made, first — an app was installed on the demo box and removed
My first release-B probe posted `/api/stacks/vaultwarden/deploy` with empty values, expecting the
required-field refusal. **The deploy API answers 202 and validates ASYNCHRONOUSLY**, and vaultwarden
has no required field — so it installed. **That is the same mistake the 2026-09-17 session recorded
in STATUS.md, repeated.** It is not a trap in the product; it is a trap in the probe, and the lesson
is narrower than "pick a different app": **the deploy endpoint cannot be probed for a refusal at all,
because it accepts before it validates.**
Removed through the product's own path — stop, then `POST /remove` with `remove_hdd_data` and
`remove_backups` — and verified gone: no container, no `/opt/felhom/stacks/vaultwarden`, zero
`vaultwarden` volumes. The 23 standing containers are all up. The removal answered
`hdd_note: „Az alkalmazás nem tárolt saját adatot külső meghajtón…"`, so nothing was on a drive.
The probe was rewritten to use only endpoints whose validator runs BEFORE any mutation
(`probeB2.sh`, `probeB3.sh`).
## 1. An error made deep in a package, rendered on the page in each language
`POST /sharing/shares` with a bad name. The message is produced in `internal/settings/smb.go`, three
layers below the handler, and the redirect is FOLLOWED so what is read is what the page shows.
| refusal | hu | en |
|---|---|---|
| name has a slash | „a megosztás neve nem tartalmazhat perjelet vagy pontot" | "the share name cannot hold a slash or a dot" |
| name empty | „a megosztás neve nem lehet üres" | "the share name cannot be empty" |
| name longer than 15 | „a megosztás neve legfeljebb 15 karakter lehet" | "the share name can be at most 15 characters" |
| name starts with `_` | „a megosztás neve nem kezdődhet aláhúzással — ezek a nevek a rendszernek vannak fenntartva" | "the share name cannot start with an underscore — those names are reserved for the system" |
Every Hungarian line is the literal that stood in `smb.go` before the conversion, byte for byte.
## 2. The compatibility case, still true after B
`/sharing?flash=Be%C3%A1ll%C3%ADt%C3%A1s+mentve.` — a URL an older controller minted — renders
„Beállítás mentve." in **both** languages. Shown verbatim, never as a key, never dropped.
## 3. What stays English on purpose, confirmed live
`POST /api/stacks/felhom-controller/remove` → `403 stack "felhom-controller" is protected and cannot
be removed`. Internal English, identical in both languages: **R-569**, not this slice. Recorded here
so it is not read as a gap release B left.
An agent-side refusal (`/api/disks/format` on a device that is not whitelisted) comes back as
`agentapi: format: HTTP 400: storage: refusing to operate on non-whitelisted block device` — the
AGENT's sentence, relayed verbatim in both languages. That is the rule working: text this controller
did not write is text it does not translate.
## 4. State left behind
Controller **0.253.0**; saved language **`hu`** (every English probe used the `?lang=en` override,
which is not persisted); 23 standing containers up, unchanged; **vaultwarden installed by mistake and
fully removed**; no drive touched, no floor raised, no golden baked.
@@ -0,0 +1,19 @@
LOGIN OK
##### lang=hu
1 disks/assign, mount name with a slash (internal/web validator): {"error":"uuid and where are required","ok":false}
[400]
2 disks/format, unsupported filesystem (internal/web validator): {"error":"agentapi: format: HTTP 400: storage: refusing to operate on non-whitelisted block device \"/dev/zzz\"","ok":false}
[500]
3 share name with a slash (internal/settings/smb.go validator): err.settings.a_megosztas_neve_nem_tartalmazhat_perjelet
4 share name empty (internal/settings/smb.go validator): err.settings.a_megosztas_neve_nem_lehet_ures
5 remove a PROTECTED stack (internal/stacks, English by design): {"ok":false,"error":"stack \"felhom-controller\" is protected and cannot be removed"}
[403]
##### lang=en
1 disks/assign, mount name with a slash (internal/web validator): {"error":"uuid and where are required","ok":false}
[400]
2 disks/format, unsupported filesystem (internal/web validator): {"error":"agentapi: format: HTTP 400: storage: refusing to operate on non-whitelisted block device \"/dev/zzz\"","ok":false}
[500]
3 share name with a slash (internal/settings/smb.go validator): err.settings.a_megosztas_neve_nem_tartalmazhat_perjelet
4 share name empty (internal/settings/smb.go validator): err.settings.a_megosztas_neve_nem_lehet_ures
5 remove a PROTECTED stack (internal/stacks, English by design): {"ok":false,"error":"stack \"felhom-controller\" is protected and cannot be removed"}
[403]
@@ -0,0 +1,29 @@
#!/bin/bash
# Release B probe, SAFE BY CONSTRUCTION: every request below is refused by a validator that runs
# BEFORE any mutation. Nothing is installed, created, formatted or deleted.
IP=172.17.0.2:8080
H="Host: felhom.enkisfelhom.hu"
PW=$(cat /tmp/.felhompw); J=/tmp/pj.txt; rm -f $J
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; }
curl -s -H "$H" -H "Cookie: $SESS" "http://$IP/launcher" > /tmp/lp.html
MC=$(grep -o 'name="csrf-token" content="[^"]*"' /tmp/lp.html | head -1 | sed 's/.*content="//;s/"//')
SC=$(grep -o 'name="_csrf" value="[^"]*"' /tmp/lp.html | head -1 | sed 's/.*value="//;s/"//')
echo "LOGIN OK"
for L in hu en; do
echo "##### lang=$L"
JP(){ curl -s -H "$H" -H "Cookie: $SESS" -H "X-CSRF-Token: $MC" -H 'Content-Type: application/json' --max-time 12 -X POST "http://$IP$1" -d "$2" -w " [%{http_code}]"; }
FP(){ curl -s -H "$H" -H "Cookie: $SESS" --max-time 12 -o /dev/null -w "%{redirect_url}" -X POST "http://$IP$1" --data-urlencode "_csrf=$SC" "${@:2}"; }
echo -n " 1 disks/assign, mount name with a slash (internal/web validator): "
JP "/api/disks/assign?lang=$L" '{"device":"/dev/zzz","name":"bad/name"}' | head -c 200; echo
echo -n " 2 disks/format, unsupported filesystem (internal/web validator): "
JP "/api/disks/format?lang=$L" '{"device":"/dev/zzz","name":"probe","fstype":"btrfs"}' | head -c 200; echo
echo -n " 3 share name with a slash (internal/settings/smb.go validator): "
FP "/sharing/shares?lang=$L" --data-urlencode "name=bad/name" --data-urlencode "path=/tmp" | sed 's/.*flash[^=]*=//' | head -c 200; echo
echo -n " 4 share name empty (internal/settings/smb.go validator): "
FP "/sharing/shares?lang=$L" --data-urlencode "name=" --data-urlencode "path=/tmp" | sed 's/.*flash[^=]*=//' | head -c 200; echo
echo -n " 5 remove a PROTECTED stack (internal/stacks, English by design): "
JP "/api/stacks/felhom-controller/remove?lang=$L" '{}' | head -c 200; echo
done
@@ -0,0 +1,34 @@
#!/bin/bash
# Release B, the sentence AS RENDERED. Every request is refused by a validator that runs before any
# mutation; the redirect is then FOLLOWED so what is read is what the customer sees on the page.
IP=172.17.0.2:8080
H="Host: felhom.enkisfelhom.hu"
PW=$(cat /tmp/.felhompw); J=/tmp/pj.txt; rm -f $J
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; }
curl -s -H "$H" -H "Cookie: $SESS" "http://$IP/sharing" > /tmp/sp.html
SC=$(grep -o 'name="_csrf" value="[^"]*"' /tmp/sp.html | head -1 | sed 's/.*value="//;s/"//')
echo "LOGIN OK"
# $1 label, $2 form args...
try(){ local label="$1"; shift
for L in hu en; do
LOC=$(curl -s -H "$H" -H "Cookie: $SESS" --max-time 12 -o /dev/null -w "%{redirect_url}" \
-X POST "http://$IP/sharing/shares" --data-urlencode "_csrf=$SC" "$@")
# the URL the browser is sent to, then the page as it renders it
Q=$(echo "$LOC" | sed 's|.*/sharing?||')
TXT=$(curl -s -H "$H" -H "Cookie: $SESS" --max-time 12 "http://$IP/sharing?${Q}&lang=$L" \
| grep -o 'alert alert-success">[^<]*' | head -1 | sed 's/.*">//')
printf " %-34s %s -> %s\n" "$label" "$L" "$TXT"
done
}
try "share name with a slash" --data-urlencode "name=bad/name" --data-urlencode "path=/tmp"
try "share name empty" --data-urlencode "name=" --data-urlencode "path=/tmp"
try "share name too long (>15)" --data-urlencode "name=aaaaaaaaaaaaaaaaaaaa" --data-urlencode "path=/tmp"
try "share name starts with _" --data-urlencode "name=_reserved" --data-urlencode "path=/tmp"
echo "--- and the legacy case: a URL an OLD controller minted, carrying prose ---"
for L in hu en; do
TXT=$(curl -s -H "$H" -H "Cookie: $SESS" --max-time 12 "http://$IP/sharing?flash=Be%C3%A1ll%C3%ADt%C3%A1s+mentve.&lang=$L" | grep -o 'alert alert-success">[^<]*' | head -1 | sed 's/.*">//')
printf " %-34s %s -> %s\n" "legacy prose in the URL" "$L" "$TXT"
done
@@ -0,0 +1,12 @@
LOGIN OK
share name with a slash hu -> a megosztás neve nem tartalmazhat perjelet vagy pontot
share name with a slash en -> the share name cannot hold a slash or a dot
share name empty hu -> a megosztás neve nem lehet üres
share name empty en -> the share name cannot be empty
share name too long (>15) hu -> a megosztás neve legfeljebb 15 karakter lehet
share name too long (>15) en -> the share name can be at most 15 characters
share name starts with _ hu -> a megosztás neve nem kezdődhet aláhúzással — ezek a nevek a rendszernek vannak fenntartva
share name starts with _ en -> the share name cannot start with an underscore — those names are reserved for the system
--- and the legacy case: a URL an OLD controller minted, carrying prose ---
legacy prose in the URL hu -> Beállítás mentve.
legacy prose in the URL en -> Beállítás mentve.
@@ -24,3 +24,31 @@ Each line: what was broken, what convicted, restored green after.
7. internal/web — a Server with no bundle field renders raw keys
planted: s.bundle() returns s.i18n unchanged (no i18n.Shared fallback)
CONVICTS: TestFlashOnAServerWithNoBundleField — showed "flash.share.enabled"
RELEASE B (v0.253.0), 2026-09-18 — errors carry a key
8. util.msgError.Error() returns ENGLISH instead of Hungarian
CONVICTS: TestMsgErrorKeepsKindAndHuText — "%v printed 'That setting is not valid: …'"
9. Unwrap() returns the kind ALONE (the old KindErrorf behaviour)
CONVICTS: TestMsgErrorUnwrapsTheCauseToo — "the wrapped CAUSE is unreachable"
10. ErrText rewrites a FOREIGN error (restic/docker/stdlib) instead of passing it through
CONVICTS: TestErrTextFallsBackVerbatim, both languages
11. an inner error argument is pre-rendered in Hungarian instead of recursively
CONVICTS: TestErrTextRendersAWrappedMessageErrorToo
12. err.Error() put back at a converted display sink (storage_handlers.go:822)
CONVICTS: TestNoErrErrorInPageOutput, naming the file, the line and the call
13. the English plural collapsed to the singular (alert.deadapp.group.other)
CONVICTS: TestPluralEnglish — "4 installed app is not running"
14. Hungarian GIVEN a plural form it must not have
CONVICTS: TestPluralEnglish — "the Hungarian sentence moved"
TWO LIVE CATCHES IN RELEASE B — neither planted:
A. The bulk converter SILENTLY DROPPED the continuation of a multi-line concatenation
(`fmt.Errorf("a: "+ "b: %s", x)` kept only "a: "), damaging 7 producers. Found by
TestR356_ScenarioC and TestR379_ScenarioA, which assert the SENTENCE a customer reads.
The parity gate did NOT catch it: every surviving fragment was byte-equal to a base literal,
so the gate's question ("is this text real?") was answered yes while the CALL had lost text.
All 7 rebuilt as joined keys; the six wrong keys pruned from both bundles.
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.