docs: v0.258.0's report, and what the walk then found
gates / gates (push) Successful in 27s

The release fixed four Go-side leftovers. The walk that judged it on a box installed
from scratch that day found two more of the SAME SHAPE — a composed sentence handed
to a renderer as page data — on the claim page (R-596, P1) and the Backup page
(R-598). That shape now has three instances on file with R-573, which is worth naming
as a class rather than fixing one at a time.

Verdict recorded: not yet ready for an English-speaking tester.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
2026-09-20 19:27:33 +02:00
parent 7c4a33b6d1
commit f6909492cc
2 changed files with 64 additions and 127 deletions
+60 -126
View File
@@ -1,149 +1,83 @@
# REPORT — v0.257.0: the app catalog can speak English (R-560, slice 5 Part A)
# REPORT — v0.258.0: the last four Hungarian things, and the walk that judged them (R-561 slice 6)
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.
2026-09-20 · `main` at the v0.258.0 commit · image `gitea.dooplex.hu/admin/felhom-controller:0.258.0`
· live on demo-hp guest 9201 and on the drill box built from scratch the same day. MinAgent 0.131.0,
unchanged. Hungarian byte-identical.
## What this release does
Architecture read first and named: `felhom.eu/documentation/architecture/10-localisation.md` §1 (the
Hungarian must not move), §4 (fallback), §5 (voice per language), §10.6b (this slice).
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.
## What shipped
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).
Four rows, all of them Go-side — which is why neither the template parity fixtures nor
`TestI18nEnglishPages` could see them, and why **slice 5's LIVE proof is what found them**.
## The five properties, and why each is load-bearing
| row | what | how |
|---|---|---|
| **R-589** | the update badge („Naprakész" + its tooltip) and the abandoned-lifecycle badge | bundle-backed forms in `localeFuncs`, reusing `compareInstalledToTemplate` and `EffectiveLifecycle` so **the decision cannot drift between languages — only the words do**; the badge's `Class` is asserted equal across languages for the same stack |
| **R-590** | the data-folder card's backup sentence — **a promise about the customer's files** | `consequenceFor` takes its lookup as a parameter; the test asserts the CONSEQUENCE in both languages, and that the two languages do not produce the same string |
| **R-573** | the agent-channel and endpoint-drift banners | keyed by the checker's own CLASSIFICATION, with the composed sentence kept as a **fail-open** fallback: an unmapped reason renders it verbatim rather than a blank banner or a raw key |
| **R-572** | **not what the row said** | the row claimed a template renders „vasárnap" on an English page. Measured: **no template and no Go file called `pruneLabel` or `nextPruneLabel`.** Dead func-map entries returning Hungarian — **deleted**, not translated |
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.
`lifecycleBadge` was not in any row. It is the same builder one function from `updateBadge`, and
leaving a known identical defect there would have been a choice.
## Where a page may read catalog copy
## Why deleting was the right direction for R-572, and how that was proven
**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.
Translating dead code would have added machinery with no reader and a test pinning a fiction.
Deletion is the **fail-loud** direction: `html/template` refuses to parse a template naming an
undefined function, and `loadTemplates` parses every template once per language at startup. Proven
rather than asserted — a template naming the removed function makes the parity test panic with
`function "pruneLabel" not defined`. `fmtDuration`, named in the same row, produces no Hungarian at
all ("< 1s", "5s", "2m 3s") and was left alone.
**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.
## Red-proofs — five, each seen to fail and then revert
`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) |
| what was broken | what convicted |
|---|---|
| `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 |
| the `updateBadge` override removed from `localeFuncs` | English read „Naprakész" |
| `MessageKey` dropped from `SetAgentChannelAlert` | English read the Hungarian sentence |
| the folder sentence hard-coded back to Hungarian | the English arm convicted on three assertions |
| one Hungarian byte changed in a badge key | `TestLocaleFuncsHungarianBundleMatchesFuncMap` named both sides |
| a template naming a deleted helper | startup panic in the parity test |
**Two of the nine convicted a hollow TEST rather than the code, and that is the finding worth
keeping.**
**The fourth says something about the gate, not the code.** `i18n_go_parity.py` did NOT convict that
changed byte, because `localeFuncs` keys sit under `_preexisting` and are deliberately not measured
against the base capture. The citation to the test is what carries them — so the test had to be made
real, and it was extended to compare `hu.json` against the frozen funcmap forms word for word.
- `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.
## Proven live, twice
Full suite, vet and build green; `controller_gates.py` green (17 gates).
- **demo-hp (0.258.0):** the English app page shows "Up to date" with an English tooltip while the
Hungarian page still says „Naprakész"; the folder sentence is English on one and unchanged on the
other; the free-space line reads "47.9 GB free" / „47.9 GB szabad". After the fixes, a scan of the
English app pages and the Apps list finds **zero** Felhom-authored Hungarian — the only hit is the
language picker naming itself „Magyar", which is correct.
- **A box installed from scratch that day**, walked by an English speaker: the update badge and the
time helpers were English there too, on hardware that had never run an older controller.
## Live proof — endpoint level, and that is stated because it matters
## What the walk then found, which this release does not fix
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.
The walk is `felhom.eu/documentation/audits/DRILL-first-hour-en-0258-2026-09-20.md`. Its verdict is
**not yet ready for an English-speaking tester**, and the blocker is in this repo:
- **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.
- **R-596 (P1)** — `internal/web/claim.go` carries **sixteen** raw Hungarian literals, every one a
message the claim page shows. The page's chrome is English; its answers are not. Same shape as
R-573, one screen earlier in the journey, on the one screen between a household and their box.
- **R-598** — `backup_handlers.go` (12) and `backup_target_offer.go` (10): the Backup page's two
protection warnings and its two target names.
Evidence: `felhom.eu/documentation/audits/i18n-slice5-2026-09-20/`.
Both are the *composed-sentence-into-page-data* shape. That shape now has three instances on file
(R-573, R-596, R-598) and is worth naming as a class the next converter looks for.
## What is still Hungarian on an English app page
## Needs the operator
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).
## The floor was raised to 0.257.0, and it delivered function rather than a version string
The operator asked for the floor at the end of the translation work, so it was raised the same
session: `min_controller_version=0.257.0`, `min_agent=0.131.0` — the value this release's CHANGELOG
header declares. The floor is **above the vouched golden (0.246.0)**, so the declaration is what
carries it (R-472); an undeclared floor there is held by design.
Observables, both positive:
- Hub: `managed floor SERVED for demo-felhom: floor 0.257.0, agent requirement "0.131.0" from
declared (golden 0.246.0)`.
- Box: **demo-felhom went 0.255.0 → 0.257.0 by itself in under 12 seconds**, healthy, its own log
reading `settle-gate: GO — at/above floor 0.257.0 (we are 0.257.0)`.
- **And then it rendered the English tagline** for an app whose block it had just synced. That is the
observable that matters: the floor delivered the FEATURE, not just a version number.
**Peti's box and tester-1 are DOWN** and take it unattended when they return — untested on this
version, which is the standing situation for every floor raise.
## The whole catalog followed the same day
The operator read the pilot's English and said go, so Part C shipped too: 50 more apps in three
pushes, 1 031 of the catalog's 1 032 strings. **The English Apps list now shows zero Hungarian app
descriptions across all 53 apps** — the only Hungarian left on it is this release's own two gaps
(R-589's „Naprakész" badge, ten occurrences) and the language picker naming itself.
**Raise the fleet floor to 0.258.0.** Nothing breaks if it waits — the four fixes simply do not reach
the other boxes. The drill box already ran 0.258.0, because the golden baked today is 0.258.0 and a
golden above the floor is served normally (`managed floor SERVED … floor 0.257.0 … golden 0.258.0`).
## 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.
Nothing installed or removed on demo-hp; guest 9201 restarted onto the new image, which is the
deploy. The drill VM and its hub customer are torn down in three layers — see the drill document.
Every staged credential was shredded from both hosts and both guests.