Localisation slice 5: the catalog's copy model is MEASURED, not proposed (R-560)
gates / gates (push) Successful in 25s

10-localisation.md §7 goes from [DESIGN, proposed] to [FACT], with the numbers it was
specced against corrected — and one correction chose an instrument rather than a
footnote. The catalog has 1 032 copy strings, not 835. 832 carry a Hungarian letter,
which was right. But the ASCII-ONLY Hungarian is ~120 strings, not three: „Aldomain"
appears 53 times and „A szerver domain neve" 53 times, and the three the plan named
(„Igen"/„Nem"/„Nincs") do not occur in this catalog at all. An accent-only gate passes
every one of them inside an English block — R-565's blind spot arriving again in a
different repo.

New §10.6 records what was proven live rather than reasoned about: the English pages
show English; the seven Hungarian pages are byte-identical before and after the push
apart from the per-session CSRF token; and a 0.255.0 box with the block synced onto it
renders identically and logs no warning, in a 93-line window that contains the sync's
own lines, so the absence is evidence and not a dead log.

Rows R-589 (the update badge is Hungarian on an English page), R-590 (the data-folder
card's backup promise, likewise, and it is a promise about the customer's files),
R-591 (Stack.Copy() deep-copies five Meta fields and not the new I18n map — safe
today, which is precisely why it is a row), R-592 (three defects inside the new
catalog gate, closed the same session, each found by its own decoy). R-560 updated:
Parts A and B done, Part C waiting on the operator's read of the pilot.

STATUS asks for that read, and for the floor to 0.257.0.

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 14:44:20 +02:00
parent 31eeb36e88
commit f538a03bd5
44 changed files with 45985 additions and 67 deletions
+5 -1
View File
@@ -736,7 +736,7 @@ class (an image `VOLUME` at an unmounted path) is still live — `immich-server`
| **R-557** | **[P3-LOW] Localisation slice 2 — Go-side customer strings follow the language.** PLAN 2026-09-17 (10 §10). Inventory §2.2: 947 shown + 184 error literals in 84 files; 237 format strings, 111 concatenations, 42 numeric `%d` (English plurals). Flash messages travel inside the redirect URL (`?flash=<Hungarian>`) and must become keys; `cloudflare/countries.go` (113 country names); alert texts; handler errors printed with `err.Error()`. **After R-553** (the four compare-not-show sites). Cost 16–20 CC-hours. **DEPENDENCY added 2026-09-17 (R-553 shipped, v0.251.0):** the behaviour-by-wording sites are fixed, so this slice is unblocked — EXCEPT one producer: `"Sikeres — nincs mentésre jelölt alkalmazás"` (controller/internal/backup/offbox.go) must stay Hungarian until **R-570** closes, because the page's legacy fallback still reads it on boxes that have not run off-site since 0.251.0. Everything else this slice touches is now decided by a kind, not by its words. **RELEASE A SHIPPED 2026-09-18 (controller v0.252.0):** 226 Go literals converted -- flash-as-key (8 writers, 8 readers, legacy prose still shown verbatim), page data and view-model text, the `internal/api` JSON answers, the alert banners (`Alert.MessageKey`), 237 country names at DISPLAY (the cloudflare table is untouched -- only CODES are on the wire, the task claimed otherwise), and the four app-named page titles (R-566 CLOSED). New gate `i18n_go_parity.py` + `i18n_go_base.json` (7 467 base-commit literals, frozen) refuses any key whose Hungarian is not byte-identical; three decoys. Wire goldens freeze `internal/monitor` and `internal/notify` -- the hub MAILS the event message when it has no `customerMessages` entry, so those stay Hungarian until R-558. `HU_FORMAL_CEILING` 16 -> 18 (measured, no word changed). **RELEASE B SHIPPED 2026-09-18 (controller v0.253.0): all 179 error literals carry a key** via `util.MsgError` -- `Error()` is still the Hungarian byte for byte, `errors.Is` answers for the kind AND a wrapped cause, an error ARGUMENT renders recursively, and a foreign error (restic/docker/ssh/stdlib) prints verbatim. 76 display sites go through `errText`, pinned by `TestNoErrErrorInPageOutput`. `memoryVerdict` returns an error; `UpdateRefusal` gained a `Cause`. Plurals are a BUNDLE rule (a key with `.one`/`.other` takes its count first), not a call-site flag. **ZERO Hungarian error literals remain** (ASCII search with both controls, case-insensitive). Two tooling defects found and filed: R-576 (the converter dropped concatenations and the gate could not see it) and the case-sensitive counter. New gap: R-575. **RELEASE C SHIPPED 2026-09-18 (controller v0.254.0) -- SLICE 2 CLOSED.** ~70 saved-note producers write in the BOX language at write time (s16 option 1; a household that switches sees the previous run's note in the old language until the next run). `EndRestoreOp` takes no Hungarian literal from anywhere. The switch became a GLOBE on the dashboard and on the pages outside the dashboard chrome; a visitor with no session gets a display-only `felhom_lang` cookie that `langFor` reads ONLY when there is no session, and a successful CLAIM carries it into the setting (s16). Two parity blocks, measured with a real diff: 3 change shapes across 106 fixtures (footer; the recovery globe posting to the household switch; the login/claim globe posting to /lang), 5 byte-identical. **A DEADLOCK was introduced and caught by the suite hanging (R-578).** Gaps filed: R-572..R-578. | **CLOSED 2026-09-18 - controller v0.252.0 + v0.253.0 + v0.254.0** |
| **R-558** | **[P3-LOW] Localisation slice 3 — the hub's customer e-mails follow the household's language; the operator sets it at customer creation.** PLAN 2026-09-17 (10 §3, §10), operator decision 2. The box already reports `"language"` (controller v0.247.0); nothing hub-side reads it. Build: a per-customer language on the hub (default hu) set at creation and rendered into `controller.yaml` beside `customer.*` (`hub/internal/configgen/configgen.go`), used by the box only while `settings.json` has no choice; the dispatcher picks the language the box REPORTS; English for the 39 `customerMessages`, the 4 severity labels, the body wrapper, the claim/re-enroll/reset/claimed/self-bind mails and the public bind page (inventory §2.3). Delete the `language` allowlist entry in `wire_contract_gate.py` when the field is read. Cost 6-8 CC-hours, one hub + one controller release. **THE WIRE SURVEY (R-557 release A, 2026-09-18) HANDS THIS ROW ITS INPUT LIST:** (1) `internal/notify/notifier.go` -- 31 event messages. The task assumed the hub composes its own mail from the event kind; it does NOT. `FormatCustomerEmail` uses `customerMessages[eventType]` when it has one and **falls back to the controller's message when it has none**, and appends it as „- Üzenet: %s” whenever the two differ -- several types carry no entry precisely so the controller's dynamic sentence IS the mail (hub `api/handler.go` L2034/L2045/L2066). So an English household's mail is Hungarian until this row ships. (2) `internal/monitor/healthcheck.go` -- 5 storage sentences in report `health.warnings`/`health.issues`, operator-facing. Both are frozen by wire goldens in those packages, so no later slice can translate them by accident; this row is what unfreezes them. **SLICE 3 SHIPPED AND CLOSED 2026-09-18 — hub v0.118.0/v0.118.1 + controller v0.256.0/v0.256.1.** The hub reads the language the box reports and writes the household's e-mail in it; nothing an operator reads moved. 56 mail goldens per language captured from hub v0.117.0 BEFORE any string moved, and all 56 Hungarian ones pass unchanged. Order: last reported -> created-with -> hu. `message_customer` carries the sentences the box composes and the hub cannot translate; 19 producers render both from ONE key, and their Hungarian is byte-identical by the Go parity gate. A Hungarian household sends no second copy, so the fleet's payload is unchanged. The bind page is per-language with `expired` pinned to Hungarian (the language would otherwise be the oracle the text refuses to be). Proven live twice: a real English e-mail read in the inbox and the Hungarian one 74 s later (Part A), and the box's push log showing `[hu-only]` then `[+household(en)]` for the same event 57 s apart (Part B). Gaps filed: R-581 (received_at ties), R-582 (the English guard), R-583 (the test mail), R-584 (credential litter), R-585 (six unconverted producers). R-555 closed with it. | **CLOSED 2026-09-18 - hub v0.118.1 + controller v0.256.1** |
| **R-559** | **[P3-LOW] Localisation slice 4 — the console banner and the download page in English.** IN SCOPE — operator ruling 2026-09-17 evening („yes", 10 §11 1b). PLAN 2026-09-17 (10 §10, open decision 1b): the starter listed them; the operator's scope ruling names „the controller, emails, guide, app catalog" and not them. Inventory §2.4: 34 banner lines (`scripts/iso/felhom-bootstrap.sh`, printf to the console; `/etc/issue` byte-coupled to `iso/pkg/debian/postinst`, console font avoids ő/ű) and 32 strings on `website/letoltes.html`. Cost 4–6 CC-hours plus an ISO release train. **SLICE 4 SOURCE SHIPPED 2026-09-18 (ISO 1.29.0 source + the English download page); the IMAGE IS NOT PUBLISHED — that is the operator's step.** The three console texts are bilingual: Hungarian block byte-for-byte as before, then English, inside the same frame; the GRUB entries gain an English half; `postinst`'s byte-coupled copy of the issue text changed identically. The Hungarian is a GOLDEN captured before any English existed, and the harness also pins no-Hungarian-letter-in-English (Hungarian block as the positive control), ≤ 80 columns, and ≤ 25 ROWS — the pairing banner is 24 on a 25-row console, one row of margin. `felhom.eu/en/download` is LIVE, linked both ways, and a site gate refuses the two download pages naming different installer files or checksums. **The release gate's G16 required every Felhom string to be Hungarian and would have stopped this**; ruling 1b (2026-09-17) supersedes that scope, so G16 was REWRITTEN, not waived. Built and gate-checked mechanically: `felhom-installer-1.29.0-pve9.2-1.iso`, sha256 `c67ceaa3…fb02`. Publishing needs a proof install on BOTH menu entries plus a reboot. Gaps filed: R-586, R-587, R-588. **PUBLISHED 2026-09-18: ISO 1.29.0, sha256 `dceacae5…e94829`, live at iso.felhom.eu, and both download pages now name it.** Proof installs on BOTH menu entries (graphical VM 323, text VM 325), each with first boot AND one reboot: `/etc/issue` bilingual with 0 hits for 8006, `pvebanner` masked, package 1.29.0 installed, unit enabled and fired, pairing code present, and the installed script byte-identical to repo HEAD. G11 round trip: the downloaded bytes hash to the published checksum. **A defect was caught between builds by looking at the screen**: the second menu entry's English half was cut off at `Instal` by the GRUB box width, so it was shortened and the image rebuilt. VMs destroyed, the two unclaimed appliance registrations discarded, guest 9201 untouched (23 containers before and after). | **CLOSED 2026-09-18 — ISO 1.29.0 published** |
| **R-560** | **[P3-LOW] Localisation slice 5 — catalog cards, settings and first steps in English.** PLAN 2026-09-17 (10 §7, §10). Inventory §2.5: 835 strings, ~4 984 words across all 53 `.felhom.yml` (use_cases 262, first_steps 233, deploy_fields descriptions 79 / labels 68, prerequisites 60, description 56, tagline 53). Proposed format: an `i18n: {en: {...}}` block inside each `.felhom.yml` (controller's `yaml.Unmarshal` ignores unknown keys, so older controllers are unaffected), field-by-field fallback. Needs a controller change to read it and catalog copy gates with an English rule. Cost 12–16 CC-hours. | **READY - rank P3-LOW; owner: CC (catalog + controller)** |
| **R-560** | **[P3-LOW] Localisation slice 5 — catalog cards, settings and first steps in English.** PLAN 2026-09-17 (10 §7, §10). Inventory §2.5: 835 strings, ~4 984 words across all 53 `.felhom.yml` (use_cases 262, first_steps 233, deploy_fields descriptions 79 / labels 68, prerequisites 60, description 56, tagline 53). Proposed format: an `i18n: {en: {...}}` block inside each `.felhom.yml` (controller's `yaml.Unmarshal` ignores unknown keys, so older controllers are unaffected), field-by-field fallback. Needs a controller change to read it and catalog copy gates with an English rule. Cost 12–16 CC-hours. **PART A SHIPPED 2026-09-20 (controller v0.257.0)** — `Metadata.For(lang)` merges the block FIELD BY FIELD (absent or blank English shows Hungarian, so a half-translated app is shippable); `For("hu")` is the parsed struct with `I18n` cleared, measured against all 53 real catalog files copied into `internal/stacks/testdata/catalog/`; 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 shared metadata. Pages reach copy only via `LocalizeStacks`/`LocalizeStackPtr`/`MetaFor`, pinned by `TestNoDirectMetaCopyReadOnPages`'s named allow-list. Eight red-proofs; TWO of them convicted a hollow TEST rather than the code (a struct copy shares its slices' backing arrays, so the obvious DeepEqual check passed a broken merge; a one-entry fixture cannot tell key matching from position matching). **PART B SHIPPED 2026-09-20 (catalog `e81d41e`)** — the freeze (`scripts/copy_freeze/hu.json`, its own commit, 1 032 strings across 53 apps), the `copy-i18n` gate (freeze + structure + language + credentials + ratchet) with 18 decoy cases, and the PILOT: privatebin, paperless-ngx and romm, 89 strings, `EN_MISSING_CEILING` 1032 → 943. **PROVEN LIVE on demo-hp** (0.257.0): the three English app pages and the Apps list show the English catalog text, and the HUNGARIAN pages are byte-identical before and after the push apart from the CSRF token (7 pages). **PROVEN on an OLD controller** (demo-felhom, 0.255.0): with the block synced onto the box, its pages hash identically before and after and its log carries no parse warning — in a 93-line window that includes the sync lines, so the absence is real. **MEASURED AGAINST THIS ROW'S OWN NUMBERS:** 1 032 copy strings, not 835; 832 of them carry a Hungarian letter (that half was right); the ASCII-only Hungarian is ~120 strings, not three — „Aldomain" and „A szerver domain neve" alone are 53 each, and „Igen"/„Nem"/„Nincs" do not occur in this catalog at all. **PART C (the other 50) IS NOT STARTED** — the pilot stops for the operator's read of the three apps' English (§16 default). Gaps filed: R-589, R-590, R-591, R-592. | **PART A+B DONE 2026-09-20 (ctrl v0.257.0 + catalog pilot) — PART C waits on the operator's read of the pilot; rank P3-LOW; owner: CC** |
| **R-561** | **[P3-LOW] Localisation slice 6 — the volunteer guide in English, then a stranger's first hour in English; closes R-516.** PLAN 2026-09-17 (10 §10). `runbooks/VOLUNTEER-first-hour.md`: 44 paragraphs, ~1 579 words. Then the 2026-09-14 walk repeated by an English speaker on a fresh box, R-516 closed against `audits/I18N-INVENTORY-2026-09-17.md`. Depends on R-556..R-560. Cost 6–8 CC-hours plus the attended walk. | **READY - rank P3-LOW; owner: CC** |
| **R-562** | **[P3-LOW] Dates and sizes are not formatted for any locale — and the Hungarian pages disagree with themselves.** FOUND 2026-09-17 by the i18n inventory §2.8: the two template date layouts differ (`2006. 01. 02. 15:04` Hungarian vs `2006-01-02 15:04` ISO); 10 layout literals in `internal/web` Go and 25 elsewhere pick formats ad hoc; sizes print a decimal POINT (`%.1f GB`, 4 helpers) where Hungarian uses a comma; `timeAgo`/`nextRunLabel`/`pruneLabel` produce Hungarian words outside the three converted pages. Not changed by v0.247.0 (Hungarian bytes are frozen by the parity rule). **Fix shape:** one date and one size formatter per language in `internal/i18n`, the Hungarian output deliberately changed in ONE reviewed release with the parity fixtures re-captured for that release only and the change named in its CHANGELOG. Needs an operator word on the Hungarian format (comma, date style). | **READY - rank P3-LOW; owner: CC** |
| **R-564** | **[P3-LOW] The retrieval-promise gate's Hungarian stems cannot see a SPLIT verb — „csak akkor állíthatók vissza", „hozod vissza" — so those Hungarian sentences were never scanned; the English translation exposed them.** FOUND 2026-09-17 by slice 1 release B (R-556): after the gate learnt English (`EN_PATTERNS`), seven English retrieval phrases on `backups_remote`, `backups_escrow`, `backups_restore` and `backups_restore_wizard` had NO Hungarian registration, because their Hungarian carries the verb particle after the verb („A távoli mentések csak akkor állíthatók vissza …", „a távoli mentések CSAK ezzel a kóddal állíthatók vissza", „csak a hiányzó fájlokat hozod vissza"). The stems (`visszaállíthat`, `visszaszerezhet`, `visszahozhat`, `visszanyit`) match only the joined form. The seven were registered in English with reasons (none is a false promise: two are preconditions, five describe the action on the same page). **Fix shape:** add split-form patterns to the Hungarian scan (`állíthatók? vissza`, `(hoz|szerez|nyit)\w* vissza`), register the Hungarian occurrences found, decoy with a planted split-verb promise. | **READY - rank P3-LOW; owner: CC** |
@@ -764,6 +764,10 @@ class (an image `VOLUME` at an unmounted path) is still live — `immich-server`
| **R-586** | **[P2-MED] The ISO bootstrap harness captured the console to a FILE, so each banner erased the one before it — two checks were RED for two days and nobody saw.** FOUND 2026-09-18 starting slice 4 (R-559): running `scripts/iso/test/bootstrap-modes.sh` unchanged at `183727db9c44` reported `FAIL: R-496: banner painted to the console seam` and `FAIL: R-496: banner names the Tulajdonosi jelmondat`. **Cause:** the script paints each banner with `> "$CONSOLE_DEV"`. On a real console that is a character device and truncation is a no-op, so every paint appears; pointed at a plain file, as the harness did, each paint TRUNCATES. Commit `c033b3b` (ISO 1.28.0, R-535, 2026-09-16) added `print_bound_banner`, which paints immediately after the pairing banner in the same invocation — from that commit the pairing banner was wiped before the check read it. `c033b3b` did not touch the harness. **Why it survived: the harness is in NO gate and NO CI run** — not in `repo_gates.py`, not in `.gitea/`; it runs only when a person runs it, and between 09-16 and 09-18 nobody did. FIXED in the same session: the harness points `FELHOM_CONSOLE_DEV` at a FIFO with a background reader, which restores device semantics (opening a FIFO with `>` truncates nothing) and lets a test see EVERY paint — which slice 4's golden checks then needed anyway. Production code untouched. **What is still open: the harness remains outside every gate.** It needs a container, so it cannot join `repo_gates.py --fast`, which is what both the pre-push hook and CI run — meaning a non-fast entry would still never execute. **Fix shape:** either give CI a container-capable job that runs it, or make the ISO release gate's G16 the place it is required (done for G16 this session — so it now runs at least once per ISO release, which is better than never but later than a push). | **READY - rank P2-MED; owner: CC** |
| **R-587** | **[P3-LOW] Two root-password files sit in the directory the public ISO is published FROM.** FOUND 2026-09-18 running the ISO release gate's credential criteria for slice 4: `/mnt/5_hdd/felhom.eu/felhom-iso/out/` holds `felhom-pve-9.2-1-v1.24.0-nested-probe-generic.iso.rootpw.txt` and `felhom-pve-9.2-1-v1.25.0-nested-vm-generic-mkimage.iso.rootpw.txt` from 2026-07-22/23, mode 0600, left by two appliance-mode builds. **They are NOT on the bucket** — both `https://iso.felhom.eu/<name>` return **404**, against a control (`felhom-installer-1.28.0-pve9.2-1.iso.sha256`) that returns 200, so the check is real and not a dead probe. **The only thing keeping them off a PUBLIC bucket is the `--include "felhom-installer-<VER>*"` pattern in the publish command**, and they are named `felhom-pve-*`, so the pattern misses them by an accident of naming rather than by design. A publish typed without the include, or an include widened to `felhom-*`, uploads root passwords to a world-readable bucket. **Fix shape:** shred the two files (they are three months old and their VMs are long gone), and make the release build refuse to run — or the publish step refuse to start — while any `*.rootpw.txt` exists in the out directory. A pattern that protects by coincidence is not a control. **The two files were SHREDDED 2026-09-18** from the publish source directory, immediately before the 1.29.0 upload ran from it. The directory now holds no `*.rootpw.txt`. **The mechanism half is still open**: nothing stops the next appliance build leaving one there, and nothing refuses a publish while one exists — the `--include` pattern still protects by coincidence. | **PARTLY DONE 2026-09-18 (files gone; the guard is not built) - rank P3-LOW; owner: CC** |
| **R-588** | **[P3-LOW] ISO release records live in two different places, so "was the gate run for this image?" cannot be answered by looking.** FOUND 2026-09-18: the release records for 1.27.0 and 1.27.1 are directories under `documentation/tests/iso-release-<ver>-<date>/`, and there is none for **1.28.0** — which led me to conclude its gate had not been run. It had: the record is `documentation/audits/evidence-backup-promise-2026-09-16/phaseD-iso-gate.txt`, inside an audit about something else. **The gate's own rule is "a criterion with no recorded observation is a criterion that was not run"**, and that rule is unenforceable while the records have no single home — the question it answers has to be settled by a full-text search for a checksum, which is what it took here. **Fix shape:** one home, `documentation/tests/iso-release-<ver>-<date>/`, and a line in `iso-release-gate.md`'s Result-recording section naming it; optionally a check that every published ISO version has a record directory. | **READY - rank P3-LOW; owner: CC** |
| **R-589** | **[P3-LOW] The update badge is Hungarian on an English app page — and it is the badge, not a corner case.** FOUND 2026-09-20 live on demo-hp (0.257.0) while proving localisation slice 5's pilot: on `/apps/privatebin?lang=en` the only Felhom-authored Hungarian left, apart from the language picker naming itself, was the tag „Naprakész" with the title „Ez az alkalmazás a legfrissebb elérhető változatot futtatja." `controller/internal/web/updatebadge.go` `updateBadgeAt` builds a `MetaBadge` from four RAW Hungarian literals (L82, L84, L87 + the „ — %d napja" suffix, L98-99) with no key, so `executeTemplate`'s language set never sees them. `hu.json` already carries `backups.naprakesz` → 'Up to date' for a DIFFERENT surface, so the English word exists and this producer simply does not use it. Slice 1 listed „Naprakész" among what stays Hungarian (10 §2.3) and slice 2 was the slice that was to take it; slice 2 is CLOSED and it is still there. **Fix shape:** four keys, `MetaBadge.LabelKey`/`TitleKey` (or `Msgf` at the handler, as R-566's page titles did — the „N napja" suffix has a count and needs the same `%d` treatment), with a render test per branch in both languages. Small. | **READY - rank P3-LOW; owner: CC (controller)** |
| **R-590** | **[P3-LOW] The data-folder card tells an English household, in Hungarian, whether its files are backed up.** FOUND 2026-09-20 live on demo-hp (0.257.0), same pass as R-589: `/apps/paperless-ngx?lang=en` renders „Ide másold a feldolgozandó fájlokat. Az alkalmazás beolvassa, majd törli innen — ez a mappa átmeneti, és nem készül róla biztonsági mentés." and `/apps/romm?lang=en` renders „Itt tárolódnak a fájljaid. Biztonsági mentés készül róla." `controller/internal/web/datapath_card.go` `consequenceFor` (L33-48) returns four raw Hungarian literals chosen by BACKUP CLASS. The R-75 `label` beside them IS translated now (it is catalog copy), so the English page reads „Documents to read in" followed by a Hungarian sentence — the mixed line is worse than either language whole. **This one carries a promise about the customer's files**, which is why it ranks with R-589 rather than below it: the sentence exists to say a folder is temporary and unbacked, and a household that cannot read it may leave originals in a drop-zone the backup filter drops at every tier. **Fix shape:** four keys, class-keyed exactly as now (the comment's whole point is that the promise is class-driven and never per-app), plus a render test per class in both languages. | **READY - rank P3-LOW; owner: CC (controller)** |
| **R-591** | **[P3-LOW] `Stack.Copy()` is a deep copy with one shallow field, and the field is new.** FOUND 2026-09-20 while adding the catalog's English overlay: `controller/internal/stacks/manager.go` `Copy()` deep-copies `Meta.DeployFields` (with nested `Options`), `Meta.OptionalConfig` (with nested `Fields`), `Meta.Integrations`, `Meta.HealthCheck` and `Meta.InitialCreds` — and does NOT copy the new `Meta.I18n` map, which the struct assignment leaves shared between the original and the "copy". **It is safe TODAY and that is exactly the shape worth filing:** `Metadata.For` reads the overlay and never writes to it (pinned by `TestForDoesNotMutateTheReceiver`), so nothing can observe the sharing yet. The hole is in the CONTRACT — a function whose whole purpose is "a snapshot the caller may mutate" now has a field that is not one, and the next person to write through an overlay will find a bug with no failing test in front of it. **Fix shape:** deep-copy `I18n` in `Copy()` and pin it with a test that mutates the copy's overlay and asserts the original is unchanged. Alternatively state in `Copy()`'s comment that `I18n` is deliberately shared and immutable, and pin THAT with a test. Either is fine; silence is not. | **READY - rank P3-LOW; owner: CC (controller)** |
| **R-592** | **[P3-LOW] Two defects in the new catalog copy gate, each found by its own decoy rather than by reading it.** FOUND AND FIXED 2026-09-20 building `app-catalog-felhom.eu/scripts/check-copy-i18n.py` (R-560 slice 5). (1) **Coverage was counted only for the apps NAMED on the command line**, so `check-copy-i18n.py privatebin` reported 47 more untranslated strings than the same tree unscoped — a ratchet anybody could loosen by naming one app, and either number could have been made to "pass". (2) **The credential check searched for its token as a bare substring**, so a `default_creds` of „admin / adminadmin" translated to "administrator / hunter2" PASSED: "admin" is inside "administrator". (3) **The ASCII-Hungarian stems matched as bare substrings too**, so „ird be" convicted "the third best" and „angol" convicted "Angola". All three now have their own case in `scripts/test_gate_decoys.py` (33 cases, every one seen to convict or pass as intended). **Recorded rather than left in a commit message** because the general form is worth the row: every one of the three was a check that matched a LABEL where the FACT was a word, a login or a whole catalog — the R-421 shapes, inside the gate written to enforce R-421. | **CLOSED 2026-09-20 — fixed in the same session, decoys added** |
| **R-537** | **[P1-HIGH] The app-backup page labels the tier-1 backup „DB + Konfig + Adatok" and prints the app's data-drive size next to it — but the tier-1 unit contains NO drive-side app data at all.** MEASURED 2026-09-16 on the drill box (fresh install, controller 0.243.0, one drive, tier 2 and tier 3 both „Nincs beállítva"): five photos (3 000 000 B) were uploaded into Nextcloud through its own WebDAV interface, then the customer-visible „Mentés most" was pressed (`POST /api/backup/run` → 200, the unit grew 25 337 B → 978 MB). The resulting unit's `manifest.json` lists `db-dumps` + three **docker volume** dumps and nothing else; listing the 781 MB `nextcloud_nextcloud_html.tar` (29 346 entries, positive control `version.php` = 3 hits) gives **`Fotok` = 0 and `nyaralas` = 0**, and `./data/` is the empty bind-mount point. A `find` over the whole `backups/` tree for `*appdata*` / `*Fotok*` returns nothing. The page nevertheless renders „1. mentés … DB + Konfig + Adatok" and „Nextcloud Adatlemez 65.1 MB" — a size measured on exactly the data it does not copy (`internal/web/handlers.go:1176-1178`, `BackupContents`). **This is a truth defect, not a design defect:** `07-backup-architecture.md` §6.2 places nextcloud's file leg at **Tier 2 and Tier 3 only**, and its „[FACT] What the whole-guest tiers do NOT carry" says `mp8 /mnt/felhom-drives` is out of vzdump scope (confirmed live: „excluding bind mount point mp8 … (not a volume)"). So on a one-drive box with no off-site tier — the state every fresh install starts in — the household's files are in **no backup**, while the page says „Adatok". Same family as R-517/R-518. **Fix shape:** render tier-1 contents from the capture set actually written (`ComputeCaptureSet`), so a unit with no file leg reads „DB + Konfig" and the drive size is not shown beside it; and say on the page that the app's files need tier 2 or tier 3. Evidence: `audits/evidence-drill-0243-2026-09-16/phase2-f10.txt`. **CLOSED 2026-09-16 — controller v0.244.0, proven live.** The contents label is computed PER TIER from what that tier captures: Tier 1 says „Adatok" only when the app's data really is in the volumes the unit captured, and a class-A app carries one sentence saying where its files ARE protected. Proven on demo-hp through the page the customer opens: Paperless-ngx reads „1. mentés … DB + Konfig" with „Az alkalmazás fájljait a távoli másolat (és a második meghajtó) védi …", while its „2. mentés" row still reads „DB + Konfig + Adatok". Red-proof: restoring the old app-shaped label fails `TestAppBackupRows_Tier1LabelDoesNotClaimFilesItCannotHold`. **RE-PROVEN 2026-09-16 on a FRESH box** (installed from the built ISO 1.28.0, controller 0.244.0, off-site on by default): the Nextcloud row read „1. mentés … DB + Konfig" with the new sentence, „2. mentés … Nincs 2. (off-drive) másolat", „3. mentés Sikeres restic → …your-storagebox.de"; „DB + Konfig + Adatok" appeared ZERO times while the local unit held no file leg. | **CLOSED 2026-09-16 — controller v0.244.0 (proven live on demo-hp)** |
| **R-538** | **[P1-HIGH] A tier-1 app restore reports plain success and leaves Nextcloud listing files whose bytes were never in the backup — and it destroys the app's own trash, the customer's last copy.** MEASURED 2026-09-16 on the drill box, F10 („a child deletes the photo folder"): the five photos were deleted through Nextcloud (DELETE 204, PROPFIND 404), then restored through the page exactly as a customer would (`POST /backup/restore` `stack_name=nextcloud` `snapshot_id=helyi` → 302, finished in **35 s**, „A(z) nextcloud: 3 adatkötet és az adatbázis visszaállítva — az alkalmazás újraindult."). Afterwards the folder is back and **lists all five photos**, and **none of them opens**: `GET nyaralas-1..5` = 404 / 503×4 with `Sabre\DAV\Exception\NotFound`, while the positive controls at the same moment pass (`status.php` 200, WebDAV PUT 201, GET 200). Cause: the replayed MariaDB dump (11:01:45Z) knows the photos, the bytes live on `mp8` and were never captured (R-537). **Worse:** the bytes were still on the drive in Nextcloud's own trash (`appdata/nextcloud/admin/files_trashbin/files/Fotok.d1789556707/nyaralas-1..5.jpg`, all five present) and the restored database no longer references them — the trash listing comes back **empty**, so „restore from trash", the one route that would have worked, is gone. The customer is left with five unopenable photos, a success message, and no warning. **Fix shape:** before replaying a database whose app has an uncaptured file leg, refuse or warn („ennek az alkalmazásnak a fájljai nincsenek ebben a mentésben — a visszaállítás után a fájlok hiányozni fognak"); and never present a DB-only restore of a class-A app as a complete one. Evidence: `audits/evidence-drill-0243-2026-09-16/phase2-f10.txt`. **CLOSED 2026-09-16 — controller v0.244.0, proven live.** A unit restore refuses before anything is touched when the unit cannot return the app's drive-side files, and names the route that can. Fired live on demo-hp: `POST /backup/restore` for paperless-ngx → 302 with „Ez a mentés nem tartalmazza az alkalmazás fájljait, ezért nem állítjuk vissza az adatbázist föléjük — a fájlok így a helyükön maradnak. A fájlok a távoli másolatból állíthatók vissza …", and the app read `running` before AND after, so nothing was stopped and no trash was made unreachable. The database-and-settings-only path exists as a separately worded second step. Red-proof: disabling the guard fails `TestUnitRestore_RefusesWhenTheUnitCannotHoldTheFiles`. **RE-PROVEN 2026-09-16 on a FRESH box, and this time the refusal had somewhere to point:** after five photos were deleted, `POST /backup/restore` was refused with „…a fájlok így a helyükön maradnak. A fájlok a távoli másolatból állíthatók vissza: … „Teljes visszaállítás (fájlok + adatbázis)"", the app read `running` before AND after, and the wastebasket was untouched. The off-site route then returned all five photos — 200 with the exact uploaded sizes and sha256 IDENTICAL to the originals, 5/5, with a negative control. Evidence: `audits/evidence-backup-promise-2026-09-16/phaseE-photos.txt`. | **CLOSED 2026-09-16 — controller v0.244.0 (proven live on demo-hp)** |
| **R-525** | **[P3-LOW] FileBrowser has its own login; putting it behind the dashboard session (traefik forwardAuth or Quantum proxy auth) is a new mechanism nobody has measured.** Filed 2026-09-15 by the P1-fixes task (B.5). R-513 closed the default-password hole with a generated password; a household still has two logins. **What it needs:** a spike on a scratch guest — forwardAuth to the controller session, and what FileBrowser Quantum does with a trusted header. | **READY — rank P3-LOW; owner: CC (spike)** |