From eb806f5780e9670ff91ba29d0fe9d7153c996347 Mon Sep 17 00:00:00 2001 From: kisfenyo Date: Sun, 20 Sep 2026 14:43:52 +0200 Subject: [PATCH] =?UTF-8?q?docs:=20v0.257.0=20=E2=80=94=20README,=20REPORT?= =?UTF-8?q?,=20CONTEXT=20for=20the=20catalog=20read=20path=20(R-560)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS --- CONTEXT.md | 4 +- REPORT.md | 156 +++++++++++++++++++++++++++++++------------ controller/README.md | 51 +++++++++++++- 3 files changed, 167 insertions(+), 44 deletions(-) diff --git a/CONTEXT.md b/CONTEXT.md index 32b41d6..1c1fd78 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -7,7 +7,9 @@ > > Ask Claude Code: "Please update CONTEXT.md with what we did today" -Last updated: 2026-09-18 (v0.255.0 — the globe on the sign-in-flow pages: styled, and inside the card) +Last updated: 2026-09-20 (v0.257.0 — the app catalog can speak English) + +> **2026-09-20 — v0.257.0 (R-560 slice 5 Part A).** The catalog's `.felhom.yml` may now carry an `i18n: {en: …}` sibling block, and the dashboard reads it. `Metadata.For(lang)` merges FIELD BY FIELD (absent or blank English shows Hungarian, so a half-translated app ships); `For("hu")` is the parsed struct with `I18n` cleared and nothing else, deep-compared over all **53 real catalog files** copied into `internal/stacks/testdata/catalog/`; the three prose lists replace WHOLE and every other list is matched by its own key (`env_var`, option `value`, `match_group`, `target`, `path`) — **never by position**; `For` never writes through the manager's shared metadata. Pages reach copy only via `LocalizeStacks`/`LocalizeStackPtr`/`MetaFor`, and `TestNoDirectMetaCopyReadOnPages` AST-parses `internal/web` against a named allow-list so the NEXT page to read catalog copy fails the suite instead of rendering Hungarian to an English household. **Two of the nine red-proofs convicted a hollow TEST, not the code:** a struct copy shares its slices' backing arrays, so the obvious `DeepEqual` mutation check passed a deliberately broken merge; and a ONE-entry fixture cannot tell key matching from position matching. Both rewritten. Proven live on demo-hp (English pages English, seven Hungarian pages byte-identical apart from the CSRF token) and on demo-felhom **0.255.0** (block synced, pages identical, no parse warning in a 93-line log window). Gaps: R-589 (update badge still Hungarian in English), R-590 (the data-folder promise sentence), R-591 (`Stack.Copy()` does not deep-copy the new `Meta.I18n`), R-592 (three defects inside the new catalog gate, each found by its own decoy). **Floor NOT raised — the operator's step; until it moves, only demo-hp reads English app text.** > **2026-09-18 — v0.255.0 (fixes the v0.254.0 globe).** The sign-in-flow pages requested `style.css` with **no `?v=`**, so a browser holding a pre-0.254.0 copy served CSS with no `.lang-globe` rules and the globe rendered as a bare unstyled `
` — **invisible to every test, because they all read markup and the fault was in which CSS file the browser fetched**. It was FIVE shells, not three (both guest share pages too), and `.Version` was missing from three of their data maps — now set once in `executeTemplateLang`. The globe also moved INSIDE the card, centred under the footer, menu opening upward via the shared rule. 15 shell fixtures re-captured, **zero dashboard ones**. R-579. diff --git a/REPORT.md b/REPORT.md index 8e4c30c..a7c57ec 100644 --- a/REPORT.md +++ b/REPORT.md @@ -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.` 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. diff --git a/controller/README.md b/controller/README.md index f55f74d..912603c 100644 --- a/controller/README.md +++ b/controller/README.md @@ -3614,7 +3614,7 @@ without reaching `Images()`. --- -### 18. Dashboard language (i18n) (v0.247.0–v0.256.0) +### 18. Dashboard language (i18n) (v0.247.0–v0.257.0) Design: `felhom.eu/documentation/architecture/10-localisation.md`. Inventory: `felhom.eu/documentation/audits/I18N-INVENTORY-2026-09-17.md`. @@ -3715,6 +3715,55 @@ reads** (see below). captured from the unconverted templates (`internal/web/testdata/i18n_parity/`). Fixtures are never regenerated to make a conversion pass. +#### Catalog copy in a second language (v0.257.0, R-560 slice 5) + +The message bundle above covers text the CONTROLLER owns. An app's own words — its one-line +description, tagline, `use_cases`, `first_steps`, `prerequisites`, `default_creds`, the deploy-field +labels/descriptions/placeholders and select-option labels, `optional_config` group and field text, +integration labels, `data_paths` labels and the initial-credentials note — come from the catalog's +`.felhom.yml` and are Hungarian. They are NOT bundle keys: they belong to the catalog, change on a +catalog push, and a box may run a catalog the controller has never seen. + +- **Format:** a sibling block in the SAME file, so a reviewer sees one app whole: + + ```yaml + description: "Titkosított jegyzet és szöveg megosztás" + i18n: + en: + description: "Encrypted note and text sharing" + ``` + + `Metadata.I18n map[string]MetadataOverlay` (`internal/stacks/metadata_i18n.go`), `json:"-"` — the + untranslated half never travels to a page or an API answer; the merged view does. +- **Read path:** `Metadata.For(lang)`, reached only through `stacks.LocalizeStacks` (a list), + `LocalizeStackPtr` (one stack) or `Stack.MetaFor`. **`For("hu")` is the parsed struct with `I18n` + cleared and nothing else** — `TestMetaForHuIsIdentity` deep-compares it over all 53 real catalog + files in `internal/stacks/testdata/catalog/`. `LocalizeStacks` returns the INPUT SLICE for + Hungarian rather than a copy. +- **Field-by-field fallback:** an English field that is absent — or blank — shows the Hungarian one. + A half-translated app is a legal state, which is what lets the catalog be translated in batches. +- **The three prose lists replace WHOLE** (`use_cases`, `first_steps`, `prerequisites`): merging by + index produces a list nobody wrote. **Every other list is matched by its own key** — + `deploy_fields` by `env_var`, options by `value`, `optional_config` groups by `match_group` (the + Hungarian `group` value they translate; a group has no other identity), its fields by `env_var`, + integrations by `target`, `data_paths` by `path`. Position matching mistranslates silently. +- **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. +- **Pages:** the dashboard rows, the Apps list, the logs page, the deploy page and the app page. + `TestNoDirectMetaCopyReadOnPages` AST-parses `internal/web` and fails on any `x.Meta.` read + outside a named, reasoned allow-list — so a NEW page that reads catalog copy off the manager fails + the suite instead of quietly rendering Hungarian to an English household. +- **Two producers 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. +- **Older controllers are unaffected:** `LoadMetadata` uses non-strict `yaml.Unmarshal` (this repo + constructs no `yaml.Decoder`), so a pre-0.257.0 box drops the whole block. Proven live on a 0.255.0 + box: pages byte-identical before and after the block arrived, no parse warning in a live log. +- **The catalog side** carries the freeze and the gate: `app-catalog-felhom.eu/scripts/copy_freeze/hu.json` + (every Hungarian copy string, captured before any translation) and `scripts/check-copy-i18n.py` + (freeze + structure + language + credential tokens + a coverage ratchet), fifth row of that repo's + `catalog_gates.py`. + ## Repository Layout ```