docs: v0.257.0 — README, REPORT, CONTEXT for the catalog read path (R-560)
gates / gates (push) Successful in 25s
gates / gates (push) Successful in 25s
README gains "Catalog copy in a second language" under §18: the format, the field-by-field fallback, whole-list replacement vs key matching, the no-write-through rule, the four templates that carry copy, and why an older controller is unaffected. REPORT records the nine red-proofs — including the two that convicted a hollow TEST rather than the code — and the live proof on both demo boxes, naming the method (endpoint level, no browser on DooPlex) rather than implying a click-through. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
@@ -1,56 +1,128 @@
|
||||
# REPORT — localisation slice 3 Part B: the box sends its sentence in the household's language
|
||||
# REPORT — v0.257.0: the app catalog can speak English (R-560, slice 5 Part A)
|
||||
|
||||
**controller v0.256.0** · base `8fb2f9ef9d5a` · 2026-09-18 · R-558 Part B · MinAgent 0.131.0 (unchanged)
|
||||
2026-09-20 · `main` `60f0a86` · image `gitea.dooplex.hu/admin/felhom-controller:0.257.0` · live on
|
||||
demo-hp guest 9201. MinAgent 0.131.0, unchanged.
|
||||
|
||||
## What shipped
|
||||
## What this release does
|
||||
|
||||
The hub half (v0.118.0/0.118.1) writes a household's e-mails in their language. This is the other
|
||||
half: about a third of those mails carry a sentence the BOX composed — naming a drive, an app, a
|
||||
number — and the hub cannot translate one. The box now sends it twice.
|
||||
An app's customer-facing text lives in the catalog's `.felhom.yml` and was Hungarian only. This
|
||||
release lets the same file carry an English twin in an `i18n: {en: …}` sibling block, and makes the
|
||||
dashboard read it when the household is on English.
|
||||
|
||||
- **`message_customer`** on the event wire: the same sentence in the household's language, beside the
|
||||
unchanged Hungarian `message`. `omitempty`, and **a Hungarian household sends nothing extra**, so
|
||||
its payload stays byte-for-byte what every box in the fleet sends today.
|
||||
- **19 producers converted** to bundle keys, both renderings from ONE key.
|
||||
- **`customer.language` bootstraps a new box**: stored choice → config → Hungarian. The config value
|
||||
is never persisted into `settings.json`; doing so would record a choice nobody made.
|
||||
Architecture document read first and named here: `felhom.eu/documentation/architecture/10-localisation.md`
|
||||
— §1 (the Hungarian must not move), §4 (fallback), §5 (the gates), §7 (the catalog's copy model, which
|
||||
this release turns from a proposal into a measured fact), §11 ruling 7 (app names are not translated).
|
||||
|
||||
## The Hungarian did not move, measured twice
|
||||
## The five properties, and why each is load-bearing
|
||||
|
||||
- `TestEventMessageWireTextIsFrozen` — goldens captured at the slice-2 base commit.
|
||||
- **The Go parity gate** — all 19 new keys checked byte-for-byte against the base-commit literals
|
||||
(`688 slice-2 keys listed, 34 pre-existing, 484 named in Go`, gate OK).
|
||||
1. **`For("hu")` is the parsed struct with `I18n` cleared, and nothing else.** Deep-compared against
|
||||
all 53 real catalog files, copied into `internal/stacks/testdata/catalog/`. `LocalizeStacks`
|
||||
returns the INPUT SLICE for Hungarian rather than a copy, so nothing on the Hungarian path is
|
||||
even touched. §1 holds by construction, then measured live.
|
||||
2. **Fallback is field by field, and blank counts as absent.** A half-translated app is a legal,
|
||||
shippable state — that is what makes fifty apps pushable a batch at a time.
|
||||
3. **The three prose lists replace WHOLE; every other list is matched by its own key** — `env_var`,
|
||||
option `value`, `match_group`, `target`, `path`. Position matching mistranslates silently the
|
||||
first time a Hungarian field is inserted above another, and the page still looks right.
|
||||
4. **`For` never writes through.** The metadata is the stack manager's and is shared by concurrent
|
||||
requests; an in-place merge would put one household's language on another's page.
|
||||
5. **An older controller ignores the block.** `LoadMetadata` uses non-strict `yaml.Unmarshal` — this
|
||||
repo constructs no `yaml.Decoder` at all, so there is no `KnownFields` anywhere to turn strict.
|
||||
Proven live below.
|
||||
|
||||
## Three guards had to learn the change, and one caught me
|
||||
## Where a page may read catalog copy
|
||||
|
||||
- **The test seam** `pushFn` now carries `messageCustomer`. A seam that cannot see a new field cannot
|
||||
test it — the lesson this repo recorded one release ago when `PushEvent` had to become the seam.
|
||||
- **The R-329 severity contract** walks call sites by FUNCTION NAME. Converting producers moved two
|
||||
dynamic-severity sites off `PushEvent` and the register reported them as no longer existing — the
|
||||
"a stale register entry is also a failure" half doing its job. The three new helpers are registered;
|
||||
the walk now checks **36** severity literals, up from 20.
|
||||
- **`TestConfigLanguageIsWiredInMain`** reads `main.go` and asserts the setter is CALLED. `cmd/` is
|
||||
gitignored, so ripgrep skips `main.go` and a reviewer's search finds nothing — which is exactly how
|
||||
a seam gets built and never wired in this repo. Red-proofed by deleting the call.
|
||||
**Four templates** carry catalog copy — `dashboard.html` (the app rows' second line), `stacks.html`
|
||||
(the Apps list), `app_info.html` (the app page, including the data-folder cards) and `deploy.html`.
|
||||
A fifth, `app_export.html`, reads `.Stack.Meta.Subdomain`, which is configuration.
|
||||
|
||||
## A mistake, named
|
||||
**Five handler sites** feed them, and each now goes through `LocalizeStacks` / `LocalizeStackPtr` /
|
||||
`meta.For(lang)`: the dashboard, the Apps list, the deploy page, the app page — and the logs page,
|
||||
which today shows only the app's NAME and is routed through the same call anyway, because the next
|
||||
person to add a line to `logs.html` should not have to notice that this one page was the exception.
|
||||
Two producers sit in packages that have no request and therefore no language — the integration rows
|
||||
(`internal/integrations`) and the initial-credentials note (`internal/stacks/initialcreds.go`); both
|
||||
are re-taken from the localised metadata in the handler, identical bytes for Hungarian.
|
||||
|
||||
The first pass of the conversion **dropped the `displayName` argument from three producers**
|
||||
(`app_deployed`, `app_deploy_started`, `app_removed`). Shipped, that would have mailed customers
|
||||
`Alkalmazás telepítve: %!s(MISSING)`. Caught by reading my own diff, and now pinned by a test that
|
||||
refuses any sentence containing `%!`, `MISSING`, `%s` or `%d` in either language.
|
||||
`TestNoDirectMetaCopyReadOnPages` parses every non-test file in `internal/web` and fails on any
|
||||
`x.Meta.<copy-field>` read that is not in a named, reasoned allow-list (three entries today, all
|
||||
`appDetailHandler`'s localised `found`). **What it cannot do, stated in the test:** it reads source,
|
||||
so it cannot tell a localised receiver from an unlocalised one. It catches the ARRIVAL of a new copy
|
||||
read, which is when a human has to think — that is the whole claim made for it.
|
||||
|
||||
## What is NOT converted, and why
|
||||
## Tests, and the red-proofs
|
||||
|
||||
- **The operator-tier types** (15 of them, listed in the hub's `operatorOnlyEvents`): the operator
|
||||
reads Hungarian, and these never reach a customer.
|
||||
- **Producers whose sentence is composed in another package** and reaches the notifier already
|
||||
finished — `backup_failed`, `db_dump_failed`, `backup_integrity_ok`/`_failed`,
|
||||
`offbox_enlarge_blocked`, `local_api_endpoint_drift`. They still send Hungarian only, so an English
|
||||
household can still see one Hungarian line in some mails. **Filed as a row rather than left as
|
||||
prose.**
|
||||
- The R-570 sentence, which stays Hungarian until that row closes.
|
||||
| test | red-proof (each seen to fail, then reverted) |
|
||||
|---|---|
|
||||
| `TestMetaForHuIsIdentity` (53 real files) | a copy field mutated inside `For`'s hu branch — FAILED naming 53 apps |
|
||||
| `TestDeployFieldsMatchedByKey` | deploy fields matched by position — FAILED, "Second field" landed on FIRST |
|
||||
| `TestEmptyEnglishFallsBack` | a blank English string allowed to win — FAILED |
|
||||
| `TestForDoesNotMutateTheReceiver` | the defensive copy removed — FAILED, showing the written-through strings |
|
||||
| `TestOverlayMatches…` (optional_config) | groups matched by position — FAILED |
|
||||
| `TestOverlayMatches…` (integrations) | integrations matched by position — FAILED |
|
||||
| `TestOverlayMatches…` (data paths) | data paths matched by position — FAILED |
|
||||
| `TestLocalizeStacksLeavesHungarianSliceAlone` | `I18n` left on the value handed to the page — FAILED |
|
||||
| `TestNoDirectMetaCopyReadOnPages` | `_ = st.Meta.Description` added to `logsHandler` — FAILED naming file and field |
|
||||
|
||||
## Green
|
||||
**Two of the nine convicted a hollow TEST rather than the code, and that is the finding worth
|
||||
keeping.**
|
||||
|
||||
`go build` / `go vet` / `go test ./...` clean. All 17 controller gates OK. All 23 decoys behave.
|
||||
- `TestForDoesNotMutateTheReceiver` first did the obvious thing: copy the struct, call `For`,
|
||||
`DeepEqual` the copy against the original. **A struct copy shares its slices' backing arrays**, so
|
||||
a write through `out.DeployFields[i].Label` changes BOTH sides and the compare passes. It passed a
|
||||
deliberately broken merge. It now snapshots the concrete strings before the call.
|
||||
- The optional-config / integrations / data-path fixture first carried ONE entry per list. With one
|
||||
entry, position and key agree, and a position-matching implementation passes. It passed. The
|
||||
fixture now carries two entries per list with only the second translated.
|
||||
|
||||
Full suite, vet and build green; `controller_gates.py` green (17 gates).
|
||||
|
||||
## Live proof — endpoint level, and that is stated because it matters
|
||||
|
||||
No browser exists on DooPlex. Every page was fetched with `curl` from inside guest 9201 at the
|
||||
controller's container address, with the customer `Host` header and a real logged-in session: the
|
||||
same handlers and the same templates a browser drives, with only the drawing skipped.
|
||||
|
||||
- **demo-hp, 0.257.0.** With the catalog pilot synced, `/apps/privatebin`, `/apps/paperless-ngx`,
|
||||
`/apps/romm`, `/stacks` and two deploy pages render the English catalog text on `?lang=en`.
|
||||
- **The Hungarian did not move.** The same seven pages in Hungarian, before and after the catalog
|
||||
push, are byte-identical apart from the per-session CSRF token: equal raw byte counts (43 253 /
|
||||
41 532 / 41 547 / 147 121 / 74 326 / 70 290) and equal hashes once the token is normalised.
|
||||
- **demo-felhom, 0.255.0 — the old-controller check.** With the block synced onto that box
|
||||
(confirmed positively: the sync named the three apps, and the block is in both the cache and the
|
||||
stack copy), its pages hash identically before and after, and its log carries **no** parse warning
|
||||
— in a 93-line window that contains the sync's own lines, so the absence is evidence and not a
|
||||
dead log.
|
||||
|
||||
Evidence: `felhom.eu/documentation/audits/i18n-slice5-2026-09-20/`.
|
||||
|
||||
## What is still Hungarian on an English app page
|
||||
|
||||
Measured with an accent scan AND an ASCII-folded stem scan, both with a positive and a negative
|
||||
control. Three things, and the first is correct:
|
||||
|
||||
1. **the language picker's own „Magyar" button** — a picker names each language in its own tongue;
|
||||
2. **the update badge „Naprakész" and its tooltip** — **R-589**. Slice 1 listed it (§2.3); slice 2
|
||||
was the slice that was to take it, and closed without it;
|
||||
3. **the data-folder card's consequence sentence** — **R-590**. This one makes a promise about the
|
||||
customer's files, and the label above it is already English, so the line reads half and half.
|
||||
|
||||
The Apps LIST also shows fifty Hungarian descriptions — those are the apps nobody has translated yet.
|
||||
|
||||
## Gaps filed
|
||||
|
||||
**R-589** (the update badge) · **R-590** (the data-folder promise) · **R-591** (`Stack.Copy()` deep-copies
|
||||
five `Meta` fields and not the new `I18n` map — safe today because nothing mutates an overlay, which is
|
||||
exactly why it is worth a row) · **R-592** (three defects inside the new catalog gate, each found by its
|
||||
own decoy).
|
||||
|
||||
## Needs the operator
|
||||
|
||||
**Raise the fleet floor to 0.257.0.** Until then only demo-hp can read an English app block at all;
|
||||
every other box keeps showing Hungarian app text to an English household. Nothing breaks if it waits.
|
||||
|
||||
## Teardown
|
||||
|
||||
Nothing installed or removed on any box; no app deployed. Guest 9201 on demo-hp was restarted onto the
|
||||
new image, which is the deploy. The staged password file and the capture scripts were shredded from
|
||||
both Proxmox hosts and both guests. Both boxes left on Hungarian.
|
||||
|
||||
Reference in New Issue
Block a user