hub v0.79.0 — R-97c: make the operator-only claim true

v0.78.0 asserted in a comment that a type with no customerMessages entry cannot
reach a customer. It can: templates.go falls back to the raw message when the
entry is missing, and the only customer gate is prefs.EnabledEvents — pure
configuration. A customer with whole_guest_backup_failed enabled would have been
emailed raw English operator text about a backup they cannot act on. The new test
proves it against the v0.78.0 shape.

operatorOnlyEvents is now an explicit register checked before prefs, logging a
skipped/operator_only row so the skip is visible. NOT implemented as 'missing
customerMessages blocks delivery' — several types rely on that fallback on
purpose. The handler comment now names the real mechanism.
This commit is contained in:
2026-07-27 17:54:47 +02:00
parent 9ea5675950
commit 2c0e43e0d0
5 changed files with 228 additions and 4 deletions
+37
View File
@@ -1,5 +1,42 @@
# Felhom Hub — Changelog
## v0.79.0 — R-97c: make the operator-only claim TRUE (2026-07-27)
v0.78.0 shipped a comment asserting that `whole_guest_backup_failed` / `_recovered` were operator-only
because they have no `customerMessages` entry, "so the dispatcher **structurally cannot** route them
to a customer". **That was false**, and the code says so plainly:
- `templates.go` treats a missing entry as a **fallback to the raw message**, not a block —
`hunMessage := customerMessages[eventType]; if hunMessage == "" { hunMessage = message }`;
- the only customer gate is `isEventEnabled(prefs.EnabledEvents, ...)`**configuration**.
So a customer with `whole_guest_backup_failed` in their enabled list and an email set would have been
sent the raw English operator text about a backup they can take no action on. Proven by running the
new test against the v0.78.0 shape: it emails `customer@example.com`.
This is the `EffectiveProtected` shape — a doc comment claiming a property the code stopped
providing, which is how the samba false alarm survived.
**The fix:** an explicit `operatorOnlyEvents` register, checked at the top of `processCustomer`
**before prefs are consulted**, so no customer configuration can opt in. The skip is **logged**
(`status=skipped`, `error_message=operator_only`, `channel=customer`) rather than dropped — a silent
drop is indistinguishable from a delivery that never happened.
Deliberately **not** implemented as "a missing `customerMessages` entry blocks delivery": several
types rely on the raw-message fallback on purpose (`offbox_enlarge_blocked`'s dynamic Hungarian text
is customer-grade and a template would discard its numbers), so turning the fallback into a gate
would change behaviour well outside this concern.
The recovery type is listed too, even though its customer leg is pairing-gated on a "sent" row that
cannot exist — relying on that would make one type's safety a consequence of another type's routing,
true today and silently untrue the moment the failed event became customer-visible.
The `handler.go` comment now states the actual mechanism and warns that allowlisting a type does not
make it operator-only.
Tests +4, all run under the **breaking** configuration (customer has the event enabled AND an email),
not today's safe one. 17 packages ok.
## v0.78.0 — R-97a: the whole-guest backup tier gets a voice (operator-only) (2026-07-27)
`internal/quiesce` had no route to the hub at all. On 2026-07-27 three failed whole-guest backups and