Files
felhom.eu/REPORT.md
T

8.0 KiB
Raw Blame History

REPORT — Hub notification train v0.71.0: F11 + F12 + F14-light (2026-07-22)

Task: paired recovery notifications, prefs seeding at claim, empty-email no-clobber guard, priority headers, operator test leg — all in hub/; felhom-controller read-only reference. Origin: documentation/audits/AUDIT-power-outage-recovery-2026-07-22.md (F11/F12/F14-light).

Baselines (live-verified before starting)

Repo main @ start → shipped
felhom.eu 5b35023574173e17f9ee968ba09b6ab34616a322, hub 0.70.1 in manifests/hub.yaml hub 0.71.0 — code c766c8a, manifest f1da171
felhom-controller 0eba37d5cdc6 v0.160.0 unchanged (read-only)

Phase 0a — controller prefs-push fact (read-only, cited)

Does the startup push fire for a never-configured customer, and with what email? It does NOT fire: felhom-controller/controller/cmd/controller/main.go:821830 guards the startup sync with if prefs.Email != "" — an unconfigured box pushes nothing. The settings-save leg (internal/web/handlers.go:15321545, the 2026-07-15 wipe-guard) refuses empty-email-with-events outright; only the intentional all-off case (empty email + zero events) pushes an empty email, and that push is legitimate. Verdict: the clobber was LATENT on controller 0.160.0, not live — the hub-side guard shipped as the belt for older/rogue controllers. The debug re-sync (internal/web/handler_debug.go:432) pushes stored prefs as-is. Test-leg endpoint the UI invokes: POST /settings/notifications/test (internal/web/server.go:395settingsNotificationsTestHandler, handlers.go:1616) → notifier.SendTest() → hub POST /api/v1/event with eventType test.

Phase 0b — Resend custom-headers probe

One real mail via the Resend API (from monitoring@felhom.eu, to admin@felhom.eu, subject "[Felhom] header probe") with "headers": {"X-Priority":"1","Importance":"high"}HTTP 200, mail id 34d3f7f3-849f-432a-ac8e-eb479a1c6695. Key sourced from the k8s Secret into the shell env only; never written to any file or log. Mechanism GO.

Files modified (commit c766c8a, docs+code; manifest bump f1da171)

  • hub/internal/store/store.goLastCustomerSentAt (pairing-evidence query, uses the (customer_id, created_at DESC) index), SeedNotificationPrefs (INSERT OR IGNORE, empty-email no-op); strings import.
  • hub/internal/notify/dispatcher.gopriorityHeaders; recoveredPairedDownTypes; recovery branch in ProcessEvent (before the severity gate; severityNotifies byte-untouched); processRecovery (operator = existing processOperator; customer = blocked → prefs-nil/email → pairing → prefs cooldown; INFO logs on every gated skip); sendEmailFn/sendEmail grew headers map[string]string (payload carries "headers" only when non-empty; all callers updated); sendTestEmail operator copy with forced priority headers + latent nil-prefs panic fixed (GetNotificationPrefs returns (nil, nil) — the old code dereferenced prefs.Email).
  • hub/internal/notify/templates.go icon for *_recovered (eventType-aware, severity fallback); customerMessages["host_recovered"].
  • hub/internal/claim/engine.godefaultSeedEvents (critical-only) + seed call in MarkClaimed (WARN-and-continue on failure; claim never hinges on notification plumbing).
  • hub/internal/api/handler.gohandleSavePreferences empty-email no-clobber guard (INFO log).
  • Tests: notifications_seed_test.go (store), dispatcher_recovery_test.go (notify), seed_test.go (claim), preferences_guard_test.go (api); dispatcher_test.go seam signatures.
  • Docs: hub/CHANGELOG.md, CONTEXT.md (4 rulings), REUSE.md (§Alerting rewritten — serves as the notification-plane map; no standalone notification feature doc exists, noted below), 00-capability-map.md (§F rows), ROADMAP.md (R-68 SHIPPED, R-69 minted for F14-full).

Tests — 449 → 466 (17 new), full suite green (go build && go vet && go test ./... exit 0)

Group Test Result
A TestRecovery_PairedCustomerMail (down→recovery: both channels, Hungarian subject, subject, both log rows) PASS
B TestRecovery_UnpairedStaysCustomerSilent (operator only; enabled_events ignored) PASS
C TestRecovery_FlapDamping (suppressed down ⇒ no 2nd recovery mail; op 1 h cooldown) PASS
D TestMarkClaimed_SeedsNotificationPrefs / _NeverOverwritesExistingPrefs / _EmptyEmail_NoRow_ClaimSucceeds + store TestSeedNotificationPrefs_* (2) + TestLastCustomerSentAt_PairingQuery PASS
E TestSavePreferences_EmptyEmailCannotClobber / _NonEmptyEmailStillUpdates / _EmptyEmailNoStoredRow PASS
F TestTestEvent_BothChannels / TestTestEvent_NoPrefsRow_NoPanic PASS
G TestPriorityHeaders (table) / TestPriorityHeaders_EndToEnd (headers on error both channels, absent on warning) PASS
guard TestRecovery_SeverityStaysInfo (severityNotifies unwidened; non-recovery info silent) PASS

Red-proofs (each: mutation applied → FAIL confirmed → reverted → suite green):

  1. Pairing check forced open (if false && …) → TestRecovery_UnpairedStaysCustomerSilent FAILED ("unpaired recovery must NOT mail the customer, got 1"). ✔
  2. Seed via SaveNotificationPrefs upsert → TestMarkClaimed_NeverOverwritesExistingPrefs FAILED ("claim seed MODIFIED an existing row"). ✔
  3. Clobber guard removed → TestSavePreferences_EmptyEmailCannotClobber FAILED ("CLOBBERED the stored address: email="""). ✔
  4. Headers unconditional → TestPriorityHeaders_EndToEnd FAILED ("warning mail … must carry NO headers") (+ table test). ✔

Deploy verification

Image built + pushed (build.sh 0.71.0 --push), manifest bumped (f1da171), ArgoCD hard-refresh + deliberate sync → Synced / Healthy, pod hub-6ffbf574d8-p2w5p Running, deployed image gitea.dooplex.hu/admin/felhom-hub:0.71.0, startup log clean (checkers seeded 2 ok, no dispatch errors).

Live test-mail leg (the exact endpoint the UI invokes)

POST /settings/notifications/test on the demo-felhom guest controller (in-guest, session + CSRF): controller returned „Teszt email elküldve." Hub log: Event from demo-felhom: test (info)Test email sent to doodoo21@freemail.huOperator test email sent for demo-felhom. notification_log rows (UTC):

demo-felhom  test  customer  sent  Teszt értesítés      2026-07-22 18:59:34
demo-felhom  test  operator  sent  operator test copy   2026-07-22 18:59:34

Resend accepted both (no failed rows). HANDED TO VIKTOR: confirm the operator mail landed in Primary, the phone buzzed (the new Gmail filter's proof), and the client shows it high-priority.

NOT live-validated in this task (verbatim, per spec)

  • The natural *_recovered mail on a real staleness cycle — rides the next real event or the deliberate reboot drill; NOT fabricated by blocking reports (F9-bypass-shaped).
  • Seed-at-claim on a real claim — next onboarding; Peti's Friday reinstall is the natural candidate.

Observations (not acted on)

  • sendTestEmail nil-prefs panic was latent-reachable live: a „Teszt értesítés" click on a customer with no customer_notifications row (demo-hp today) would have panicked the dispatcher goroutine. Fixed in-scope (the function was already an edit surface); noted here because it predates this train.
  • No standalone notification-plane feature doc exists under documentation/ — N.4 was satisfied in REUSE.md §Alerting (the de-facto plane map) + the capability map row; if Viktor wants a dedicated architecture/notifications.md, that is a docs-only follow-up.
  • handleNotify (legacy), mailrelay, SendClaimEmail/SendSelfBindEmail byte-untouched (claim/ selfbind sends now pass explicit nil headers — no behavior change).
  • Website shows no hub version → N.6 n/a.
  • The notification_log is now pairing evidence (recovery gate reads it) — future retention/prune work must keep customer-channel stale/down/recovered rows long enough to pair (noted in REUSE).