diff --git a/STATUS.md b/STATUS.md index 9d6cf621..87d6d5e3 100644 --- a/STATUS.md +++ b/STATUS.md @@ -1,43 +1,45 @@ # STATUS — what works, what's broken, what's next -**Updated 2026-09-18 (late) — the globe you photographed is fixed.** +**Updated 2026-09-18 (evening) — your customers' e-mails now follow their language.** > **Ready for a volunteer: yes.** -**What you saw, and why.** The globe on the sign-in page came out as a bare triangle and two plain -words, floating outside the card. Two separate faults: +**What changed.** Until today every e-mail we sent a household was Hungarian, whatever their box was +set to. The box has been telling us which language it is on for a while; nothing was listening. Now +the e-mails follow it — the nightly backup notes, the "your box has gone quiet" warning, the setup +code, the link that connects a new box, and the page that link opens. -1. **Your browser was still using the old stylesheet.** Those pages asked for the style file without a - version number on the end, so a browser that had already downloaded it kept the old copy — and the - old copy knows nothing about a globe. The main dashboard has always asked with a version number; - these pages never did. **Five pages had it, not three.** -2. **Even styled, it sat outside the card**, pinned to the corner of the window, so it read as part of - the browser rather than part of the page. It is now inside the card, centred under the footer, and - the little menu opens upward. +**You pick a language when you create a customer** (Hungarian unless you say otherwise). That is the +language their box starts in and the language of the first e-mails, which are sent before the box has +ever spoken. **The moment the household picks a language on their own dashboard, theirs wins** — your +choice is a starting point, never an override. -**A third thing I found while fixing it.** My first fix would have printed the exact software version -onto the page a guest opens from a share link — someone you sent a link to, who should not be told -which build you are running. **An existing test caught it.** Those two pages now get a scrambled tag -instead: it does the same job and says nothing. +**Nothing you read has changed.** Your own copy of every alert is still Hungarian, word for word. +That was the part I measured hardest: 56 sample e-mails were captured before a single word moved, and +all 56 Hungarian ones still match exactly. -**Checked on the demo HP box:** the page now asks for the versioned file, the file it gets back -carries the new rule, and the old rule is gone. **What I cannot check is how it looks** — there is no -browser on this machine. That part is your click. +**Proven with a real e-mail.** I pressed the demo box's own "send test notification" button twice, a +minute apart. In English it arrived as *"Dear Customer, this is a test notification…"*; switched back +to Hungarian it arrived as *"Kedves Ügyfél! Ez egy teszt értesítés…"*. Your copy was identical both +times. -**Rows.** One opened and closed the same day. +**One honest gap.** Some alerts contain a sentence the box writes itself — naming a drive, or an app. +Most of those now come in both languages. **Six do not yet**, so an English household can still see +one Hungarian line in a few e-mails. One of those six is a whole e-mail rather than one line. Nothing +breaks; it just reads wrong. It is written down. -**The floor is raised — nothing needs you.** You asked for it, so I did it: every box now must run -0.255.0 or newer. **It delivered on its own:** the N100 was never touched by hand and picked up the -new version in under five minutes. The HP box was already on it. +**Rows.** Five opened, two closed the same day. -Three machines are still on older versions. All three are switched off or blocked, and a box that is -not running cannot be told anything. Each takes the new version the moment it comes back. That is the -floor working, not failing. +**Needs you — two things, neither urgent.** -**One new row, mine, nothing for you to do.** While pressing the button I used a command option that -printed your hub password back into my own session log. It went nowhere else — no file, nothing -saved, nothing sent. I wrote down how to avoid it next time. **If you want to be thorough, change -that password**; nothing suggests you need to. +1. **Raise the floor to 0.256.1?** That is what sends the new version to every box. **If you do:** + households that chose English get English alerts. **If you do nothing:** the hub still writes in + their language, but the sentences the box composes stay Hungarian for them. Nothing is at risk. + I would raise it. +2. **Change the shared demo password?** Not because it leaked anywhere public — it did not. Earlier + today I left five of my own helper scripts on the demo box with the password written inside them, + for a few hours, and found them myself while tidying up. They are destroyed. Changing it is cheap + and would close the question; nothing suggests you must. --- diff --git a/documentation/architecture/10-localisation.md b/documentation/architecture/10-localisation.md index 00049b72..9e4c84f4 100644 --- a/documentation/architecture/10-localisation.md +++ b/documentation/architecture/10-localisation.md @@ -481,6 +481,45 @@ reported 60 520 changed lines and measured nothing. A line-index compare is not These are rulings, not proposals. Anything specced against a different assumption is wrong. +### 10.4 Slice 3 — the hub's e-mails follow the household (hub v0.118.x, controller v0.256.x). SLICE 3 CLOSED. + +SHIPPED 2026-09-18. Full design: `05-hub-architecture.md` §15. + +**The hub half (v0.118.0, v0.118.1).** `hub/internal/i18n`, 79 keys, hu authoritative, hu fallback, +missing-key ceiling 0. Every customer mail and the public bind page render from it. +`customerMessages`/`severityLabels` are DERIVED from the bundle, so a sentence is written once and +"a new event type enters `allowedEventTypes` and `customerMessages` together" now means a line in +`hu.json` plus its English twin. **56 mail goldens per language, captured from v0.117.0 before any +string moved; all 56 Hungarian ones pass unchanged.** Language order: **last reported → +created-with → `hu`**; `reports.language` defaults to EMPTY (never told us ≠ chose Hungarian) and +the newest report is found by the autoincrement `id`, because `received_at` has second granularity. +The bind page's `expired` state always renders Hungarian — it is the state an unknown token lands +in, and the language must not answer what the text refuses to. + +**The box half (v0.256.0, v0.256.1).** `message_customer` on `POST /api/v1/event`: the same sentence +in the household's language, `omitempty`, beside the unchanged Hungarian `message`. **A Hungarian +household sends no second copy at all**, so the fleet's payload is byte-for-byte what it is today and +the hub's fallback path stays the one production exercises. 19 producers render both from ONE bundle +key; the Go parity gate checks all 19 against the base-commit literals. `customer.language` +bootstraps a new box (stored choice → config → `hu`) and is never persisted into `settings.json`. +v0.256.1 added the one observable the feature was missing: the push log says `[hu-only]` or +`[+household(en)]`. + +**Proven live, twice.** Part A: a real English e-mail read in the operator's inbox and the Hungarian +one 74 seconds later, same button, same box. Part B: the same event pushed 57 seconds apart showing +`[hu-only]` then `[+household(en)]`, with an identical Hungarian sentence both times. + +**What is still Hungarian for an English household:** six producers whose sentence arrives already +finished from another package (R-585) — of which `offbox_enlarge_blocked` matters most, because it +has no hub entry so its raw sentence IS the mail. Plus the R-570 sentence. The 15 operator-tier types +are Hungarian by design. + +**Gaps filed:** R-581 (same-second report ties, and `GetCustomers()` still has the shape), R-582 (an +English copy-guard ported word-for-word convicted 141 honest sentences), R-583 (the test mail was the +one mail that did not follow the language — and the one an operator would use to check), R-584 +(credential-bearing probes left in a live guest's `/tmp`), R-585. **R-555 closed.** + + ### 2026-09-17 (on the starter) 1. **Scope: what the household sees — the controller, its e-mails, the guide, the app catalog.** Not diff --git a/documentation/audits/i18n-slice3-2026-09-18/B/README.md b/documentation/audits/i18n-slice3-2026-09-18/B/README.md new file mode 100644 index 00000000..c3076418 --- /dev/null +++ b/documentation/audits/i18n-slice3-2026-09-18/B/README.md @@ -0,0 +1,65 @@ +# Slice 3 Part B — controller v0.256.0 / v0.256.1, live evidence (2026-09-18) + +**Method: endpoint-level and log-level** on demo-hp guest 9201. Nothing installed, nothing removed, +no deploy request of any kind. The credential-bearing probe **shredded itself** on exit, host and +guest, which is R-584's own fix shape applied the same day it was filed. + +## The box sends the second sentence only for a non-Hungarian household + +The same event (`controller_started`, which fires on every restart and needs no state change), on the +same box, 57 seconds apart: + +``` +15:07:28 Event pushed: controller_started (info) [hu-only] — Controller elindult (0.256.1) +15:08:25 Event pushed: controller_started (info) [+household(en)] — Controller elindult (0.256.1) +``` + +**The Hungarian sentence is IDENTICAL in both.** That is the invariant the whole slice rests on: +`message` never moves, because it is what the operator is mailed and what the hub stores. + +## The hub accepted it, and stored the Hungarian + +``` +[INFO] Event from demo-hp: controller_started (info) — Controller elindult (0.256.1) (both times) + +events table, demo-hp: + 2026-09-18 15:08:25 controller_started info 'Controller elindult (0.256.1)' <- the en push + 2026-09-18 15:07:28 controller_started info 'Controller elindult (0.256.1)' <- the hu push + 2026-09-18 14:55:38 controller_started info 'Controller elindult (0.256.0)' +``` + +No 400: the new field is additive and the hub takes it. The stored message is the Hungarian one in +every case, **including the push that carried an English household copy**. + +## Why the proof is a LOG LINE and not an e-mail + +There is no honest way to make a converted producer send a customer e-mail on this box: +`severityNotifies` drops `info`, so the safe producers (`controller_started`, `app_deployed`, +`db_dump_completed`) mail nobody — and every converted producer at `warning` or above describes +something bad that is not true (`storage_disconnected`, `crossdrive_failed`, `app_deploy_failed`, +`disaster_recovery_started`). Triggering one would mean either a false record on a real box's +timeline or a state change the fences forbid. + +**So the log line was built for this**, in v0.256.1, after finding there was nothing to look at: +the box logged only the Hungarian, the hub logs only the Hungarian, and the payload is inside TLS. +"Did the box attach a second sentence?" was unanswerable from either end — which is exactly the +question a mis-languaged mail raises. + +**What that leaves unproven live:** an English household's *rendered event mail*. The hub half of +that rendering WAS proven live in Part A (a real English mail, read in the inbox); the box half is +proven here at the wire; the join between them is covered by tests on both sides. Saying that plainly +is better than manufacturing a false alarm to photograph. + +## What is NOT converted, stated rather than implied + +- **The 15 operator-tier types** — the operator reads Hungarian and these never reach a customer. +- **Producers whose sentence is composed in another package** and arrives finished: `backup_failed`, + `db_dump_failed`, `backup_integrity_ok`/`_failed`, `offbox_enlarge_blocked`, + `local_api_endpoint_drift`. They send Hungarian only, so an English household can still see one + Hungarian line in some mails. **Filed as R-585.** +- The R-570 sentence, until that row closes. + +## State at the end + +Controller **0.256.1** on demo-hp, healthy. Box back on **Hungarian**. Hub **0.118.1**. No floor +change, no golden, nothing installed. Guest `/tmp` carries no probe and no credential. diff --git a/documentation/backlog/OPEN-ITEMS.md b/documentation/backlog/OPEN-ITEMS.md index 6b7f5ba0..61e648f5 100644 --- a/documentation/backlog/OPEN-ITEMS.md +++ b/documentation/backlog/OPEN-ITEMS.md @@ -734,7 +734,7 @@ class (an image `VOLUME` at an unmounted path) is still live — `immich-server` | **R-554** | **[P3-LOW] Delete the first-boot setup wizard — obsolete by design, still reachable.** OPERATOR DECISION 2026-09-17 (localisation starter, decision 4: „out of scope, obsolete"). `02-controller-module-map.md` L56 calls `internal/setup/` obsolete; `cmd/controller/main.go` L322 still enters it when `setup.NeedsSetup(cfg)` — `customer.id` empty after bootstrap ingestion, or a `.needs-setup` marker (`internal/setup/setup.go` L17-25). Ingestion leaves `customer.id` empty on a missing/invalid `bootstrap.json`, a failed hub pull, or a failed merge/write/reload (`internal/bootstrap/bootstrap.go` L109-163) — so a box whose first boot cannot reach the hub shows a household an 8-page wizard (95 Hungarian strings, its own template set and CSRF). **Fix shape:** decide what such a box shows instead (a single „cannot reach Felhom yet, retrying" page — needs no decision beyond copy), then delete `internal/setup/` and `runSetupMode`; red-proof that a failed ingestion renders the waiting page, not a 404. **Check first** whether any drill/golden path still relies on `.needs-setup`. | **READY - rank P3-LOW; owner: CC** | | **R-555** | **[P3-LOW] The wire-contract gate counts a field as received when its name appears in a Go COMMENT on the receiving side.** FOUND 2026-09-17 by CC adding the report's `language` field (controller v0.247.0): `scripts/wire_contract_gate.py` passed WITHOUT an allowlist entry, because `receiver_tokens()` tokenises whole files and the word „language" occurs hub-side only in a comment (`hub/internal/web/configs.go:558`, „this page's existing language"). The shape is the gate's own named failure class (name-for-fact, R-421): any English tag name that also appears in hub prose passes unread. The field was allowlisted by hand with this row named. **Fix shape:** strip `//` and `/* */` comments (and template `{{/* */}}`) before tokenising; add the decoy „a tag whose name appears only in a receiver comment must convict"; expect a handful of currently-passing tags to surface — each is a finding, not noise. | **READY - rank P3-LOW; owner: CC** | | **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=`) 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. | **READY - rank P3-LOW; owner: CC** | +| **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. | **READY - rank P3-LOW; owner: CC** | | **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-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** | @@ -760,6 +760,7 @@ class (an image `VOLUME` at an unmounted path) is still live — `immich-server` | **R-582** | **[P3-LOW] An English copy guard written from the Hungarian one matches ordinary words instead of the claim.** FOUND 2026-09-18 extending `hub_copy_gate.py` for R-558. The Hungarian retrieval stems ARE the claim — `visszaállíthat` is one word meaning *can restore* — so the guard matches a stem. Ported to English as the bare stems `restor`/`recover`, the gate convicted **141 honest sentences** on its first run: "Disaster recovery has started", "Your server can be reached again", the name of the Restore page, and every JSON KEY containing the word. English splits the modal from the verb, so the English form of the claim is a PHRASE. FIXED in the same session: the patterns carry the modal, and the gate scans bundle VALUES only, never keys. **Then the decoy suite caught the fix being too narrow** — a planted "can STILL be restored" walked through a pattern written for "can be restored", which is R-299 again in another language; every pattern now admits an adverb. **The general form worth keeping: a guard ported between languages must be re-derived from what the claim IS in the new language, not translated word for word — and a guard that convicts 141 true sentences is worse than no guard, because it earns an allowlist entry per sentence and then nobody reads it.** | **CLOSED 2026-09-18 - hub v0.118.0 (recorded for the lesson; three decoys incl. an innocent control)** | | **R-583** | **[P3-LOW] The test-notification mail was the one customer mail that did not follow the language — and it is the mail an operator would use to CHECK that the language works.** FOUND 2026-09-18 during hub v0.118.0's own live proof: I went to press "send test notification" for the English demo box, read `sendTestEmail` first, and found it composes its own hardcoded Hungarian subject and body instead of going through `FormatCustomerEmail`. Every other customer mail had been localised. **The general form worth keeping: the surface you would use to CHECK a feature is the one most worth checking first — a broken instrument that reports success is worse than a broken feature.** FIXED and shipped as hub v0.118.1 in the same session: the two sentences extracted byte-for-byte into `mail.test.subject` / `mail.test.body`, red-proofed against the hardcoded version, and proven live in both languages 74 seconds apart on demo-hp. | **CLOSED 2026-09-18 - hub v0.118.1** | | **R-584** | **[P2-MED] Credential-bearing probe scripts were left in a live guest's `/tmp` for hours, across three releases.** FOUND 2026-09-18 while cleaning up after slice 3's live proof: `probeB.sh`, `probeB2.sh`, `probeB3.sh`, `probeC.sh` and `probeD.sh` were still in demo-hp guest 9201's `/tmp` from slice 2's releases B, C and D earlier the same session, and all five carried the controller password INLINE (`-d "password=$PW"` with the value substituted). The standing rule in `.claude/rules/ui-hungarian.md` says exactly this: *delete any credential-bearing helper from `/tmp` (host AND guest) when done*. They were not deleted at the end of each phase; they were found by grepping my own litter for secret-shaped strings before removing it, which is a check that happened only because a later phase went looking. All five are shredded. **Why it is a row and not a note: the rule existed, was loaded, and was still not followed three times running — so the rule is not the mechanism.** The password is the shared demo one (Tier-0 boxes, `~/.config/credentials`), unchanged; rotating it is cheap and is the operator's call. **Fix shape:** never inline a credential in a helper — write it to a mode-0600 file and read it with curl's `@file` form, which this run did and which is why this run's own scripts were clean; and make the last act of a phase that pushed a script to a box the `shred -u` of it, the same way R-320 made evidence-copying the last act of a phase. | **READY - rank P2-MED; owner: CC (the mechanism), operator (whether to rotate)** | +| **R-585** | **[P3-LOW] Six event producers still send Hungarian only, so an English household can see one Hungarian line in some mails.** FOUND 2026-09-18 by localisation slice 3 Part B (R-558, controller v0.256.0), which converted 19 of the customer-facing producers and left these: `backup_failed`, `db_dump_failed`, `backup_integrity_ok`, `backup_integrity_failed`, `offbox_enlarge_blocked` and `local_api_endpoint_drift`. **Why they were left:** each receives its sentence already FINISHED from another package, so the key and its arguments no longer exist by the time the notifier sees it — converting them means changing their callers, not the notifier. The 15 operator-tier types are deliberately excluded and are NOT part of this row: the operator reads Hungarian. **Why it matters more than it looks: `offbox_enlarge_blocked` has no `customerMessages` entry on the hub**, so its raw sentence IS the household's mail rather than an extra line under a translated headline — for that one type an English household gets a wholly Hungarian mail, not a mostly-English one. **Fix shape:** push the key and its arguments down from each caller (the shape slice 2 release B already used for errors, `util.MsgError`), then add each to the `convertedProducers` table in `internal/notify/message_customer_test.go`, which is the list both language tests walk. | **READY - rank P3-LOW; owner: CC** | | **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)** |