Files
felhom-controller/REPORT.md
T
admin ecefdf2dc5
gates / gates (push) Successful in 24s
docs: the floor is at 0.257.0 and it delivered the feature (R-560)
The operator asked for the floor at the end of the translation work. Raised the same
session with the declared MinAgent 0.131.0 — above the vouched golden, so the
declaration carries it. demo-felhom went 0.255.0 -> 0.257.0 by itself in under 12
seconds and THEN rendered the English tagline, which is the observable worth having:
the floor delivered the feature, not a version string.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
2026-09-20 16:40:07 +02:00

9.5 KiB

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).

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.

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.