REPORT: v0.247.0 i18n spike — live parity on demo-hp, red-proofs, observations filed
gates / gates (push) Successful in 21s
gates / gates (push) Successful in 21s
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
@@ -1,96 +1,109 @@
|
||||
# REPORT — controller v0.246.0: an interrupted restore is told; the recovery-code reminder waits
|
||||
# REPORT — controller v0.247.0: the i18n spike — English on three pages, Hungarian byte-identical
|
||||
|
||||
**2026-09-17.** R-550 (operator ruling „fix") and R-546. Architecture read first:
|
||||
`felhom.eu/documentation/architecture/07-backup-architecture.md` §3 (Lane 1) and
|
||||
`08-alarm-ladder.md` §6.2.
|
||||
**Date:** 2026-09-17 · **Task:** localisation starter (spike + design), Phase 1 · **Baseline:** `main`
|
||||
`89dd3e94b1de` (v0.246.0, MinAgent 0.131.0) · **Released:** `612c417` → image
|
||||
`gitea.dooplex.hu/admin/felhom-controller:0.247.0` · **MinAgent:** 0.131.0 (unchanged).
|
||||
**Architecture read before any claim:** `felhom.eu/documentation/architecture/02-controller-module-map.md`
|
||||
(templates, setup wizard obsolete) and `05-hub-architecture.md` (report ingestion, config push).
|
||||
**Design written after the spike ran:** `felhom.eu/documentation/architecture/10-localisation.md`.
|
||||
**Inventory:** `felhom.eu/documentation/audits/I18N-INVENTORY-2026-09-17.md`.
|
||||
|
||||
## Claims in the brief that turned out wrong — named first
|
||||
|
||||
1. **„Readiness = `escrow.pbs_storage_id` set."** Readiness is the agent's own preflight `ok` over
|
||||
**five** blocking items (`pbs_storage_id`, `dr_tier`, `age_binary`, `hub_upload`, `sudo_grant`;
|
||||
`felhom-agent/internal/localapi/escrow_ceremony.go` `handleEscrowPreflight`). `pbs_storage_id` was the
|
||||
one item R-546's box happened to show. The controller now reads the combined `ok`, never a copy.
|
||||
2. **„The failure shows a raw error"** — not in a browser. The escrow page already listed the checklist
|
||||
and showed its start form only when the preflight was `ok`. The raw `-storage` stderr came from the
|
||||
chaos-night harness calling `POST /api/escrow/start` directly. What a browser user DID meet: red
|
||||
crosses, the agent's English detail in muted brackets, no start button and no word about waiting.
|
||||
Both paths are fixed; the direct one is now refused server-side.
|
||||
3. **„The backup tiers persist their records atomically"** (read from `07`, not re-checked) — **checked
|
||||
and TRUE.** The tier run records live in `settings.json`, and `Settings.save()`
|
||||
(`internal/settings/settings.go:727-752`) writes a `.tmp` and renames it, with `.bak` recovery on a
|
||||
corrupt primary. The restore record follows the same shape through the backup package's own
|
||||
`atomicWrite` (tmp + rename; like `save()`, no fsync).
|
||||
The brief named four already. Further ones (details and methods in the inventory §0):
|
||||
|
||||
1. **„`fmtTime`, `fmtBytes`, `timeAgo` do not exist; `fmtMB` is the one size helper"** — all three
|
||||
exist (`internal/web/funcmap.go` L265/L318/L403), plus nine more copy- or locale-producing helpers
|
||||
and four size helpers. `timeAgo` builds „%d perce" — the plural problem lives there.
|
||||
2. **„`fmtMB` used 10× in templates"** — 8×.
|
||||
3. **„~13 `.Format(` layout calls in `internal/web`"** — 10 at the baseline (plus 8 `time.RFC3339`).
|
||||
4. **Template lines 1 613 / 1 838** — 1 553, by two methods; neither figure reproduced by four variants.
|
||||
5. **Go literals 1 234 in 99 files** — 1 141 in 84 (a lexer, comments skipped); comments explain most of
|
||||
the difference.
|
||||
6. **Banner 33 lines** — 34 once ASCII-only Hungarian words count (method difference).
|
||||
|
||||
## What shipped
|
||||
|
||||
**B.1 — the restore record survives a restart (R-550).** A design REVERSED by ruling, recorded in the
|
||||
file header, in `07` §3 and in `CONTEXT.md`: `restore-status.json` in `DataDir`, written at both ends of an
|
||||
op (`internal/backup/restore_record.go`). A record still marked running at startup becomes a failed,
|
||||
`Interrupted` result — „A visszaállítás megszakadt (a doboz újraindult) — indítsd el újra." — kept per
|
||||
app until that app's next restore; `restore_interrupted` (warning, household) pushed once. Wired from
|
||||
`main()` (`cmd/controller/restore_record_wiring.go`). Shown as a card on `/backups/restore` and in the
|
||||
off-site wizard's outcome card. Notification cooldowns stay in memory.
|
||||
- **Mechanism (b), expanded at template LOAD:** `internal/i18n` (`Bundle`, `Expand`, `Msg`, `Plural`,
|
||||
embedded `locales/hu.json` + `en.json`). `loadTemplates` parses one template set per language via
|
||||
`parseTemplateSet` (= `ParseFS` + `Expand`); `s.tmpl` stays Hungarian. Why not a runtime `T` func: the
|
||||
escaper changes `+ ' "` bytes and 618 of 1 867 template strings are inside `<script>`. Why not
|
||||
`x/text/message`: no gender, only count plurals — a dependency for nothing.
|
||||
- **Converted:** `launcher.html`, `backups.html`, `app_info.html`, `layout.html`, `backups_shared.html`
|
||||
(`backups_empty`) — 252 markers, 235 hu keys, all translated (238 en keys incl. plural forms).
|
||||
Page titles via `TitleKey`; English overrides for `stateLabel`, `timeAgo`, `timeAgoStr`,
|
||||
`nextRunLabel`, `statusText`; `<html lang>` follows.
|
||||
- **Setting / switch / report:** `settings.json` `language`; `POST /settings/language`; `?lang=`;
|
||||
switch in the sidebar footer only when not Hungarian or `?lang=` is present; `"language"` in the hub
|
||||
report at all four build sites in `cmd/controller/main.go`.
|
||||
- **Gates:** new `i18n_missing_gate.py` (registered in `controller_gates.py`). `emoji`,
|
||||
`native_confirm`, `secret_in_markup`, `retrieval_promise` now read templates expanded
|
||||
(`scripts/i18n_bundle.py`); `mojibake` scans `.json`. **Found while converting:** retrieval-promise
|
||||
went STALE (red) the moment copy moved; the other three went silently BLIND — the HEAD emoji gate
|
||||
passed a planted emoji in `en.json`, and the HEAD retrieval gate never saw a planted promise in
|
||||
`hu.json`.
|
||||
- **Tool:** `scripts/i18n_extract.py`. First pass pulled layout `<span>`s and a button-link pair into
|
||||
messages and mis-split a JS string that starts inside a tag — fixed and re-run from clean templates.
|
||||
It also **missed eight ASCII-only Hungarian strings** (Megszakadt, Elavult, Csak x86, Pi kompatibilis,
|
||||
Rendszermonitor, Most nem, sikertelen, ismeretlen) and one JS fragment („adatait ide:"), found by a
|
||||
by-eye review of what it left; the word list was extended and the fragment converted by hand.
|
||||
- Two existing tests (`TestTemplatesParse`, `TestTemplatesParseWithFuncmap`) parsed raw templates with
|
||||
`ParseFS`; they now parse through `parseTemplateSet` for every language.
|
||||
|
||||
**B.2 — the reminder waits (R-546).** `internal/web/escrow_readiness.go`: the agent's preflight `ok`,
|
||||
cached 60 s, probed only while paused. The bar is held back while not ready; `/backup/escrow` shows a
|
||||
waiting card that polls and reloads; `POST /api/escrow/start` refuses 409 before staging. Unknown
|
||||
readiness keeps the bar (fail loud). Transitions logged at INFO.
|
||||
## Red-proofs — each seen failing, then passing (`felhom.eu/documentation/audits/i18n-2026-09-17/redproofs/`)
|
||||
|
||||
**B.3 — the guide.** `VOLUNTEER-first-hour.md`: the recovery code is now step 7, after the first apps,
|
||||
and begins „Amikor a sárga sáv megjelenik (a beállítás után néhány perccel)".
|
||||
|
||||
**MinAgent: 0.131.0, unchanged** — the preflight has been served since agent 0.88.0.
|
||||
|
||||
## Red-proofs — each seen failing, then passing
|
||||
|
||||
| test | break | failure seen |
|
||||
| test | mutation | failure seen |
|
||||
|---|---|---|
|
||||
| `TestRestoreRecord_InterruptedRestoreSurvivesRestart` | no persistence | `StartedAt:0001-01-01 … Last:<nil>` — the chaos-night value |
|
||||
| `TestRestoreRecordAtStartup_RaisesInterruptedOnce` | helper skips loading | `raised no restore_interrupted: []` |
|
||||
| `TestMainWiresRestoreRecord` | main() not wired | `does not call both … inert seam` |
|
||||
| `TestR550_RestorePageShowsInterruptedRestore` | handler line removed | `the restore page does not show the interrupted restore` |
|
||||
| `TestR546_NotReadyBoxHoldsTheBar` | readiness not consulted | `the bar urges a ceremony the box cannot run yet` |
|
||||
| `TestR546_EscrowPageWaitsWhenNotReady` | flag never set | `does not tell the household the box is still preparing` |
|
||||
| `TestR546_StartRefusedWhenNotReady` | gate removed | `= 200 … job_id escrow-1` |
|
||||
| `TestI18nParity` | fixture file absent | `no parity fixture` (red before the fixture existed) |
|
||||
| `TestI18nParity` | „Megosztási link" → „…linK" in hu.json | `launcher_full: Hungarian page CHANGED at line 261` (+ launcher_nopassword) |
|
||||
| `TestI18nEnglishPages` | `layout.inditopult` deleted from en.json | `Hungarian template copy on the English page, line 111 … Indítópult` (5 cases) |
|
||||
| `TestLanguageSwitch_EndToEnd` | `s.addLanguageData` removed from `executeTemplate` | `?lang=en must render English and show the switch`; title and switch missing |
|
||||
| `TestI18nJSContextValuesAreSafe` | en `launcher.masolva` = `Copied'` | `contains '\'', which breaks its context` |
|
||||
| `TestBundleParametersMatchAcrossLanguages` | en banner drops `{{.RecoveryAbandonDate}}` | `template fields ".RecoveryAbandonDays", Hungarian has ".RecoveryAbandonDate,.RecoveryAbandonDays"` |
|
||||
| `TestLocaleFuncsHungarianBundleMatchesFuncMap` | hu `func.state.running` = „Futás" | `hu.json "Futás", funcmap "Fut"` |
|
||||
| gate decoys (`test_gate_decoys.py`) | undefined key, „Please", gap grew, new formal form; emoji / confirm( / secret / retrieval promise planted in a bundle | all 18 decoys rejected; HEAD emoji gate passed the bundle emoji, HEAD retrieval gate did not see the bundle promise |
|
||||
|
||||
Controls: finished restore stays finished; the next restore of that app clears the notice (another app's
|
||||
does not); a ready box shows the bar; unknown readiness shows the bar; no interruption → no card.
|
||||
Two of my own test mistakes, corrected: a `-storage` needle that matched the menu's `nav-group-storage`
|
||||
id (narrowed to `requires -storage`); the three start-order tests updated to `preflight,…` — their
|
||||
load-bearing assertion (stage BEFORE start) unchanged.
|
||||
Fixtures: captured by running the parity test with `-update-i18n-golden` in a clean `git worktree` of
|
||||
`origin/main` 89dd3e94b1de (the test file adapted to `s.tmpl`), then copied in; the current tree passed
|
||||
them before AND after conversion. All files restored after each mutation (`cmp`).
|
||||
|
||||
## Gates
|
||||
|
||||
`go build ./... && go vet ./... && go test ./...` — green, 28 packages. `controller_gates.py --fast` —
|
||||
all OK (`golden-notice` advisory; golden stays 0.245.0 under the waiver to 2026-09-27).
|
||||
- Green gate: `go build ./... && go vet ./... && go test ./...` → rc 0, 29 packages ok (first run found
|
||||
the two raw-`ParseFS` tests; fixed; re-run green).
|
||||
- `controller_gates.py --fast` → all OK incl. `i18n` (English missing 0 / ceiling 0; formal forms 6 /
|
||||
ceiling 6). Pre-push hook ran them on the push.
|
||||
|
||||
## Live validation (endpoint-level; no browser on DooPlex)
|
||||
## Live validation (endpoint-level; no browser on DooPlex) — demo-hp guest 9201
|
||||
|
||||
**B.4(a) — PASS, on demo-hp guest 9201 with a throwaway `homebox`.** Restore accepted 08:51:51Z; 2 s in
|
||||
the status and the file on disk both said `running … homebox`; `docker kill felhom-controller`; the
|
||||
agent's supervisor restarted it in 42 s (not by hand). After restart the status read
|
||||
`ok:false … "A visszaállítás megszakadt …" interrupted:true`; `/backups/restore` showed the card
|
||||
(negative control 0); the controller logged `restore_interrupted pushed OK (HTTP 200)`; the hub stores the
|
||||
event under demo-hp. A second restore of homebox completed `ok:true` and the card was gone (0).
|
||||
Evidence: `felhom.eu/documentation/audits/evidence-chaos-fixes-2026-09-17/partB4a-*.txt`.
|
||||
Deployed by hand (`/etc/felhom-controller-image`, bootstrap restart); **fleet floor NOT raised; no
|
||||
golden.** Startup: `loadTemplates: lang=hu … 252 markers expanded` and `lang=en … 252`.
|
||||
|
||||
**B.4(b) — NOT live-validated, and why (R-551).** No Tier-0 box is both paused and agent-connected:
|
||||
9201 is escrowed (bar off by design; making it paused would be hand-set state on the standing demo box,
|
||||
and a real ceremony would supersede its escrow — the hub keeps one `host_escrow` row per host); 9202 has
|
||||
no local-API token. Proven by the three tests above through `ServeHTTP` with a fake agent; chaos night
|
||||
measured live that the agent preflight is red ~17 min after a bind and turns green by itself.
|
||||
**The ceremony was deliberately NOT run on demo-hp.**
|
||||
- **Hungarian unchanged:** launcher, `/backups`, `/apps/privatebin` fetched on 0.246.0 and on 0.247.0 —
|
||||
identical after replacing version string and CSRF token; raw sizes equal (44 462 / 46 250 / 42 081).
|
||||
A fetch 7 s after restart differed only in app state tags („Nem egészséges" while health warmed up).
|
||||
- **English:** `?lang=en` and, after `POST /settings/language lang=en` (302 → `/launcher`, settings
|
||||
`"language": "en"`, INFO `language switch: household language set to en`), identical renders. No
|
||||
accented Hungarian from template copy. Remaining Hungarian, all Go view models or catalog: backup-target
|
||||
banner/offer and whole-guest tier labels (`/backups`); the „Naprakész" update badge and the catalog
|
||||
tagline / use cases / first steps (`/apps/privatebin`). Launcher: none.
|
||||
- **Hub report:** stored reports read no field (0.246.0), `hu`, `en` (12:54:43Z), `hu` (12:55:22Z).
|
||||
Negative control: demo-felhom (0.246.0) no field.
|
||||
- **Refusals:** no `_csrf` → 403; no session → 302 to login; setting unchanged.
|
||||
- Evidence (CSRF redacted): `felhom.eu/documentation/audits/i18n-2026-09-17/live/`.
|
||||
|
||||
## Teardown
|
||||
|
||||
- **Machine:** homebox removed with data and backups (0 containers, 0 volumes); 10 of 10 standing apps up.
|
||||
My first removal was refused 409 („still running — stop it first") — the product was right; stopped,
|
||||
then removed. Password and scripts shredded in the guest (0 left).
|
||||
- **Host:** nothing provisioned. 9201 stays on controller 0.246.0 (the release under validation).
|
||||
- **Hub:** the `restore_interrupted` and `controller_restarted_by_agent` events stay as history.
|
||||
Machine: guest temp scripts, password file, session file shredded/removed; box left on `hu`, 0.247.0.
|
||||
Host: the tarball removed from demo-hp. Hub: nothing written; the DB copy read for reports shredded on
|
||||
DooPlex. Nothing provisioned. The scratch worktree of `origin/main` is removed.
|
||||
|
||||
## Observations
|
||||
|
||||
1. An interrupted-restore notice for an app that is then removed stays for ever. **FILED: R-552**
|
||||
2. No Tier-0 box can exercise the paused + agent-connected escrow state. **FILED: R-551**
|
||||
3. `PushEvent` is best-effort (3 attempts, 3 s apart); `restore_interrupted` inherits that. **NOT-A-FINDING: the page notice comes from the persisted file and does not depend on the event; the event-drop path is the existing, recorded behaviour of every controller event.**
|
||||
1. The wire-contract gate passes an unread field whose name appears in a hub Go comment (`language`). **FILED: R-555**
|
||||
2. Four places decide behaviour by matching Hungarian wording (HTTP status, quota class, alert placement, a persisted warning marker). **FILED: R-553**
|
||||
3. The extractor and the inventory detect ASCII-only Hungarian only through a word list; eight strings slipped past on three pages. **FILED: R-556**
|
||||
4. `TestI18nEnglishPages` subtracts fixture data strings, so a template word equal to a data string (the launcher title „Indítópult") is masked on the launcher cases. **FILED: R-556**
|
||||
5. Six formal „ön" forms in the converted Hungarian copy, now counted by a ratchet. **FILED: R-516**
|
||||
6. Dates and sizes are not locale-formatted; the two template date layouts disagree; sizes use a decimal point. **FILED: R-562**
|
||||
7. `storage_handlers_test.go` imported `text/template` for its template-parse check. **NOT-A-FINDING: a test-only parse check with no escaping assertion; it now parses through the production `parseTemplateSet`, which uses `html/template`.**
|
||||
|
||||
Reference in New Issue
Block a user