v0.308.0: controller batch 2026-10-12 — clear-address warning (R-922), secret sweep over every page (R-255), read_failed (R-266), no NVMe heat band (R-333), share globe (R-577), Hungarian dates and numbers (R-562), lost-drive option pinned (R-521)

Operator rulings 2026-10-12, 09 §3 decisions 196, 204-207, 223, 227. MinAgent 0.131.0 unchanged.
R-255's sweep found one leak, fixed here: an optional-config secret_input was rendered into its
settings page; it is now revealed on demand. Hungarian parity fixtures re-captured once for R-562.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
2026-10-12 15:13:59 +02:00
parent dcd89364a4
commit fd28d70bff
183 changed files with 2671 additions and 411 deletions
+86
View File
@@ -0,0 +1,86 @@
package i18n
import (
"strconv"
"strings"
"time"
)
// ── Dates and numbers per language (R-562, v0.308.0, `09` §3 decision 223) ────────────────────────
//
// The ONE place the dashboard decides how a date, a time or a decimal number looks:
//
// Hungarian 2026. 10. 10. 15:04 1,5 GB (the Hungarian date order with its dots; decimal COMMA)
// English 2026-10-10 15:04 1.5 GB (ISO-like; decimal point)
//
// Every function takes the time ALREADY in the zone it is shown in (the dashboard shows Budapest
// local time; the callers convert). An unknown language formats as Hungarian, like every other
// fallback in this package. Pinned by TestFormat_* in format_test.go.
// FormatDateTime renders a date with hour and minute.
func FormatDateTime(lang string, t time.Time) string {
if Normalize(lang) == "en" {
return t.Format("2006-01-02 15:04")
}
return t.Format("2006. 01. 02. 15:04")
}
// FormatDate renders a date alone.
func FormatDate(lang string, t time.Time) string {
if Normalize(lang) == "en" {
return t.Format("2006-01-02")
}
return t.Format("2006. 01. 02.")
}
// FormatMonthDayTime renders a date in the current year: month, day, hour and minute.
func FormatMonthDayTime(lang string, t time.Time) string {
if Normalize(lang) == "en" {
return t.Format("01-02 15:04")
}
return t.Format("01. 02. 15:04")
}
// FormatDecimal renders v with prec digits after the decimal separator of lang.
func FormatDecimal(lang string, v float64, prec int) string {
s := strconv.FormatFloat(v, 'f', prec, 64)
if Normalize(lang) == "en" {
return s
}
return strings.Replace(s, ".", ",", 1)
}
// LocalizeDecimals rewrites the decimal POINT of every number in an already formatted short string
// ("1.5 GB", "Immich (95.0 GB)") into lang's separator. Only a point with a digit on BOTH sides is
// touched, so a sentence's full stop, a version ("0.307.0" — see below) or an address is not a target
// by accident: a dot preceded by a digit and followed by a digit is rewritten only when the number it
// belongs to has exactly ONE dot. English returns s unchanged.
//
// For strings another package formatted with %.1f (appbackup.HumanizeBytes and its siblings) and
// handed to a page as data. A new formatter should call FormatDecimal instead.
func LocalizeDecimals(lang, s string) string {
if Normalize(lang) == "en" || !strings.Contains(s, ".") {
return s
}
b := []byte(s)
isDigit := func(c byte) bool { return c >= '0' && c <= '9' }
for i := 1; i < len(b)-1; i++ {
if b[i] != '.' || !isDigit(b[i-1]) || !isDigit(b[i+1]) {
continue
}
// The whole digit-and-dot run around this dot: more than one dot is a version or an address.
start, end := i, i
for start > 0 && (isDigit(b[start-1]) || b[start-1] == '.') {
start--
}
for end < len(b)-1 && (isDigit(b[end+1]) || b[end+1] == '.') {
end++
}
if strings.Count(string(b[start:end+1]), ".") != 1 {
i = end
continue
}
b[i] = ','
}
return string(b)
}
+44
View File
@@ -0,0 +1,44 @@
package i18n
import (
"testing"
"time"
)
// R-562 (`09` §3 decision 223): Hungarian dates and numbers in Hungarian, ISO-like in English.
func TestFormat_DatesPerLanguage(t *testing.T) {
at := time.Date(2026, 10, 10, 15, 4, 0, 0, time.UTC)
cases := []struct{ name, got, want string }{
{"hu datetime", FormatDateTime("hu", at), "2026. 10. 10. 15:04"},
{"en datetime", FormatDateTime("en", at), "2026-10-10 15:04"},
{"hu date", FormatDate("hu", at), "2026. 10. 10."},
{"en date", FormatDate("en", at), "2026-10-10"},
{"hu month-day", FormatMonthDayTime("hu", at), "10. 10. 15:04"},
{"en month-day", FormatMonthDayTime("en", at), "10-10 15:04"},
{"unknown language falls back to Hungarian", FormatDateTime("xx", at), "2026. 10. 10. 15:04"},
}
for _, c := range cases {
if c.got != c.want {
t.Errorf("%s: got %q, want %q", c.name, c.got, c.want)
}
}
}
func TestFormat_NumbersPerLanguage(t *testing.T) {
cases := []struct{ name, got, want string }{
{"hu decimal comma", FormatDecimal("hu", 1.53, 1), "1,5"},
{"en decimal point", FormatDecimal("en", 1.53, 1), "1.5"},
{"hu integer has no separator", FormatDecimal("hu", 12, 0), "12"},
{"hu size string", LocalizeDecimals("hu", "1.5 GB"), "1,5 GB"},
{"hu list of sizes", LocalizeDecimals("hu", "Immich (95.0 GB), Nextcloud (34.0 GB)"), "Immich (95,0 GB), Nextcloud (34,0 GB)"},
{"en untouched", LocalizeDecimals("en", "1.5 GB"), "1.5 GB"},
{"hu sentence full stop untouched", LocalizeDecimals("hu", "Kész. 3 elem."), "Kész. 3 elem."},
{"hu version untouched", LocalizeDecimals("hu", "v0.307.0 és 1.5 GB"), "v0.307.0 és 1,5 GB"},
{"hu address untouched", LocalizeDecimals("hu", "192.168.0.180"), "192.168.0.180"},
}
for _, c := range cases {
if c.got != c.want {
t.Errorf("%s: got %q, want %q", c.name, c.got, c.want)
}
}
}
+6 -3
View File
@@ -1508,6 +1508,7 @@
"flash.share.disabled": "Sharing is off.",
"flash.share.enable_failed": "Sharing could not be turned on.",
"flash.share.enabled": "Sharing is on.",
"flash.share.wrong_password": "Wrong password",
"flash.share.not_on": "Sharing is not on.",
"flash.share.pw_clear_failed": "The password could not be removed.",
"flash.share.pw_cleared": "The share password is removed.",
@@ -1885,7 +1886,7 @@
"recovery.adatok": "Retrieve",
"recovery.adatok_visszaszerzese_felhom": "Retrieve data — Felhom",
"recovery.alkalmazas": "App",
"recovery.amelyet_zartunk_le": ", which we\n sealed on <strong>{{.}}</strong>",
"recovery.amelyet_zartunk_le": ", which we\n sealed on <strong>{{fmtTimeStr .}}</strong>",
"recovery.biztosan_nem_kered_vissza_a": "Are you sure you do not want the earlier backups back?",
"recovery.elerhetok": "are available",
"recovery.ez_a_lehetoseg_akkor_valik": "this option becomes available once the machine has reconnected to the off-site\n storage. Until then your backups stay untouched.",
@@ -1943,6 +1944,8 @@
"settings_notifications.beallitasok_ertesitesek": "Settings — Notifications",
"settings_notifications.bekapcsolas_utan_az_egyes_alkalmazasokna": "After turning it on, allow sending e-mail for each app too (on the app’s page).",
"settings_notifications.biztonsagi_mentes_sikertelen": "Backup failed",
"settings_notifications.clear_confirm": "Yes, remove the address",
"settings_notifications.clear_warning": "If you remove the notification e-mail address, you will get no more notification e-mails. Nothing is saved until you confirm.",
"settings_notifications.e_mail_cim": "E-mail address",
"settings_notifications.elvart_adatbazismentes_elmaradt": "Expected database backup missed",
"settings_notifications.elvart_fajlmentes_elmaradt": "Expected file backup missed",
@@ -2010,7 +2013,7 @@
"settings_security.uj_jelszo": "New password",
"settings_security.uj_jelszo_megerositese": "Confirm new password",
"settings_security.utolso": "Last:",
"settings_security.utolso_szinkronizalas": "Last sync: {{.GeoLastSync}}",
"settings_security.utolso_szinkronizalas": "Last sync: {{fmtTimeStr .GeoLastSync}}",
"settings_security.veszhelyzeti_informaciok": "Emergency information",
"settings_security.visszaallitasi_jelszo": "Retrieval password",
"settings_system.a_kiszolgalo_ugynok_jelenleg_nem": "The host agent is not reachable right now — try again later.",
@@ -2251,7 +2254,7 @@
"storage.hiba": "Error:",
"storage.host_tarhely_rendszer_sablonok_iso": "Host storage — system templates, ISOs, host-level backups. It holds no app data.",
"storage.hozzaadas": "Add",
"storage.idopont": "Time: {{.DecommissionedAt}}",
"storage.idopont": "Time: {{fmtTimeStr .DecommissionedAt}}",
"storage.inaktiv": "Inactive",
"storage.ismeretlen": "unknown",
"storage.kesz": "Done",
+6 -3
View File
@@ -1499,6 +1499,7 @@
"flash.share.disabled": "A megosztás kikapcsolva.",
"flash.share.enable_failed": "A megosztás bekapcsolása nem sikerült.",
"flash.share.enabled": "A megosztás bekapcsolva.",
"flash.share.wrong_password": "Hibás jelszó",
"flash.share.not_on": "A megosztás nincs bekapcsolva.",
"flash.share.pw_clear_failed": "A jelszó törlése nem sikerült.",
"flash.share.pw_cleared": "A megosztási jelszó törölve.",
@@ -1873,7 +1874,7 @@
"recovery.adatok": "Adatok",
"recovery.adatok_visszaszerzese_felhom": "Adatok visszaszerzése — Felhom",
"recovery.alkalmazas": "Alkalmazás",
"recovery.amelyet_zartunk_le": ", amelyet\n <strong>{{.}}</strong> zártunk le",
"recovery.amelyet_zartunk_le": ", amelyet\n <strong>{{fmtTimeStr .}}</strong> zártunk le",
"recovery.biztosan_nem_kered_vissza_a": "Biztosan nem kéred vissza a korábbi mentéseket?",
"recovery.elerhetok": "elérhetők",
"recovery.ez_a_lehetoseg_akkor_valik": "ez a lehetőség akkor válik elérhetővé, ha a gép már újra kapcsolódott a házon kívüli\n tárhelyhez. Addig a mentéseid érintetlenül megmaradnak.",
@@ -1931,6 +1932,8 @@
"settings_notifications.beallitasok_ertesitesek": "Beállítások — Értesítések",
"settings_notifications.bekapcsolas_utan_az_egyes_alkalmazasokna": "Bekapcsolás után az egyes alkalmazásoknál is engedélyezni kell az email-küldést (az alkalmazás oldalán).",
"settings_notifications.biztonsagi_mentes_sikertelen": "Biztonsági mentés sikertelen",
"settings_notifications.clear_confirm": "Igen, törlöm a címet",
"settings_notifications.clear_warning": "Ha törlöd az értesítési e-mail-címet, nem kapsz több értesítő levelet. A mentés csak akkor történik meg, ha megerősíted.",
"settings_notifications.e_mail_cim": "E-mail cím",
"settings_notifications.elvart_adatbazismentes_elmaradt": "Elvárt adatbázismentés elmaradt",
"settings_notifications.elvart_fajlmentes_elmaradt": "Elvárt fájlmentés elmaradt",
@@ -1998,7 +2001,7 @@
"settings_security.uj_jelszo": "Új jelszó",
"settings_security.uj_jelszo_megerositese": "Új jelszó megerősítése",
"settings_security.utolso": "Utolsó:",
"settings_security.utolso_szinkronizalas": "Utolsó szinkronizálás: {{.GeoLastSync}}",
"settings_security.utolso_szinkronizalas": "Utolsó szinkronizálás: {{fmtTimeStr .GeoLastSync}}",
"settings_security.veszhelyzeti_informaciok": "Vészhelyzeti információk",
"settings_security.visszaallitasi_jelszo": "Visszaállítási jelszó",
"settings_system.a_kiszolgalo_ugynok_jelenleg_nem": "A kiszolgáló ügynök jelenleg nem elérhető — próbáld meg később.",
@@ -2239,7 +2242,7 @@
"storage.hiba": "Hiba:",
"storage.host_tarhely_rendszer_sablonok_iso": "Host tárhely — rendszer-sablonok, ISO-k, host szintű mentések. Nem tárol alkalmazásadatot.",
"storage.hozzaadas": "Hozzáadás",
"storage.idopont": "Időpont: {{.DecommissionedAt}}",
"storage.idopont": "Időpont: {{fmtTimeStr .DecommissionedAt}}",
"storage.inaktiv": "Inaktív",
"storage.ismeretlen": "ismeretlen",
"storage.kesz": "Kész",