hub v0.90.0 — a dropped notification leaves a trace, and the backup digest arrives (R-182)
gates / gates (push) Successful in 7s
gates / gates (push) Successful in 7s
processOperator's cooldown no longer returns bare. It dropped the event BEFORE LogNotification, so a suppressed operator alert and an event that never happened were indistinguishable — from the operator's side and from the hub's own records. Measured 2026-08-03: nine recovery_unit_capture_failed events arrived, two were mailed, seven left no row anywhere. That is why the defect took a day to get the right way round: there was nothing to read. A suppressed operator event now writes a `suppressed` row carrying the message and the key that suppressed it. This applies to EVERY operator event, not only the one that exposed it. It does NOT change the cooldown's duration or semantics. backup_run_failures: the per-run digest. In allowedEventTypes AND in operatorOnlyEvents — allowlisting alone does not make an event operator-only, and FormatCustomerEmail falls back to the raw English message rather than blocking. A test demonstrates a customer with the type enabled receiving nothing. recordOnlyEvents: a third routing class — stored and recorded, never mailed. recovery_unit_capture_failed moves here: it is the record, the digest is the notification. A register rather than downgrading severity to info, which would relabel a genuine failure as informational everywhere it is queried. cooldownRunSuffix: a sibling of cooldownTierSuffix, not a branch inside it, so tier keeps byte-identical semantics and R-97a's tests are untouched. It makes the cooldown effectively inert for the digest, which is the intent — a digest is already rate-limited by construction; the refresh sweep sends no run_id and so stays under the ordinary hourly cooldown. The email renders as a list, not a JSON blob. An absent space reading renders as unavailable, never as zeros.
This commit is contained in:
@@ -1,3 +1,40 @@
|
||||
## v0.90.0 — a dropped notification leaves a trace, and the backup digest arrives (2026-08-03, R-182)
|
||||
|
||||
**The smallest change on the board with the largest effect on trust: `processOperator`'s cooldown no
|
||||
longer returns bare.** It used to drop the event *before* `LogNotification`, so a suppressed operator
|
||||
alert and an event that never happened were indistinguishable — from the operator's side **and from
|
||||
the hub's own records**. Measured 2026-08-03: nine `recovery_unit_capture_failed` events arrived, two
|
||||
were mailed, and **seven left no row anywhere**. That is why the defect took a day to get the right
|
||||
way round: there was nothing to read.
|
||||
|
||||
A suppressed operator event now writes a `suppressed` row carrying the message and **the key that
|
||||
suppressed it**, so the collision is readable without reading code. This applies to **every** operator
|
||||
event, not only the one that exposed it. It deliberately does **not** change the cooldown's duration
|
||||
or semantics — it makes the drop visible, not absent.
|
||||
|
||||
**`backup_run_failures` — the per-run digest.** One operator mail at the end of a backup run listing
|
||||
every app that failed, its leg and its reason, with the counts and the target filesystem's free
|
||||
space. Added to `allowedEventTypes` **and** to `operatorOnlyEvents` — allowlisting alone does not make
|
||||
an event operator-only, and `FormatCustomerEmail` falls back to the raw English message rather than
|
||||
blocking. A test demonstrates a customer with the type in their enabled list receiving nothing.
|
||||
|
||||
**`recordOnlyEvents` — a third routing class.** Types that are STORED and RECORDED but never mailed.
|
||||
`recovery_unit_capture_failed` moves here: it is the durable per-failure record, and the digest is the
|
||||
notification. Deliberately a register rather than downgrading the severity to `info`, which would have
|
||||
had the same routing effect while relabelling a genuine failure as informational in the events table,
|
||||
the operator UI and every historical query.
|
||||
|
||||
**`cooldownRunSuffix` — the run discriminator.** A sibling of `cooldownTierSuffix` rather than a
|
||||
branch inside it, so `tier` keeps byte-identical semantics and R-97a's tests are untouched. It makes
|
||||
the cooldown effectively inert for the digest, **which is the intent**: a digest is already
|
||||
rate-limited by construction, one per run and only when something failed, so there is nothing for a
|
||||
timer to collapse — while the periodic refresh sweep sends **no** `run_id` and therefore stays under
|
||||
the ordinary hourly cooldown.
|
||||
|
||||
**The e-mail is rendered as a list, not a JSON blob** — the one operator mail with a variable-length
|
||||
payload, and a dozen apps on one line is unreadable on a phone at 07:00. An absent space reading
|
||||
renders as *unavailable*, never as zeros.
|
||||
|
||||
## v0.89.0 — the two halves of decision D-c (2026-08-02, R-167 · R-158)
|
||||
|
||||
**Decision D-c routes two new signals to two different audiences, and the hub is where that routing
|
||||
|
||||
Reference in New Issue
Block a user