diff --git a/hub/internal/i18n/locales/en.json b/hub/internal/i18n/locales/en.json index c3bc17e0..af033266 100644 --- a/hub/internal/i18n/locales/en.json +++ b/hub/internal/i18n/locales/en.json @@ -45,6 +45,7 @@ "mail.customer.subject": "[Felhom] %s: %s", "mail.customer.body": "Dear Customer,\n\nYour Felhom system sent the following notification:\n\n%s\n\nDetails:\n- Server: %s\n- Time: %s\n- Level: %s\n- Type: %s", "mail.customer.line.message": "\n- Message: %s", + "mail.customer.line.health_dashboard": "\n\nYou can see the details on your dashboard.", "mail.customer.line.note": "\n- Note: %s", "mail.customer.signoff": "\n\nIf you have any questions, contact your operator.\n\nBest regards,\nFelhom.eu monitoring", "mail.claim.reset.subject": "[Felhom] Setup code to reset your password", diff --git a/hub/internal/i18n/locales/hu.json b/hub/internal/i18n/locales/hu.json index 54521675..dc2a2f5f 100644 --- a/hub/internal/i18n/locales/hu.json +++ b/hub/internal/i18n/locales/hu.json @@ -45,6 +45,7 @@ "mail.customer.subject": "[Felhom] %s: %s", "mail.customer.body": "Kedves Ügyfél!\n\nA Felhom rendszered a következő értesítést küldte:\n\n%s\n\nRészletek:\n- Szerver: %s\n- Időpont: %s\n- Szint: %s\n- Típus: %s", "mail.customer.line.message": "\n- Üzenet: %s", + "mail.customer.line.health_dashboard": "\n\nA részleteket a vezérlőpultodon látod.", "mail.customer.line.note": "\n- Megjegyzés: %s", "mail.customer.signoff": "\n\nHa kérdésed van, vedd fel a kapcsolatot az üzemeltetővel.\n\nÜdvözlettel,\nFelhom.eu monitoring", "mail.claim.reset.subject": "[Felhom] Beállító kód a jelszavad visszaállításához", diff --git a/hub/internal/notify/r79_health_mail_test.go b/hub/internal/notify/r79_health_mail_test.go new file mode 100644 index 00000000..197cb56e --- /dev/null +++ b/hub/internal/notify/r79_health_mail_test.go @@ -0,0 +1,66 @@ +package notify + +import ( + "strings" + "testing" +) + +// R-79 (D3, operator ruling 2026-10-08, `09` §3 decision 187): the HOUSEHOLD's system-health mail +// no longer carries the raw details JSON — a curly-bracket note with English lines in it that the +// household could not read. It says in plain words that something needs attention and points to the +// dashboard, where each issue is shown in the household's language. The OPERATOR's mail keeps the +// note, and every other event type keeps its note too. + +const r79HealthDetails = `{"previous_status":"ok","current_status":"fail","issues":["Protected container not running: cloudflared"]}` + +func TestR79_CustomerHealthMailHasNoRawNote(t *testing.T) { + for _, et := range []string{"health_critical", "health_degraded", "health_recovered"} { + for _, tc := range []struct{ lang, pointer string }{ + {"hu", "vezerl"}, // ASCII fragment of „vezérlőpult" (accented greps lie) + {"en", "dashboard"}, + } { + _, body := FormatCustomerEmail(tc.lang, "c1", et, "error", "msg", "", r79HealthDetails) + if strings.Contains(body, "{") || strings.Contains(body, "issues") || + strings.Contains(body, "Protected container") { + t.Errorf("%s/%s: customer mail still carries the raw note:\n%s", et, tc.lang, body) + } + if !strings.Contains(asciiFold(body), tc.pointer) { + t.Errorf("%s/%s: customer mail lacks the dashboard pointer %q:\n%s", et, tc.lang, tc.pointer, body) + } + } + } +} + +// Negative control: a health event with no details still gets the pointer (it is the event that +// carries it, not the note), and the pointer never appears on a non-health event. +func TestR79_PointerOnlyOnHealthEvents(t *testing.T) { + _, body := FormatCustomerEmail("en", "c1", "health_critical", "error", "msg", "", "") + if !strings.Contains(body, "dashboard") { + t.Errorf("health mail without details lacks the pointer:\n%s", body) + } + _, other := FormatCustomerEmail("en", "c1", "backup_failed", "error", "msg", "", `{"stack_name":"x"}`) + if strings.Contains(other, "on your dashboard") { + t.Errorf("a non-health event got the health pointer:\n%s", other) + } +} + +func TestR79_OperatorHealthMailKeepsNote(t *testing.T) { + _, body := FormatOperatorEmail("c1", "health_critical", "error", "msg", r79HealthDetails) + if !strings.Contains(body, "Protected container not running: cloudflared") { + t.Errorf("operator mail lost the details:\n%s", body) + } +} + +func TestR79_NonHealthCustomerMailKeepsNote(t *testing.T) { + _, body := FormatCustomerEmail("hu", "c1", "backup_failed", "error", "msg", "", `{"reason":"disk full"}`) + if !strings.Contains(body, `{"reason":"disk full"}`) { + t.Errorf("non-health customer mail lost its note:\n%s", body) + } +} + +// asciiFold drops the Hungarian accents so a search can use an ASCII fragment. +func asciiFold(s string) string { + r := strings.NewReplacer("á", "a", "é", "e", "í", "i", "ó", "o", "ö", "o", "ő", "o", "ú", "u", "ü", "u", "ű", "u", + "Á", "A", "É", "E", "Í", "I", "Ó", "O", "Ö", "O", "Ő", "O", "Ú", "U", "Ü", "U", "Ű", "U") + return r.Replace(s) +} diff --git a/hub/internal/notify/templates.go b/hub/internal/notify/templates.go index d266e682..bd778ce9 100644 --- a/hub/internal/notify/templates.go +++ b/hub/internal/notify/templates.go @@ -117,6 +117,14 @@ func bundleMessages(prefix string) map[string]string { return out } +// healthDashboardEvents are the system-health events whose customer mail points to the dashboard +// instead of appending the raw details (R-79). +var healthDashboardEvents = map[string]bool{ + "health_degraded": true, + "health_critical": true, + "health_recovered": true, +} + // FormatCustomerEmail returns (subject, textBody) for the customer channel, written in lang. // // SLICE 3 (R-558). Two things arrive from the box and they are NOT interchangeable: @@ -184,7 +192,14 @@ func FormatCustomerEmail(lang, customerID, eventType, severity, message, message body += b.Msgf(lang, "mail.customer.line.message", boxMessage) } - if detailsJSON != "" && detailsJSON != "{}" { + if healthDashboardEvents[eventType] { + // R-79 (D3, `09` §3 decision 187): the household's health mail carries no raw note. The + // details are `{previous_status, current_status, issues, warnings}` with the box's frozen + // wire text in them — English or Hungarian whatever the household's language. The + // dashboard shows each issue in the household's language; the mail points there. The + // operator's mail (FormatOperatorEmail) keeps the note. Pinned by r79_health_mail_test.go. + body += b.Msg(lang, "mail.customer.line.health_dashboard") + } else if detailsJSON != "" && detailsJSON != "{}" { body += b.Msgf(lang, "mail.customer.line.note", detailsJSON) } diff --git a/hub/internal/notify/testdata/mail_goldens/en/customer_event_health_critical.txt b/hub/internal/notify/testdata/mail_goldens/en/customer_event_health_critical.txt index 67edc764..6d67a91d 100644 --- a/hub/internal/notify/testdata/mail_goldens/en/customer_event_health_critical.txt +++ b/hub/internal/notify/testdata/mail_goldens/en/customer_event_health_critical.txt @@ -12,6 +12,8 @@ Details: - Level: Warning - Type: health_critical +You can see the details on your dashboard. + If you have any questions, contact your operator. Best regards, diff --git a/hub/internal/notify/testdata/mail_goldens/en/customer_event_health_degraded.txt b/hub/internal/notify/testdata/mail_goldens/en/customer_event_health_degraded.txt index 0804faa3..2e3dbfae 100644 --- a/hub/internal/notify/testdata/mail_goldens/en/customer_event_health_degraded.txt +++ b/hub/internal/notify/testdata/mail_goldens/en/customer_event_health_degraded.txt @@ -12,6 +12,8 @@ Details: - Level: Warning - Type: health_degraded +You can see the details on your dashboard. + If you have any questions, contact your operator. Best regards, diff --git a/hub/internal/notify/testdata/mail_goldens/en/customer_event_health_recovered.txt b/hub/internal/notify/testdata/mail_goldens/en/customer_event_health_recovered.txt index 70e90ca2..cbc19686 100644 --- a/hub/internal/notify/testdata/mail_goldens/en/customer_event_health_recovered.txt +++ b/hub/internal/notify/testdata/mail_goldens/en/customer_event_health_recovered.txt @@ -12,6 +12,8 @@ Details: - Level: Warning - Type: health_recovered +You can see the details on your dashboard. + If you have any questions, contact your operator. Best regards, diff --git a/hub/internal/notify/testdata/mail_goldens/hu/customer_event_health_critical.txt b/hub/internal/notify/testdata/mail_goldens/hu/customer_event_health_critical.txt index 2276c4a6..79e0e0e3 100644 --- a/hub/internal/notify/testdata/mail_goldens/hu/customer_event_health_critical.txt +++ b/hub/internal/notify/testdata/mail_goldens/hu/customer_event_health_critical.txt @@ -12,6 +12,8 @@ Részletek: - Szint: Figyelmeztetés - Típus: health_critical +A részleteket a vezérlőpultodon látod. + Ha kérdésed van, vedd fel a kapcsolatot az üzemeltetővel. Üdvözlettel, diff --git a/hub/internal/notify/testdata/mail_goldens/hu/customer_event_health_degraded.txt b/hub/internal/notify/testdata/mail_goldens/hu/customer_event_health_degraded.txt index 00be9b3a..40767db5 100644 --- a/hub/internal/notify/testdata/mail_goldens/hu/customer_event_health_degraded.txt +++ b/hub/internal/notify/testdata/mail_goldens/hu/customer_event_health_degraded.txt @@ -12,6 +12,8 @@ Részletek: - Szint: Figyelmeztetés - Típus: health_degraded +A részleteket a vezérlőpultodon látod. + Ha kérdésed van, vedd fel a kapcsolatot az üzemeltetővel. Üdvözlettel, diff --git a/hub/internal/notify/testdata/mail_goldens/hu/customer_event_health_recovered.txt b/hub/internal/notify/testdata/mail_goldens/hu/customer_event_health_recovered.txt index 6158aba2..26eae310 100644 --- a/hub/internal/notify/testdata/mail_goldens/hu/customer_event_health_recovered.txt +++ b/hub/internal/notify/testdata/mail_goldens/hu/customer_event_health_recovered.txt @@ -12,6 +12,8 @@ Részletek: - Szint: Figyelmeztetés - Típus: health_recovered +A részleteket a vezérlőpultodon látod. + Ha kérdésed van, vedd fel a kapcsolatot az üzemeltetővel. Üdvözlettel,