From 01c967e49e386dd5fdf408bfead60182198b1c82 Mon Sep 17 00:00:00 2001 From: kisfenyo Date: Tue, 20 Oct 2026 22:36:19 +0200 Subject: [PATCH] monitoring page: storage labels follow the reader's language (W26, rehearsal 2026-10-20); unreleased --- CHANGELOG.md | 14 ++++++ REPORT.md | 35 ++++--------- controller/internal/i18n/locales/en.json | 14 +++++- controller/internal/i18n/locales/hu.json | 14 +++++- .../web/agent_host_metrics_handler.go | 33 ++++++------ .../web/agent_host_metrics_handler_test.go | 50 +++++++++++++++++-- .../web/agent_host_metrics_lang_test.go | 49 ++++++++++++++++++ controller/scripts/i18n_go_keys.json | 14 +++++- 8 files changed, 176 insertions(+), 47 deletions(-) create mode 100644 controller/internal/web/agent_host_metrics_lang_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index cbb8857..18b6b17 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,17 @@ +## Unreleased (2026-10-20) — the monitoring page's storage labels follow the reader's language (W26); text only + +Found by the Tester-2 rehearsal of 2026-10-20 (felhom.eu `audits/rehearsal-2026-10-20/B6-failures/s16-README.md`, word +W26): on the English dashboard the monitoring page showed every storage label and purpose in Hungarian. No release in +this run (the system under test is frozen); it ships with the next controller version. **MinAgent: unchanged.** + +- `internal/web/agent_host_metrics_handler.go`: `enrichHostStorageTargets` takes a message lookup; `ServeHostMetricsAPI` + passes the request's language. Twelve keys `host_storage.*` in `hu.json` / `en.json`; the Hungarian text is + byte-identical to the old Go literals (`scripts/i18n_go_keys.json`, go-parity gate green). +- Tests: `TestEnrichHostStorageTargets_FollowsTheLanguage` (every type, both languages, never a key, never the Hungarian + text in English) and `TestServeHostMetricsAPI_LabelsFollowTheRequestLanguage` (through a fake agent behind the real + pinned client, `?lang=en|hu`). Each was seen RED first: a Hungarian literal put back for `pbs`, and the handler's + language forced to `hu`. + ## v0.336.0 — a failed off-site restore puts the app back exactly as it was and starts it (R-893 option A, operator D20); the held sentence says only what is certain (R-1169) (2026-10-20) **MinAgent: 0.158.1** — the floor's, carried; no agent change. No golden (`09` §3 decision 250). Task: the night brief diff --git a/REPORT.md b/REPORT.md index 7754074..03183e0 100644 --- a/REPORT.md +++ b/REPORT.md @@ -1,29 +1,14 @@ -# REPORT — controller v0.336.0: a failed off-site restore puts the app back exactly (R-893 option A) — 2026-10-20 +# REPORT — the monitoring page's storage labels follow the reader's language (W26) — 2026-10-20, unreleased -**Task:** night brief part 2 (run by day, 2026-10-20), Parts 2 and 7. Operator answer: „D20 A". Architecture: -`felhom.eu/documentation/architecture/07-backup-architecture.md` §6.3; `09` §3 decision 192. +**Task:** the Tester-2 rehearsal of 2026-10-20 (operator brief, „small text fixes allowed, committed, not released"). +Found in B6 #16 (the dashboard in English): the monitoring page showed the storage labels in Hungarian. +Architecture: `felhom.eu/documentation/architecture/10-localisation.md` (Go-side messages are keys). -**What changed:** `internal/backup/offbox_restore_undo.go` + `offbox_reconstitute.go`. Before the stop, the live volumes the -snapshot will replace are measured and the copy is added to the restore's own free-space need (too little room or an -unmeasurable volume refuses with nothing stopped). Right after the stop they are tarred into -`/backups/restore-undo///`. On any failure after a volume or the definition moved, the copies are poured -back, a volume the snapshot added is removed, the live definition is written back and the app starts; the household reads -„…sikertelen. Nem változott semmi…"; the operator gets `backup_run_failures` leg `restore-put-back`. A put-back that fails holds -the app and keeps the copy. On success the copy is removed. R-1169: the held sentence says only what is certain. +**What changed:** `internal/web/agent_host_metrics_handler.go` — the labels and purposes come from twelve `host_storage.*` +keys in the request's language; the Hungarian text is unchanged byte for byte. -**Tests:** 7 new (`r893_putback_test.go`) + 4 moved to option A (`r893_mixed_restore_test.go`); nine mutations, each red -(felhom.eu `audits/night2-2026-10-20/P2/redproof-unit.txt`). `controller_gates.py --fast` green; `go test ./...` green. +**Tests:** two new (lookup, and handler wiring through a fake agent); each seen red, then green. `go test ./internal/web/ +./internal/i18n/` green; `controller_gates.py --fast` green. -**Live proof on 9202** (rc.1, a helper with the brief's fences; `audits/night2-2026-10-20/P2/live/`): Rallly + postgres 18, a -forced failure after the volume was replaced → the copy (69 MB, 0.9 s) poured back: 36 tables, markers A–D, the table only the -backup had ABSENT, schema/data/row-count md5 EQUAL to before, the app running, no hold, the copy gone. Then a normal restore of -the same snapshot: the snapshot's content, the app running, the copy gone. 9202 back on 0.331.0 as found. - -**Release:** image `sha256:93cd67d871125877e935a024765075b5a0b7c0e7683e1165976ff7a654fe8d73` (clean clone `93fb251`); per-box -floors → the three test boxes on 0.336.0 within a minute → box walk green (0 unhealthy, 0 allow-list refusals, catalog in -sync, https-only 28 OK, „Fájlok ↗" one sign-in on demo-hp and Tester 1) → global floor 0.336.0 / MinAgent 0.158.1 at 08:54:35 -UTC; per-box floors cleared. Evidence `audits/night2-2026-10-20/P7/`. - -**Not measured live:** the version-change branch, the `_files` sentence, the refusals before the stop, a failing put-back — -each pinned by a red-proved unit test. **Found:** a successful put-back is logged at ERROR by the web handler, and an empty -`backups/restore-undo/` folder stays — one register row (small; a version is pushed once, so it waits for the next release). +**Not done:** no release, no image, no floor change — the system under test stays frozen during the rehearsal. It ships +with the next controller version. The rehearsal's own report: felhom.eu `REPORT-rehearsal-2026-10-20.md`. diff --git a/controller/internal/i18n/locales/en.json b/controller/internal/i18n/locales/en.json index cbb95d2..038bfba 100644 --- a/controller/internal/i18n/locales/en.json +++ b/controller/internal/i18n/locales/en.json @@ -2792,5 +2792,17 @@ "app_info.plex_claim_btn": "Connect Plex", "err.plex_claim.bad_code": "This is not a Plex code. A code starts with: claim-", "err.plex_claim.refused": "Plex did not accept the code. It may have run out: get a new one at plex.tv/claim.", - "err.plex_claim.not_claimed": "Plex does not report the connection yet. Try again in a minute." + "err.plex_claim.not_claimed": "Plex does not report the connection yet. Try again in a minute.", + "host_storage.usb.label": "External data drive (USB)", + "host_storage.local_dir.label": "External data drive", + "host_storage.userdata.purpose": "Your apps’ data (files, media library) is on this drive.", + "host_storage.system.label": "Internal SSD – system and apps", + "host_storage.system.purpose": "Space for the operating system and the installed apps.", + "host_storage.local.label": "Internal disk – templates and local backups", + "host_storage.local.purpose": "System templates and local backups.", + "host_storage.pbs.label": "Off-site backup", + "host_storage.pbs.purpose": "Encrypted backups kept away from your home.", + "host_storage.nfs.label": "Network backup (NFS)", + "host_storage.cifs.label": "Network backup (SMB)", + "host_storage.network.purpose": "Backups kept on a network storage." } diff --git a/controller/internal/i18n/locales/hu.json b/controller/internal/i18n/locales/hu.json index 493320d..5a3784b 100644 --- a/controller/internal/i18n/locales/hu.json +++ b/controller/internal/i18n/locales/hu.json @@ -2780,5 +2780,17 @@ "app_info.plex_claim_btn": "Plex összekapcsolása", "err.plex_claim.bad_code": "Ez nem Plex-kód. A kód így kezdődik: claim-", "err.plex_claim.refused": "A Plex nem fogadta el a kódot. Lehet, hogy lejárt: kérj újat a plex.tv/claim oldalon.", - "err.plex_claim.not_claimed": "A Plex még nem jelzi, hogy össze van kapcsolva. Próbáld újra egy perc múlva." + "err.plex_claim.not_claimed": "A Plex még nem jelzi, hogy össze van kapcsolva. Próbáld újra egy perc múlva.", + "host_storage.usb.label": "Külső adattároló (USB)", + "host_storage.local_dir.label": "Külső adattároló", + "host_storage.userdata.purpose": "Az alkalmazások adatai (fájlok, médiatár) ezen a meghajtón vannak.", + "host_storage.system.label": "Belső SSD – rendszer és alkalmazások", + "host_storage.system.purpose": "Az operációs rendszer és a telepített alkalmazások tárhelye.", + "host_storage.local.label": "Belső lemez – sablonok és helyi mentések", + "host_storage.local.purpose": "Rendszersablonok és helyi biztonsági mentések.", + "host_storage.pbs.label": "Távoli biztonsági mentés", + "host_storage.pbs.purpose": "Titkosított, telephelyen kívüli biztonsági mentések.", + "host_storage.nfs.label": "Hálózati mentés (NFS)", + "host_storage.cifs.label": "Hálózati mentés (SMB)", + "host_storage.network.purpose": "Hálózati tárolón őrzött biztonsági mentések." } diff --git a/controller/internal/web/agent_host_metrics_handler.go b/controller/internal/web/agent_host_metrics_handler.go index 3787ef1..d818f60 100644 --- a/controller/internal/web/agent_host_metrics_handler.go +++ b/controller/internal/web/agent_host_metrics_handler.go @@ -38,9 +38,11 @@ func (s *Server) ServeHostMetricsAPI(w http.ResponseWriter, r *http.Request) { return } // The agent enumerates storages via `pvesm` in a non-deterministic order, so #host-storage-bars - // reordered on every poll (item 2). Stabilise the order Go-side and attach friendly Hungarian - // labels + a one-line purpose per entry — display-only; we NEVER rename the PVE storage ids. - enrichHostStorageTargets(resp.StorageTargets) + // reordered on every poll (item 2). Stabilise the order Go-side and attach friendly labels + a + // one-line purpose per entry in the reader's language (W26, rehearsal 2026-10-20: they were + // Hungarian on the English dashboard) — display-only; we NEVER rename the PVE storage ids. + lang := s.langFor(r) + enrichHostStorageTargets(resp.StorageTargets, func(key string) string { return s.msgLang(lang, key) }) writeDiskJSON(w, http.StatusOK, true, "", resp) } @@ -48,7 +50,8 @@ func (s *Server) ServeHostMetricsAPI(w http.ResponseWriter, r *http.Request) { // order (user-data → system+apps → backup → other; alphabetical by id within a tier) and fills in // a friendly label + purpose per entry. Mirrors the disk-overview's sortDisksForView contract: // a Go-side ordering beats relying on the agent's enumeration order or template JS. -func enrichHostStorageTargets(targets []agentapi.StorageTarget) { +// msg turns a message key into the reader's text. +func enrichHostStorageTargets(targets []agentapi.StorageTarget, msg func(key string) string) { sort.SliceStable(targets, func(i, j int) bool { if ri, rj := storageTypeRank(targets[i].Type), storageTypeRank(targets[j].Type); ri != rj { return ri < rj @@ -56,7 +59,7 @@ func enrichHostStorageTargets(targets []agentapi.StorageTarget) { return targets[i].Name < targets[j].Name }) for i := range targets { - label, purpose := storageLabelAndPurpose(targets[i]) + label, purpose := storageLabelAndPurpose(targets[i], msg) targets[i].Label = label targets[i].Purpose = purpose } @@ -79,24 +82,24 @@ func storageTypeRank(typ string) int { } } -// storageLabelAndPurpose maps a storage target to a friendly Hungarian label + one-line purpose. -// Falls back to the raw id for unrecognised types. The raw id stays in Name (rendered muted). -func storageLabelAndPurpose(t agentapi.StorageTarget) (string, string) { +// storageLabelAndPurpose maps a storage target to a friendly label + one-line purpose (message keys +// host_storage.*). Falls back to the raw id for unrecognised types. The raw id stays in Name (rendered muted). +func storageLabelAndPurpose(t agentapi.StorageTarget, msg func(key string) string) (string, string) { switch t.Type { case "usb": - return "Külső adattároló (USB)", "Az alkalmazások adatai (fájlok, médiatár) ezen a meghajtón vannak." + return msg("host_storage.usb.label"), msg("host_storage.userdata.purpose") case "local-dir": - return "Külső adattároló", "Az alkalmazások adatai (fájlok, médiatár) ezen a meghajtón vannak." + return msg("host_storage.local_dir.label"), msg("host_storage.userdata.purpose") case "lvmthin", "lvm": - return "Belső SSD – rendszer és alkalmazások", "Az operációs rendszer és a telepített alkalmazások tárhelye." + return msg("host_storage.system.label"), msg("host_storage.system.purpose") case "local": - return "Belső lemez – sablonok és helyi mentések", "Rendszersablonok és helyi biztonsági mentések." + return msg("host_storage.local.label"), msg("host_storage.local.purpose") case "pbs": - return "Távoli biztonsági mentés", "Titkosított, telephelyen kívüli biztonsági mentések." + return msg("host_storage.pbs.label"), msg("host_storage.pbs.purpose") case "nfs": - return "Hálózati mentés (NFS)", "Hálózati tárolón őrzött biztonsági mentések." + return msg("host_storage.nfs.label"), msg("host_storage.network.purpose") case "cifs": - return "Hálózati mentés (SMB)", "Hálózati tárolón őrzött biztonsági mentések." + return msg("host_storage.cifs.label"), msg("host_storage.network.purpose") default: return t.Name, "" } diff --git a/controller/internal/web/agent_host_metrics_handler_test.go b/controller/internal/web/agent_host_metrics_handler_test.go index 5238f5b..b736828 100644 --- a/controller/internal/web/agent_host_metrics_handler_test.go +++ b/controller/internal/web/agent_host_metrics_handler_test.go @@ -1,11 +1,23 @@ package web import ( + "strings" "testing" "gitea.dooplex.hu/admin/felhom-controller/internal/agentapi" + "gitea.dooplex.hu/admin/felhom-controller/internal/i18n" ) +// msgIn returns the real message bundle's text for lang — the same lookup the handler's msgLang does. +func msgIn(t *testing.T, lang string) func(string) string { + t.Helper() + b, err := i18n.Shared() + if err != nil { + t.Fatalf("bundle: %v", err) + } + return func(key string) string { return b.Msg(lang, key) } +} + // enrichHostStorageTargets must produce a STABLE order regardless of how the agent enumerated the // storages (the #host-storage-bars reorder bug, item 2), and attach a friendly label + purpose per // entry without ever mutating the raw PVE id (Name). @@ -17,7 +29,7 @@ func TestEnrichHostStorageTargets_OrderAndLabels(t *testing.T) { {Name: "felhom-usb", Type: "usb"}, {Name: "local-lvm", Type: "lvmthin"}, } - enrichHostStorageTargets(targets) + enrichHostStorageTargets(targets, msgIn(t, "hu")) wantOrder := []string{"felhom-usb", "local-lvm", "local", "felhom-pbs"} for i, want := range wantOrder { @@ -50,8 +62,8 @@ func TestEnrichHostStorageTargets_Stable(t *testing.T) { } } a, b := mk(), mk() - enrichHostStorageTargets(a) - enrichHostStorageTargets(b) + enrichHostStorageTargets(a, msgIn(t, "hu")) + enrichHostStorageTargets(b, msgIn(t, "hu")) for i := range a { if a[i].Name != b[i].Name { t.Fatalf("non-deterministic at %d: %q vs %q", i, a[i].Name, b[i].Name) @@ -66,7 +78,7 @@ func TestEnrichHostStorageTargets_Stable(t *testing.T) { // An unrecognised type falls back to the raw id and an empty purpose. func TestEnrichHostStorageTargets_UnknownType(t *testing.T) { targets := []agentapi.StorageTarget{{Name: "weird-store", Type: "zfspool"}} - enrichHostStorageTargets(targets) + enrichHostStorageTargets(targets, msgIn(t, "hu")) if targets[0].Label != "weird-store" || targets[0].Purpose != "" { t.Errorf("unknown type: label=%q purpose=%q, want raw id + empty", targets[0].Label, targets[0].Purpose) } @@ -79,3 +91,33 @@ func names(ts []agentapi.StorageTarget) []string { } return out } + +// W26 (rehearsal 2026-10-20 B6 #16): on the English dashboard the monitoring page showed the storage +// labels and purposes in Hungarian. Every known storage type must come out in the reader's language: +// English text for "en", and never a key name (a missing key) in either language. +func TestEnrichHostStorageTargets_FollowsTheLanguage(t *testing.T) { + mk := func() []agentapi.StorageTarget { + return []agentapi.StorageTarget{ + {Name: "u", Type: "usb"}, {Name: "d", Type: "local-dir"}, {Name: "local-lvm", Type: "lvmthin"}, + {Name: "local", Type: "local"}, {Name: "felhom-pbs", Type: "pbs"}, {Name: "n", Type: "nfs"}, {Name: "c", Type: "cifs"}, + } + } + en, hu := mk(), mk() + enrichHostStorageTargets(en, msgIn(t, "en")) + enrichHostStorageTargets(hu, msgIn(t, "hu")) + for i := range en { + for _, s := range []string{en[i].Label, en[i].Purpose, hu[i].Label, hu[i].Purpose} { + if s == "" || strings.HasPrefix(s, "host_storage.") { + t.Errorf("%s (%s): missing text %q", en[i].Name, en[i].Type, s) + } + } + if en[i].Label == hu[i].Label || en[i].Purpose == hu[i].Purpose { + t.Errorf("%s (%s): the English text is the Hungarian one: %q / %q", en[i].Name, en[i].Type, en[i].Label, en[i].Purpose) + } + } + for _, tg := range en { + if tg.Type == "lvmthin" && tg.Label != "Internal SSD – system and apps" { + t.Errorf("lvmthin English label = %q", tg.Label) + } + } +} diff --git a/controller/internal/web/agent_host_metrics_lang_test.go b/controller/internal/web/agent_host_metrics_lang_test.go new file mode 100644 index 0000000..f64cafc --- /dev/null +++ b/controller/internal/web/agent_host_metrics_lang_test.go @@ -0,0 +1,49 @@ +package web + +import ( + "crypto/sha256" + "encoding/hex" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "gitea.dooplex.hu/admin/felhom-controller/internal/config" +) + +// W26 (rehearsal 2026-10-20 B6 #16), pinned at the handler: GET /api/host-metrics answers the storage +// labels in the REQUEST's language. TestEnrichHostStorageTargets_FollowsTheLanguage pins the lookup; +// this pins the wiring (the reader's language reaching it), through a fake agent behind the real pinned client. +func TestServeHostMetricsAPI_LabelsFollowTheRequestLanguage(t *testing.T) { + mux := http.NewServeMux() + mux.HandleFunc("GET /host/metrics", func(w http.ResponseWriter, r *http.Request) { + _, _ = w.Write([]byte(`{"ok":true,"data":{"vmid":1,"host":{"node":"n"},"storage_targets":[ + {"name":"local-lvm","type":"lvmthin","state":"attached","reachable":true}, + {"name":"felhom-pbs","type":"pbs","state":"attached","reachable":true}]}}`)) + }) + agent := httptest.NewTLSServer(mux) + defer agent.Close() + sum := sha256.Sum256(agent.Certificate().Raw) + s := testServer(t) + s.cfg = &config.Config{LocalAPI: config.LocalAPIConfig{ + Endpoint: strings.TrimPrefix(agent.URL, "https://"), Token: "tok", Fingerprint: hex.EncodeToString(sum[:])}} + + get := func(lang string) string { + rec := httptest.NewRecorder() + s.ServeHostMetricsAPI(rec, httptest.NewRequest(http.MethodGet, "/api/host-metrics?lang="+lang, nil)) + if rec.Code != http.StatusOK { + t.Fatalf("lang=%s: status %d body %s", lang, rec.Code, rec.Body.String()) + } + return rec.Body.String() + } + en, hu := get("en"), get("hu") + if !strings.Contains(en, "Internal SSD") || !strings.Contains(en, "Off-site backup") { + t.Errorf("English answer lacks the English labels: %s", en) + } + if strings.Contains(en, "Belső") || strings.Contains(en, "Távoli") { + t.Errorf("English answer carries Hungarian labels: %s", en) + } + if !strings.Contains(hu, "Belső SSD") || !strings.Contains(hu, "Távoli biztonsági mentés") { + t.Errorf("Hungarian answer lacks the Hungarian labels: %s", hu) + } +} diff --git a/controller/scripts/i18n_go_keys.json b/controller/scripts/i18n_go_keys.json index 1b8bf48..2096a4f 100644 --- a/controller/scripts/i18n_go_keys.json +++ b/controller/scripts/i18n_go_keys.json @@ -1321,5 +1321,17 @@ "restore.refuse.files.head": "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. ", "restore.refuse.files.offsite": "A fájlok a távoli másolatból állíthatók vissza: Biztonsági mentés → Visszaállítás, „Teljes visszaállítás (fájlok + adatbázis)”.", "restore.refuse.files.second_drive": "A fájlok a második meghajtó másolatából állíthatók vissza: „Fájlok visszaállítása”.", - "restore.refuse.files.none": "Ezekről a fájlokról jelenleg nincs másolat — kapcsold be a távoli mentést, vagy csatlakoztass egy második meghajtót." + "restore.refuse.files.none": "Ezekről a fájlokról jelenleg nincs másolat — kapcsold be a távoli mentést, vagy csatlakoztass egy második meghajtót.", + "host_storage.usb.label": "Külső adattároló (USB)", + "host_storage.local_dir.label": "Külső adattároló", + "host_storage.userdata.purpose": "Az alkalmazások adatai (fájlok, médiatár) ezen a meghajtón vannak.", + "host_storage.system.label": "Belső SSD – rendszer és alkalmazások", + "host_storage.system.purpose": "Az operációs rendszer és a telepített alkalmazások tárhelye.", + "host_storage.local.label": "Belső lemez – sablonok és helyi mentések", + "host_storage.local.purpose": "Rendszersablonok és helyi biztonsági mentések.", + "host_storage.pbs.label": "Távoli biztonsági mentés", + "host_storage.pbs.purpose": "Titkosított, telephelyen kívüli biztonsági mentések.", + "host_storage.nfs.label": "Hálózati mentés (NFS)", + "host_storage.cifs.label": "Hálózati mentés (SMB)", + "host_storage.network.purpose": "Hálózati tárolón őrzött biztonsági mentések." }