eb806f5780
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
129 lines
8.2 KiB
Markdown
129 lines
8.2 KiB
Markdown
# REPORT — v0.257.0: the app catalog can speak English (R-560, slice 5 Part A)
|
|
|
|
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 this release does
|
|
|
|
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.
|
|
|
|
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 five properties, and why each is load-bearing
|
|
|
|
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.
|
|
|
|
## Where a page may read catalog copy
|
|
|
|
**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.
|
|
|
|
**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.
|
|
|
|
`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.
|
|
|
|
## Tests, and the red-proofs
|
|
|
|
| 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 |
|
|
|
|
**Two of the nine convicted a hollow TEST rather than the code, and that is the finding worth
|
|
keeping.**
|
|
|
|
- `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.
|