From f806baf4c7304dbf43e65c5fa26cd9fd5ae06b05 Mon Sep 17 00:00:00 2001 From: kisfenyo Date: Thu, 17 Sep 2026 21:02:47 +0200 Subject: [PATCH] =?UTF-8?q?R-563:=20the=20remote-backup=20page=20polls=20f?= =?UTF-8?q?rom=20a=20status=20attribute,=20and=20=E2=80=9EFut=E2=80=A6"=20?= =?UTF-8?q?is=20finally=20translatable?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - backups_remote.html: the status stat carries data-status="{{.Offbox.LastStatus}}"; the poll reads v.dataset.status === 'running' instead of v.textContent.indexOf('Fut'). The English page could never see a running backup before this (slice 1 shipped the English page with that word left Hungarian). - The running label becomes {{T "backups_remote.fut"}} — hu „Fut…" unchanged, en „Running…". - 12 backups_remote parity fixtures re-captured. Proven (audit switch/fixture diff): each differs from its predecessor ONLY by the data-status attribute and that one poll line; the other 94 are byte-identical. The displayed Hungarian is unchanged. - Tests: TestR563_PollStartsFromAttribute (hu and en), TestR563_AttributeFollowsTheStatus. Red-proofed twice: poll back on textContent → both languages fail; word back inline → the English page shows Hungarian. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS --- controller/internal/i18n/locales/en.json | 1 + controller/internal/i18n/locales/hu.json | 1 + .../internal/web/r563_poll_attribute_test.go | 80 +++++++++++++++++++ .../web/templates/backups_remote.html | 7 +- .../i18n_parity/backups_remote_empty.html | 5 +- .../i18n_parity/backups_remote_error.html | 7 +- .../i18n_parity/backups_remote_escrowed.html | 7 +- .../i18n_parity/backups_remote_full.html | 7 +- .../backups_remote_incomplete.html | 7 +- .../i18n_parity/backups_remote_notconf.html | 5 +- .../backups_remote_notconf_hub.html | 5 +- .../backups_remote_pending_agent.html | 7 +- .../backups_remote_pending_old.html | 7 +- .../i18n_parity/backups_remote_running.html | 7 +- .../i18n_parity/backups_remote_stale.html | 7 +- .../i18n_parity/backups_remote_stale_old.html | 7 +- 16 files changed, 144 insertions(+), 23 deletions(-) create mode 100644 controller/internal/web/r563_poll_attribute_test.go diff --git a/controller/internal/i18n/locales/en.json b/controller/internal/i18n/locales/en.json index b07de82..f8625f0 100644 --- a/controller/internal/i18n/locales/en.json +++ b/controller/internal/i18n/locales/en.json @@ -777,6 +777,7 @@ "backups_remote.az_alkalmazas_mentesek_titkositott_masol": "An encrypted copy of the app backups on a remote store — your own NAS or Felhom offsite storage — over restic + SFTP. The store sees only encrypted data. This is the „1 off-site\" leg of the 3-2-1 rule — independent of the local second copy and the full system backup.", "backups_remote.rendben": "✓ OK", "backups_remote.hianyos": "! Incomplete", + "backups_remote.fut": "Running…", "backups_remote.hiba": "✗ Error", "backups_remote.utolso_tavoli_mentes": "Last remote backup", "backups_remote.tarolo_meret": "Store size ·", diff --git a/controller/internal/i18n/locales/hu.json b/controller/internal/i18n/locales/hu.json index f1673d1..23fd486 100644 --- a/controller/internal/i18n/locales/hu.json +++ b/controller/internal/i18n/locales/hu.json @@ -774,6 +774,7 @@ "backups_remote.az_alkalmazas_mentesek_titkositott_masol": "Az alkalmazás-mentések titkosított másolata egy távoli tárolóra — saját NAS vagy Felhom offsite tárhely — restic + SFTP kapcsolaton. A tároló csak titkosított adatot lát. Ez a 3-2-1 szabály „1 off-site\" lába — független a helyi másodpéldánytól és a teljes rendszermentéstől.", "backups_remote.rendben": "✓ Rendben", "backups_remote.hianyos": "! Hiányos", + "backups_remote.fut": "Fut…", "backups_remote.hiba": "✗ Hiba", "backups_remote.utolso_tavoli_mentes": "Utolsó távoli mentés", "backups_remote.tarolo_meret": "Tároló méret ·", diff --git a/controller/internal/web/r563_poll_attribute_test.go b/controller/internal/web/r563_poll_attribute_test.go new file mode 100644 index 0000000..cd9a4cf --- /dev/null +++ b/controller/internal/web/r563_poll_attribute_test.go @@ -0,0 +1,80 @@ +package web + +import ( + "regexp" + "strings" + "testing" + + "gitea.dooplex.hu/admin/felhom-controller/internal/settings" +) + +// R-563 — the Távoli mentés page starts its progress poll from a MACHINE-READABLE status, not from +// the word it happens to display. +// +// Until v0.251.0 the page did `v.textContent.indexOf('Fut') >= 0`. Two consequences, both measured: +// the English page (slice 1 shipped it) could never see a running backup, because the English word is +// „Running…"; and the Hungarian word could not be translated at all while that line existed — it was +// the ONE word slice 1 had to leave behind. +// +// RED-PROOF (REPORT): put the textContent test back in backups_remote.html and the English half fails. +func TestR563_PollStartsFromAttribute(t *testing.T) { + statusEl := regexp.MustCompile(`
]*)>([^<]*)
`) + for _, lang := range []string{"hu", "en"} { + data := appRowSplitData() + data["Offbox"] = &settings.OffboxTarget{ + Enabled: true, Host: "nas.local", LastStatus: "running", EscrowState: "escrowed", + } + html := renderBackupPageLang(t, lang, "backups_remote", data) + m := statusEl.FindStringSubmatch(html) + if m == nil { + t.Fatalf("[%s] the status element is gone — this test no longer reads what it thinks it reads", lang) + } + if !strings.Contains(m[1], `data-status="running"`) { + t.Errorf("[%s] the status element carries no machine-readable status: %q", lang, m[1]) + } + if !strings.Contains(html, "v.dataset.status === 'running'") { + t.Errorf("[%s] the poll does not read the attribute", lang) + } + if strings.Contains(html, "textContent.indexOf('Fut')") { + t.Errorf("[%s] the poll reads the displayed Hungarian word again (R-563)", lang) + } + // The displayed word itself: Hungarian as always, English on the English page. Before this + // change the English page showed the Hungarian word here, because it could not be translated. + word := strings.TrimSpace(m[2]) + want := map[string]string{"hu": "Fut…", "en": "Running…"}[lang] + if word != want { + t.Errorf("[%s] status label = %q, want %q", lang, word, want) + } + } +} + +// A state that is NOT running must not claim to be: the attribute follows LastStatus exactly, so the +// poll cannot be started by a word that merely contains another. +func TestR563_AttributeFollowsTheStatus(t *testing.T) { + statusEl := regexp.MustCompile(`id="offbox-status-value" data-status="([a-z]*)"`) + for _, st := range []string{"ok", "error", "incomplete", "running"} { + data := appRowSplitData() + data["Offbox"] = &settings.OffboxTarget{Enabled: true, Host: "nas.local", LastStatus: st, EscrowState: "escrowed"} + html := renderBackupPage(t, "backups_remote", data) + m := statusEl.FindStringSubmatch(html) + if m == nil { + t.Fatalf("[%s] no data-status attribute on the status element", st) + } + if m[1] != st { + t.Errorf("[%s] data-status = %q — the poll would read the wrong state", st, m[1]) + } + } +} + +// renderBackupPageLang renders a backup page from a chosen language's template set (R-563 needs the +// English page: it is the one that could never see a running backup). +func renderBackupPageLang(t *testing.T, lang, page string, data map[string]interface{}) string { + t.Helper() + s := testServer(t) + s.loadTemplates() + var buf strings.Builder + if err := s.templatesFor(lang).ExecuteTemplate(&buf, page, data); err != nil { + t.Fatalf("render %s [%s]: %v", page, lang, err) + } + return buf.String() +} diff --git a/controller/internal/web/templates/backups_remote.html b/controller/internal/web/templates/backups_remote.html index 07d2ad3..8770c3b 100644 --- a/controller/internal/web/templates/backups_remote.html +++ b/controller/internal/web/templates/backups_remote.html @@ -43,7 +43,7 @@ {{if .Offbox}}
-
{{if eq .Offbox.LastStatus "ok"}}{{T "backups_remote.rendben"}}{{else if eq .Offbox.LastStatus "incomplete"}}{{T "backups_remote.hianyos"}}{{else if eq .Offbox.LastStatus "error"}}{{T "backups_remote.hiba"}}{{else if eq .Offbox.LastStatus "running"}}Fut…{{else}}–{{end}}
+
{{if eq .Offbox.LastStatus "ok"}}{{T "backups_remote.rendben"}}{{else if eq .Offbox.LastStatus "incomplete"}}{{T "backups_remote.hianyos"}}{{else if eq .Offbox.LastStatus "error"}}{{T "backups_remote.hiba"}}{{else if eq .Offbox.LastStatus "running"}}{{T "backups_remote.fut"}}{{else}}–{{end}}
{{T "backups_remote.utolso_tavoli_mentes"}}{{if .Offbox.LastRun}}
{{timeAgoStr .Offbox.LastRun}}{{end}}
@@ -277,7 +277,10 @@ var bar = document.getElementById('offbox-progress-bar'); var GRACE = 5; // ~15s of quiet before concluding nothing is running var quiet = 0; - var sawRunning = (v && v.textContent.indexOf('Fut') >= 0); + // R-563: the page asks the ELEMENT what the status is (data-status, rendered server-side from + // Offbox.LastStatus), never the word it displays. Reading „Fut" meant an English page never noticed + // a running backup — and the word could not be translated while this line existed. + var sawRunning = (v && v.dataset.status === 'running'); var timer = setInterval(function(){ fetch('/backup/offbox/status',{headers:{'Accept':'application/json'}}) diff --git a/controller/internal/web/testdata/i18n_parity/backups_remote_empty.html b/controller/internal/web/testdata/i18n_parity/backups_remote_empty.html index 0e82f0c..2b1998f 100644 --- a/controller/internal/web/testdata/i18n_parity/backups_remote_empty.html +++ b/controller/internal/web/testdata/i18n_parity/backups_remote_empty.html @@ -194,7 +194,10 @@ var bar = document.getElementById('offbox-progress-bar'); var GRACE = 5; var quiet = 0; - var sawRunning = (v && v.textContent.indexOf('Fut') >= 0); + + + + var sawRunning = (v && v.dataset.status === 'running'); var timer = setInterval(function(){ fetch('/backup/offbox/status',{headers:{'Accept':'application/json'}}) diff --git a/controller/internal/web/testdata/i18n_parity/backups_remote_error.html b/controller/internal/web/testdata/i18n_parity/backups_remote_error.html index 98ba0fe..33ff238 100644 --- a/controller/internal/web/testdata/i18n_parity/backups_remote_error.html +++ b/controller/internal/web/testdata/i18n_parity/backups_remote_error.html @@ -185,7 +185,7 @@
-
✗ Hiba
+
✗ Hiba
Utolsó távoli mentés
# napja
@@ -269,7 +269,10 @@ var bar = document.getElementById('offbox-progress-bar'); var GRACE = 5; var quiet = 0; - var sawRunning = (v && v.textContent.indexOf('Fut') >= 0); + + + + var sawRunning = (v && v.dataset.status === 'running'); var timer = setInterval(function(){ fetch('/backup/offbox/status',{headers:{'Accept':'application/json'}}) diff --git a/controller/internal/web/testdata/i18n_parity/backups_remote_escrowed.html b/controller/internal/web/testdata/i18n_parity/backups_remote_escrowed.html index cd72d88..c60d1a2 100644 --- a/controller/internal/web/testdata/i18n_parity/backups_remote_escrowed.html +++ b/controller/internal/web/testdata/i18n_parity/backups_remote_escrowed.html @@ -185,7 +185,7 @@
-
✓ Rendben
+
✓ Rendben
Utolsó távoli mentés
# napja
@@ -272,7 +272,10 @@ var bar = document.getElementById('offbox-progress-bar'); var GRACE = 5; var quiet = 0; - var sawRunning = (v && v.textContent.indexOf('Fut') >= 0); + + + + var sawRunning = (v && v.dataset.status === 'running'); var timer = setInterval(function(){ fetch('/backup/offbox/status',{headers:{'Accept':'application/json'}}) diff --git a/controller/internal/web/testdata/i18n_parity/backups_remote_full.html b/controller/internal/web/testdata/i18n_parity/backups_remote_full.html index 441e4fe..05c9cc1 100644 --- a/controller/internal/web/testdata/i18n_parity/backups_remote_full.html +++ b/controller/internal/web/testdata/i18n_parity/backups_remote_full.html @@ -197,7 +197,7 @@
-
✓ Rendben
+
✓ Rendben
Utolsó távoli mentés
# napja
@@ -370,7 +370,10 @@ var bar = document.getElementById('offbox-progress-bar'); var GRACE = 5; var quiet = 0; - var sawRunning = (v && v.textContent.indexOf('Fut') >= 0); + + + + var sawRunning = (v && v.dataset.status === 'running'); var timer = setInterval(function(){ fetch('/backup/offbox/status',{headers:{'Accept':'application/json'}}) diff --git a/controller/internal/web/testdata/i18n_parity/backups_remote_incomplete.html b/controller/internal/web/testdata/i18n_parity/backups_remote_incomplete.html index 143dcb8..75b9c30 100644 --- a/controller/internal/web/testdata/i18n_parity/backups_remote_incomplete.html +++ b/controller/internal/web/testdata/i18n_parity/backups_remote_incomplete.html @@ -185,7 +185,7 @@
-
! Hiányos
+
! Hiányos
Utolsó távoli mentés
# napja
@@ -284,7 +284,10 @@ var bar = document.getElementById('offbox-progress-bar'); var GRACE = 5; var quiet = 0; - var sawRunning = (v && v.textContent.indexOf('Fut') >= 0); + + + + var sawRunning = (v && v.dataset.status === 'running'); var timer = setInterval(function(){ fetch('/backup/offbox/status',{headers:{'Accept':'application/json'}}) diff --git a/controller/internal/web/testdata/i18n_parity/backups_remote_notconf.html b/controller/internal/web/testdata/i18n_parity/backups_remote_notconf.html index f970790..b96dbc8 100644 --- a/controller/internal/web/testdata/i18n_parity/backups_remote_notconf.html +++ b/controller/internal/web/testdata/i18n_parity/backups_remote_notconf.html @@ -220,7 +220,10 @@ var bar = document.getElementById('offbox-progress-bar'); var GRACE = 5; var quiet = 0; - var sawRunning = (v && v.textContent.indexOf('Fut') >= 0); + + + + var sawRunning = (v && v.dataset.status === 'running'); var timer = setInterval(function(){ fetch('/backup/offbox/status',{headers:{'Accept':'application/json'}}) diff --git a/controller/internal/web/testdata/i18n_parity/backups_remote_notconf_hub.html b/controller/internal/web/testdata/i18n_parity/backups_remote_notconf_hub.html index 9dcb484..e331653 100644 --- a/controller/internal/web/testdata/i18n_parity/backups_remote_notconf_hub.html +++ b/controller/internal/web/testdata/i18n_parity/backups_remote_notconf_hub.html @@ -221,7 +221,10 @@ var bar = document.getElementById('offbox-progress-bar'); var GRACE = 5; var quiet = 0; - var sawRunning = (v && v.textContent.indexOf('Fut') >= 0); + + + + var sawRunning = (v && v.dataset.status === 'running'); var timer = setInterval(function(){ fetch('/backup/offbox/status',{headers:{'Accept':'application/json'}}) diff --git a/controller/internal/web/testdata/i18n_parity/backups_remote_pending_agent.html b/controller/internal/web/testdata/i18n_parity/backups_remote_pending_agent.html index 1ca2af3..d9344d9 100644 --- a/controller/internal/web/testdata/i18n_parity/backups_remote_pending_agent.html +++ b/controller/internal/web/testdata/i18n_parity/backups_remote_pending_agent.html @@ -185,7 +185,7 @@
-
–
+
–
Utolsó távoli mentés
# napja
@@ -275,7 +275,10 @@ var bar = document.getElementById('offbox-progress-bar'); var GRACE = 5; var quiet = 0; - var sawRunning = (v && v.textContent.indexOf('Fut') >= 0); + + + + var sawRunning = (v && v.dataset.status === 'running'); var timer = setInterval(function(){ fetch('/backup/offbox/status',{headers:{'Accept':'application/json'}}) diff --git a/controller/internal/web/testdata/i18n_parity/backups_remote_pending_old.html b/controller/internal/web/testdata/i18n_parity/backups_remote_pending_old.html index 1c18819..25d4d62 100644 --- a/controller/internal/web/testdata/i18n_parity/backups_remote_pending_old.html +++ b/controller/internal/web/testdata/i18n_parity/backups_remote_pending_old.html @@ -185,7 +185,7 @@
-
✓ Rendben
+
✓ Rendben
Utolsó távoli mentés
# napja
@@ -275,7 +275,10 @@ var bar = document.getElementById('offbox-progress-bar'); var GRACE = 5; var quiet = 0; - var sawRunning = (v && v.textContent.indexOf('Fut') >= 0); + + + + var sawRunning = (v && v.dataset.status === 'running'); var timer = setInterval(function(){ fetch('/backup/offbox/status',{headers:{'Accept':'application/json'}}) diff --git a/controller/internal/web/testdata/i18n_parity/backups_remote_running.html b/controller/internal/web/testdata/i18n_parity/backups_remote_running.html index 57228b7..a0de748 100644 --- a/controller/internal/web/testdata/i18n_parity/backups_remote_running.html +++ b/controller/internal/web/testdata/i18n_parity/backups_remote_running.html @@ -195,7 +195,7 @@
-
Fut…
+
Fut…
Utolsó távoli mentés
# napja
@@ -289,7 +289,10 @@ var bar = document.getElementById('offbox-progress-bar'); var GRACE = 5; var quiet = 0; - var sawRunning = (v && v.textContent.indexOf('Fut') >= 0); + + + + var sawRunning = (v && v.dataset.status === 'running'); var timer = setInterval(function(){ fetch('/backup/offbox/status',{headers:{'Accept':'application/json'}}) diff --git a/controller/internal/web/testdata/i18n_parity/backups_remote_stale.html b/controller/internal/web/testdata/i18n_parity/backups_remote_stale.html index 834df79..6319a4f 100644 --- a/controller/internal/web/testdata/i18n_parity/backups_remote_stale.html +++ b/controller/internal/web/testdata/i18n_parity/backups_remote_stale.html @@ -185,7 +185,7 @@
-
✓ Rendben
+
✓ Rendben
Utolsó távoli mentés
# napja
@@ -273,7 +273,10 @@ var bar = document.getElementById('offbox-progress-bar'); var GRACE = 5; var quiet = 0; - var sawRunning = (v && v.textContent.indexOf('Fut') >= 0); + + + + var sawRunning = (v && v.dataset.status === 'running'); var timer = setInterval(function(){ fetch('/backup/offbox/status',{headers:{'Accept':'application/json'}}) diff --git a/controller/internal/web/testdata/i18n_parity/backups_remote_stale_old.html b/controller/internal/web/testdata/i18n_parity/backups_remote_stale_old.html index a70b8cd..f6ee496 100644 --- a/controller/internal/web/testdata/i18n_parity/backups_remote_stale_old.html +++ b/controller/internal/web/testdata/i18n_parity/backups_remote_stale_old.html @@ -185,7 +185,7 @@
-
✓ Rendben
+
✓ Rendben
Utolsó távoli mentés
# napja
@@ -273,7 +273,10 @@ var bar = document.getElementById('offbox-progress-bar'); var GRACE = 5; var quiet = 0; - var sawRunning = (v && v.textContent.indexOf('Fut') >= 0); + + + + var sawRunning = (v && v.dataset.status === 'running'); var timer = setInterval(function(){ fetch('/backup/offbox/status',{headers:{'Accept':'application/json'}})