Files
felhom.eu/hub/internal/notify/templates.go
T
admin 6b40eb8619 hub: customer-claim password arc parts 1+2 — code engine, emails, ACK, configgen bake, UI (v0.50.0)
Closes DRILL-day0-vm F-4 hub-side: per-customer claim state (customer_claims,
bcrypt-only custody), the claim engine (issue at real config retrieve = Day-0
bake; first-report issue for live boxes; resend rotates generation; reset
rate-limited 3/day), three Hungarian emails via the dispatcher, report-ACK
claim object {code_hash, generation, issued_at} + set-only claimed ingest,
web.claim_code_* baked into generated controller.yaml, Setup-tab status chip
+ resend button, POST /api/v1/claim/reset-request (self-scoped), claim_lockout
event allowlisted. 13 new tests; full repo green.

Claude-Session: https://claude.ai/code/session_01NptTCFtu7dz2Ru89qHRagN
2026-07-12 18:12:48 +02:00

230 lines
9.0 KiB
Go

package notify
import (
"fmt"
"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 := "⚠️"
if 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!",
// 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.",
// 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ő.",
// 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
}
}