R-516 items 7-10: one banner per drive, local times, te-form, health banners in the household's language

- item 7: the storage page shows the disconnect time in local time (fmtTimeStr), not the raw RFC3339 UTC.
- item 8: a disconnected drive no longer has two banners - the health check's warning for it is dropped
  when the dedicated alert.storage.disconnected banner was built (it stays on the wire).
- item 9: alert.deadapp.group says "nezd meg" (te-form); formal ceiling 14 -> 13.
- item 10: the disk/memory/CPU/temperature health banners show the dashboard's own sentence
  (health.* keys, hu + en) via HealthReport.WarningMsgs/IssueMsgs; the wire text is unchanged.
  checkResources split out of RunHealthCheck as the test seam.

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-06 01:42:16 +02:00
parent 0f2eab7a3e
commit e4774e6a06
10 changed files with 388 additions and 115 deletions
+19 -3
View File
@@ -235,11 +235,16 @@ func (am *AlertManager) Refresh(report *monitor.HealthReport, cfg *config.Config
}
// From health check issues (critical)
for _, issue := range report.Issues {
for i, issue := range report.Issues {
// R-516 item 10: the banner shows the dashboard's own sentence (household language) when the
// check named one; the wire text is the fallback and stays untouched on the wire.
msg := report.IssueMsgAt(i)
alerts = append(alerts, Alert{
ID: "health-" + simpleHash(issue),
Level: "error",
Message: issue, // ON THE WIRE (report health.issues) -- not ours to translate; slice 3
Message: issue, // ON THE WIRE (report health.issues) -- the fallback when MessageKey is empty
MessageKey: msg.Key,
MessageArgs: msg.Args,
Link: "/monitoring",
LinkTextKey: "alert.link.monitoring",
})
@@ -247,10 +252,21 @@ func (am *AlertManager) Refresh(report *monitor.HealthReport, cfg *config.Config
// From health check warnings
for i, w := range report.Warnings {
// R-516 item 8: a disconnected drive already has its own banner above (alert.storage.disconnected,
// in the household's language). The health check's „Meghajtó leválasztva" warning for the same
// drive was a SECOND banner on every page — dropped here whenever the dedicated banners were built
// from the storage list. The warning itself stays on the wire (report health.warnings).
// Pinned by TestR516_DisconnectedDriveHasOneBanner.
if len(storagePaths) > 0 && report.WarningKindAt(i) == monitor.WarnKindStorageDisconnected {
continue
}
msg := report.WarningMsgAt(i) // R-516 item 10, as for the issues above
alert := Alert{
ID: "health-" + simpleHash(w),
Level: "warning",
Message: w, // ON THE WIRE (report health.warnings) -- not ours to translate; slice 3
Message: w, // ON THE WIRE (report health.warnings) -- the fallback when MessageKey is empty
MessageKey: msg.Key,
MessageArgs: msg.Args,
Link: "/monitoring",
LinkTextKey: "alert.link.monitoring",
}
+3 -3
View File
@@ -194,10 +194,10 @@ func TestPluralEnglish(t *testing.T) {
hu7 := am.GetAlerts("hu")[0].Message
en7 := am.GetAlerts("en")[0].Message
if hu4 != "4 telepített alkalmazás nem fut — nézze meg a rendszermonitort" {
if hu4 != "4 telepített alkalmazás nem fut — nézd meg a rendszermonitort" { // R-516 item 9: te-form
t.Errorf("the Hungarian sentence moved: %q", hu4)
}
if hu7 != "7 telepített alkalmazás nem fut — nézze meg a rendszermonitort" {
if hu7 != "7 telepített alkalmazás nem fut — nézd meg a rendszermonitort" { // R-516 item 9: te-form
t.Errorf("the Hungarian sentence moved: %q", hu7)
}
if !strings.Contains(en4, "4 installed apps are not running") {
@@ -213,7 +213,7 @@ func TestPluralEnglish(t *testing.T) {
t.Errorf("English singular: %q", got)
}
// And Hungarian at 1 is the SAME single form — no .one, no change.
if got := b.Msgf("hu", "alert.deadapp.group", 1); got != "1 telepített alkalmazás nem fut — nézze meg a rendszermonitort" {
if got := b.Msgf("hu", "alert.deadapp.group", 1); got != "1 telepített alkalmazás nem fut — nézd meg a rendszermonitort" {
t.Errorf("Hungarian must have one form at every count: %q", got)
}
}
+113
View File
@@ -0,0 +1,113 @@
package web
import (
"io"
"log"
"strings"
"testing"
"gitea.dooplex.hu/admin/felhom-controller/internal/config"
"gitea.dooplex.hu/admin/felhom-controller/internal/monitor"
"gitea.dooplex.hu/admin/felhom-controller/internal/settings"
)
// R-516 item 8 — a disconnected drive showed TWO banners on every page: its own
// (alert.storage.disconnected, in the household's language) and the health check's Hungarian
// „Meghajtó leválasztva" warning for the same drive. The dedicated banner is the one kept.
//
// COMPANION RED-PROOF: drop the WarnKindStorageDisconnected `continue` in AlertManager.Refresh — the
// first half fails with two banners.
func TestR516_DisconnectedDriveHasOneBanner(t *testing.T) {
const warn = "Meghajtó leválasztva: Külső (/mnt/usb)"
hr := &monitor.HealthReport{Status: "warn", Warnings: []string{warn, "CPU usage high: 91%"},
WarningKinds: []string{monitor.WarnKindStorageDisconnected, ""}}
cfg := &config.Config{}
cfg.Hub.Enabled = false
sp := []settings.StoragePath{{Path: "/mnt/usb", Label: "Külső", Disconnected: true}}
count := func(alerts []Alert) (dedicated, health, cpu int) {
for _, a := range alerts {
switch {
case strings.HasPrefix(a.ID, "storage-disconnected-"):
dedicated++
case a.Message == warn:
health++
case a.Message == "CPU usage high: 91%":
cpu++
}
}
return
}
am := NewAlertManager(log.New(io.Discard, "", 0))
am.Refresh(hr, cfg, nil, false, "", sp)
d, h, c := count(am.GetAlerts("hu"))
if d != 1 || h != 0 {
t.Errorf("a disconnected drive must have exactly one banner: dedicated=%d health-warning=%d", d, h)
}
if c != 1 {
t.Errorf("positive control: another health warning must still reach the banners (got %d)", c)
}
// Without the storage list there is no dedicated banner, so the health warning is the only one
// that can say it — it must stay.
am2 := NewAlertManager(log.New(io.Discard, "", 0))
am2.Refresh(hr, cfg, nil, false, "")
if d, h, _ := count(am2.GetAlerts("hu")); d != 0 || h != 1 {
t.Errorf("without the storage list the health warning is the only banner: dedicated=%d health-warning=%d", d, h)
}
}
// R-516 item 7 — the storage page printed the disconnect time as the raw stored RFC3339 UTC string
// („Leválasztva: 2026-09-14T19:58:02Z"). It now goes through fmtTimeStr, the page's local-time format,
// in both languages.
//
// COMPANION RED-PROOF: put `{{.DisconnectedAt}}` back in storage.levalasztva_2 (hu.json) — the hu half
// fails with the raw string.
func TestR516_DisconnectTimeIsLocalNotRawUTC(t *testing.T) {
const raw = "2026-09-14T19:58:02Z"
for _, tc := range []struct{ lang, prefix string }{{"hu", "Leválasztva: "}, {"en", "Disconnected: "}} {
d := i18nLayoutData("storage", "Tárhely")
paths := i18nStoragePaths()
paths[0]["DisconnectedAt"] = raw
d["StoragePaths"] = paths
h := renderBackupPageLang(t, tc.lang, "storage", d)
if strings.Contains(h, raw) {
t.Errorf("%s: the raw UTC timestamp reached the page", tc.lang)
}
if want := tc.prefix + fmtRFC3339Local(raw); !strings.Contains(h, want) {
t.Errorf("%s: the page must carry %q", tc.lang, want)
}
}
}
// R-516 item 10 — a health warning with a dashboard sentence shows it in the household's language;
// a warning without one shows its wire text verbatim (the fail-open fallback).
func TestR516_HealthBannersFollowTheHousehold(t *testing.T) {
hr := &monitor.HealthReport{Status: "fail",
Issues: []string{"SSD disk usage critical: 95%"},
IssueMsgs: []monitor.MsgRef{{Key: "health.system_disk_critical", Args: []interface{}{95.0}}},
Warnings: []string{"SSD disk usage high: 90%", "Docker: something"}, WarningKinds: []string{"", ""},
WarningMsgs: []monitor.MsgRef{{Key: "health.system_disk_high", Args: []interface{}{90.0}}, {}}}
cfg := &config.Config{}
cfg.Hub.Enabled = false
am := NewAlertManager(log.New(io.Discard, "", 0))
am.Refresh(hr, cfg, nil, false, "")
got := map[string][]string{}
for _, lang := range []string{"hu", "en"} {
for _, a := range am.GetAlerts(lang) {
if strings.HasPrefix(a.ID, "health-") {
got[lang] = append(got[lang], a.Message)
}
}
}
want := map[string][]string{
"hu": {"A rendszer tárhelye majdnem megtelt: 95% foglalt", "A rendszer tárhelye fogy: 90% foglalt", "Docker: something"},
"en": {"System storage is almost full: 95% used", "System storage is filling up: 90% used", "Docker: something"},
}
for lang, w := range want {
if strings.Join(got[lang], "|") != strings.Join(w, "|") {
t.Errorf("%s banners:\n got %q\n want %q", lang, got[lang], w)
}
}
}