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:
+3
-1
@@ -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 `<details>` — **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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
+50
-1
@@ -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.<copy>` 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
|
||||
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user