1257014c2b
An event type the hub does not allowlist makes POST /event return 400 and the
event vanishes (R-97a). The controller cannot emit backup_target_absent until
this is live, so the hub half ships first.
E-2 Phase 0 established that an absent backup target has NO prompt signal today.
The controller's drive-gate path stops apps and logs a WARN but emits nothing:
NotifyStorageDisconnected is defined and never called anywhere (verified against
the gitignored-cmd/ trap with a positive control). A drive that is ONLY a backup
target has no apps to stop, so it is entirely silent. The sole signal is the
tier's own failure at its next due cycle -- up to ~24h on the daily local tier,
which is the R-100 shape: a real fault visible only after a deadline elapses.
Added to BOTH registers, because each half fails differently:
allowedEventTypes -- without it the event is lost at the door;
customerMessages -- without it the event IS delivered but in the controller's
raw operator English, and nothing looks broken.
backup_target_absent is deliberately NOT folded into storage_disconnected: that
says "a drive went away and some apps may have stopped"; this says "the thing
that makes your backup survive a disk failure is gone".
Hungarian copy names the consequence, not just the fact. backup_target_restored
is the paired recovery at info severity -- severityNotifies NOT widened.
Three tests pin the pair and the copy's substance. All red-proofed with the
mutation VERIFIED to have landed first: the initial attempt silently no-op'd
(gofmt had realigned the map) and the test "passed" -- a false proof that would
have been reported as evidence.
Green gate: build + vet + test rc=0, run separately from this commit.
274 lines
12 KiB
Go
274 lines
12 KiB
Go
package notify
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// budapest timezone for formatting.
|
|
var budapest *time.Location
|
|
|
|
func init() {
|
|
var err error
|
|
budapest, err = time.LoadLocation("Europe/Budapest")
|
|
if err != nil {
|
|
budapest = time.FixedZone("CET", 3600)
|
|
}
|
|
}
|
|
|
|
// ──────────────────────────────────────────────────────────────────────
|
|
// Operator email — concise, English
|
|
// ──────────────────────────────────────────────────────────────────────
|
|
|
|
// FormatOperatorEmail returns (subject, textBody) for the operator channel.
|
|
func FormatOperatorEmail(customerID, eventType, severity, message, detailsJSON string) (string, string) {
|
|
// Icon is eventType-aware for recovery (v0.71.0, audit F11); severity stays the fallback.
|
|
icon := "⚠️"
|
|
switch {
|
|
case strings.HasSuffix(eventType, "_recovered"):
|
|
icon = "✅"
|
|
case severity == "error" || severity == "critical":
|
|
icon = "🔴"
|
|
}
|
|
|
|
subject := fmt.Sprintf("[Felhom] %s %s: %s", icon, customerID, eventType)
|
|
|
|
now := time.Now().In(budapest).Format("2006-01-02 15:04 MST")
|
|
body := fmt.Sprintf(`Customer: %s
|
|
Event: %s
|
|
Severity: %s
|
|
Time: %s
|
|
Message: %s`, customerID, eventType, severity, now, message)
|
|
|
|
if detailsJSON != "" && detailsJSON != "{}" {
|
|
body += fmt.Sprintf("\nDetails: %s", detailsJSON)
|
|
}
|
|
|
|
body += fmt.Sprintf("\n\nDashboard: https://hub.felhom.eu/customers/%s", customerID)
|
|
|
|
return subject, body
|
|
}
|
|
|
|
// ──────────────────────────────────────────────────────────────────────
|
|
// Customer email — Hungarian, friendly
|
|
// ──────────────────────────────────────────────────────────────────────
|
|
|
|
// customerMessages maps event_type → Hungarian customer message.
|
|
var customerMessages = map[string]string{
|
|
// Customer-claim arc (v0.50.0)
|
|
"claim_lockout": "Túl sok hibás beállító/visszaállító kód próbálkozás történt — a beállító oldal 15 percre zárolva lett. Ha nem te próbálkoztál, jelezd az üzemeltetőnek.",
|
|
// Backup events
|
|
"backup_completed": "A biztonsági mentés sikeresen elkészült.",
|
|
"backup_failed": "A biztonsági mentés sikertelen! Kérjük, ellenőrizd a rendszert.",
|
|
"db_dump_completed": "Az adatbázis mentés sikeresen elkészült.",
|
|
"db_dump_failed": "Az adatbázis mentés sikertelen!",
|
|
"backup_integrity_ok": "A mentés integritás ellenőrzés sikeres.",
|
|
"backup_integrity_failed": "A mentés integritás ellenőrzés hibát talált!",
|
|
"crossdrive_completed": "A másodlagos mentés sikeresen elkészült.",
|
|
"crossdrive_failed": "A másodlagos mentés sikertelen!",
|
|
// Offsite-repo continuity (controller v0.142.0)
|
|
"offbox_repo_orphaned": "A távoli mentési tároló elárvult: a benne lévő mentések egy korábbi, már nem elérhető kulccsal készültek (jellemzően újratelepítés után). Új mentés a tároló visszaállításáig nem készül — nyisd meg a Távoli mentés oldalt.",
|
|
"offbox_repo_reset": "A távoli mentési tároló visszaállítva: a régi előzmény félretéve (nem törölve), és egy üres, új tároló jött létre a mostani kulccsal.",
|
|
|
|
// Disk events (GUEST — the controller's own cgroup view)
|
|
"disk_warning": "A lemezterület 90% felett van — kérjük, szabadíts fel helyet.",
|
|
"disk_critical": "A lemezterület kritikusan magas (95%+) — azonnali beavatkozás szükséges!",
|
|
|
|
// Host disk events (the Proxmox HOST root filesystem — distinct from the guest disk above)
|
|
"host_disk_warning": "A házszerver alaprendszerének (Proxmox-gazda) gyökérlemeze 90% felett van — kérjük, szabadíts fel helyet (pl. régi biztonsági mentések).",
|
|
"host_disk_critical": "A házszerver alaprendszerének gyökérlemeze kritikusan tele van (95%+) — azonnali beavatkozás szükséges, különben a rendszer hibázhat!",
|
|
|
|
// Per-storage fill events (a SPECIFIC tároló — mentési kötet, adatmeghajtó, tároló-készlet — telik be)
|
|
"storage_fill_warning": "Egy tároló a házszerveren 90% felett telt — kérjük, szabadíts fel helyet, mielőtt megtelik.",
|
|
"storage_fill_critical": "Egy tároló a házszerveren kritikusan tele van (95%+) — a rá készülő mentések/írások meghiúsulhatnak; azonnali beavatkozás szükséges!",
|
|
|
|
// Storage events
|
|
"storage_disconnected": "Egy meghajtó leválasztva — a mentések szünetelhetnek.",
|
|
"storage_reconnected": "A meghajtó újra csatlakoztatva.",
|
|
|
|
// E-2 — the drive the whole-system backup is written to is gone. Deliberately NOT folded into
|
|
// storage_disconnected: the customer action differs (reconnect THIS drive, or the system backup
|
|
// stops surviving a disk failure), and the fallback is honest about what still protects them.
|
|
"backup_target_absent": "A rendszermentés meghajtója nem érhető el — amíg vissza nem csatlakoztatod, a teljes rendszermentés nem készül el.",
|
|
"backup_target_restored": "A rendszermentés meghajtója újra elérhető — a mentés folytatódik.",
|
|
|
|
// Staleness events (Hub-generated)
|
|
"node_stale": "A szerver nem küldött jelentést az elmúlt időszakban.",
|
|
"node_down": "A szerver nem elérhető!",
|
|
"node_recovered": "A szerver újra elérhető.",
|
|
"host_recovered": "A házszerver alaprendszere (Proxmox-gazda) újra elérhető.",
|
|
|
|
// Health events
|
|
"health_degraded": "A rendszer állapota romlott.",
|
|
"health_critical": "A rendszer állapota kritikus!",
|
|
"health_recovered": "A rendszer állapota helyreállt.",
|
|
|
|
// Controller events
|
|
"controller_started": "A vezérlő elindult.",
|
|
"controller_updated": "A vezérlő frissítve lett.",
|
|
|
|
// Deadline events (Hub-generated)
|
|
"expected_backup_missed": "A mai biztonsági mentés nem készült el a határidőig!",
|
|
"expected_dbdump_missed": "A mai adatbázis mentés nem készült el a határidőig!",
|
|
|
|
// App lifecycle events
|
|
"app_deployed": "Alkalmazás telepítve.",
|
|
"app_removed": "Alkalmazás eltávolítva.",
|
|
"app_start_failed": "Egy telepített alkalmazás nem fut — ellenőrizze a rendszermonitort.",
|
|
|
|
// Disaster recovery events
|
|
"disaster_recovery_started": "Katasztrófa helyreállítás elindítva.",
|
|
"disaster_recovery_completed": "Katasztrófa helyreállítás befejezve.",
|
|
|
|
// Test
|
|
"test": "Ez egy teszt értesítés.",
|
|
}
|
|
|
|
// severityLabels maps severity to Hungarian labels.
|
|
var severityLabels = map[string]string{
|
|
"info": "Információ",
|
|
"warning": "Figyelmeztetés",
|
|
"error": "Hiba",
|
|
"critical": "Kritikus hiba",
|
|
}
|
|
|
|
// FormatCustomerEmail returns (subject, textBody) for the customer channel.
|
|
func FormatCustomerEmail(customerID, eventType, severity, message, detailsJSON string) (string, string) {
|
|
label := severityLabels[severity]
|
|
if label == "" {
|
|
label = severity
|
|
}
|
|
|
|
// Use the per-event-type Hungarian message if available, otherwise fall back to message
|
|
hunMessage := customerMessages[eventType]
|
|
if hunMessage == "" {
|
|
hunMessage = message
|
|
}
|
|
|
|
subject := fmt.Sprintf("[Felhom] %s: %s", label, hunMessage)
|
|
|
|
now := time.Now().In(budapest).Format("2006-01-02 15:04")
|
|
body := fmt.Sprintf(`Kedves Ügyfél!
|
|
|
|
A Felhom rendszered a következő értesítést küldte:
|
|
|
|
%s
|
|
|
|
Részletek:
|
|
- Szerver: %s
|
|
- Időpont: %s
|
|
- Szint: %s
|
|
- Típus: %s`, hunMessage, customerID, now, label, eventType)
|
|
|
|
if message != "" && message != hunMessage {
|
|
body += fmt.Sprintf("\n- Üzenet: %s", message)
|
|
}
|
|
|
|
if detailsJSON != "" && detailsJSON != "{}" {
|
|
body += fmt.Sprintf("\n- Megjegyzés: %s", detailsJSON)
|
|
}
|
|
|
|
body += `
|
|
|
|
Ha kérdésed van, vedd fel a kapcsolatot az üzemeltetővel.
|
|
|
|
Üdvözlettel,
|
|
Felhom.eu monitoring`
|
|
|
|
return subject, body
|
|
}
|
|
|
|
// ──────────────────────────────────────────────────────────────────────
|
|
// Customer-claim password arc (v0.50.0) — Hungarian code-delivery emails
|
|
// ──────────────────────────────────────────────────────────────────────
|
|
|
|
// FormatClaimEmail returns (subject, textBody) for a claim-arc email. kind is one of
|
|
// "claim" | "reset" | "claimed" (claim.EmailKind values). The code appears ONLY in the
|
|
// returned body — callers must never log it.
|
|
func FormatClaimEmail(kind, customerID, domain, code string) (string, string) {
|
|
dashboardURL := "https://felhom." + domain
|
|
switch kind {
|
|
case "reset":
|
|
subject := "[Felhom] Jelszó-visszaállítási kód"
|
|
body := fmt.Sprintf(`Kedves Ügyfél!
|
|
|
|
Jelszó-visszaállítást kértél a Felhom vezérlőpultodhoz.
|
|
|
|
Visszaállító kód: %s
|
|
|
|
A kód 72 óráig érvényes, és egyszer használható fel. Add meg a vezérlőpult
|
|
"Elfelejtett jelszó" oldalán, majd válassz új jelszót:
|
|
|
|
%s
|
|
|
|
Ha nem te kérted, hagyd figyelmen kívül — a jelenlegi jelszavad változatlan.
|
|
|
|
Üdvözlettel,
|
|
Felhom.eu`, code, dashboardURL)
|
|
return subject, body
|
|
case "claimed":
|
|
subject := "[Felhom] A vezérlőpultod mostantól jelszóval védett"
|
|
body := fmt.Sprintf(`Kedves Ügyfél!
|
|
|
|
A Felhom vezérlőpultod beállítása elkészült — a vezérlőpultod mostantól
|
|
jelszóval védett. A megadott jelszóval tudsz bejelentkezni:
|
|
|
|
%s
|
|
|
|
Ha nem te végezted a beállítást, azonnal vedd fel a kapcsolatot az üzemeltetővel.
|
|
|
|
Üdvözlettel,
|
|
Felhom.eu`, dashboardURL)
|
|
return subject, body
|
|
default: // "claim"
|
|
subject := "[Felhom] Elindult a Felhom szervered — beállító kód"
|
|
body := fmt.Sprintf(`Kedves Ügyfél!
|
|
|
|
Elindult a Felhom szervered. A vezérlőpult első használatához add meg az
|
|
alábbi beállító kódot, majd válassz saját jelszót:
|
|
|
|
Beállító kód: %s
|
|
|
|
A kód 72 óráig érvényes, és egyszer használható fel. A vezérlőpultot itt éred el:
|
|
|
|
%s
|
|
|
|
Ha nem kaptad volna meg időben, a vezérlőpult "Új kód kérése" gombjával
|
|
kérhetsz frisset — az mindig erre az e-mail címre érkezik.
|
|
|
|
Üdvözlettel,
|
|
Felhom.eu`, code, dashboardURL)
|
|
return subject, body
|
|
}
|
|
}
|
|
|
|
// FormatSelfBindEmail builds the customer-facing Hungarian email carrying the self-bind capability
|
|
// link (v0.66.0, R-27 slice 1). The link is the ONLY secret here — the passphrase is never in the
|
|
// mail (the customer already holds it), and the console pairing code is read off the box screen. The
|
|
// copy tells the customer they will need both factors on the page. Adult tone, no emoji.
|
|
func FormatSelfBindEmail(customerID, link string) (string, string) {
|
|
subject := "[Felhom] Kösd össze a Felhom dobozodat"
|
|
body := fmt.Sprintf(`Kedves Ügyfél!
|
|
|
|
Elkészült a Felhom dobozod, és készen áll az összekötésre. Az alábbi hivatkozáson
|
|
tudod te magad összekötni a fiókoddal — nincs szükség bejelentkezésre:
|
|
|
|
%s
|
|
|
|
A hivatkozás megnyitása után két adatot kell megadnod:
|
|
|
|
1. A párosító kódot, amely a doboz képernyőjén (a monitoron) látható.
|
|
2. A visszaállító jelszavadat (az 5 szóból álló kifejezést), amelyet a
|
|
beállításkor kaptál.
|
|
|
|
A hivatkozás 7 napig érvényes. Biztonsági okból 5 sikertelen próbálkozás után
|
|
zárolódik — ilyenkor vedd fel a kapcsolatot az ügyfélszolgálattal.
|
|
|
|
Ha nem te kérted ezt, hagyd figyelmen kívül ezt az e-mailt.
|
|
|
|
Üdvözlettel,
|
|
Felhom.eu`, link)
|
|
return subject, body
|
|
}
|