docs: localisation slice 2 release B (controller v0.253.0) — R-557 progress, R-575, R-576
gates / gates (push) Successful in 25s
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:
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user