hub: household health mail points to the dashboard, no raw note (R-79, D3)
The customer mail of health_degraded / health_critical / health_recovered no longer appends the raw details JSON (frozen wire text, English or Hungarian whatever the household's language). It says where to read the details: the dashboard. The operator mail keeps the note; every other event keeps its note. Red tests: r79_health_mail_test.go (2 failed on the old templates.go). Goldens regenerated for the six health mails. 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:
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
+2
@@ -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,
|
||||
|
||||
+2
@@ -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,
|
||||
|
||||
+2
@@ -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,
|
||||
|
||||
+2
@@ -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,
|
||||
|
||||
+2
@@ -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,
|
||||
|
||||
+2
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user