monitoring page: storage labels follow the reader's language (W26, rehearsal 2026-10-20); unreleased
gates / gates (push) Successful in 1m11s
gates / gates (push) Successful in 1m11s
This commit is contained in:
@@ -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."
|
||||
}
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
|
||||
@@ -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, ""
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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."
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user