docs(v0.215.0): CHANGELOG, CONTEXT decisions, README feature, REUSE entries
gates / gates (push) Successful in 10s

- CHANGELOG leads with the severity fix and the live warning-vs-warn proof.
- CONTEXT records the settled decisions so they are not re-litigated: Hiba is
  the label for predicted failure (no fourth word); sustain before count and
  why; the provenance of 64/55/60; phase 2 owns the new SMART attributes
  because they are a wire change under G-1; phase 1 state is one record per
  disk, not a series.
- README documents the 14-row ladder, the persisted state, the hourly cadence
  and the five message shapes.
- REUSE pins the severity wire contract on PushEvent — the defect's real home,
  so the next typo'd severity is caught at the table rather than in production
  — and records priorFor vs cardPriorFor, which differ by one observation and
  make the chip disagree with the email if mixed up.
This commit is contained in:
2026-08-14 08:34:31 +02:00
parent 34d83f5a02
commit 8144a70a72
4 changed files with 181 additions and 14 deletions
+77
View File
@@ -1,3 +1,80 @@
## v0.215.0 — the disk alert that never sent (2026-08-14, R-328..R-333)
**MinAgent: 0.129.0** (unchanged — every field this reads has been on the wire since agent v0.94.0/0.95.0)
**Lead with the one-word defect, because it is the one that decides whether anything arrives at all.**
`NotifyDiskHealthDegraded` set `severity := "warn"`. The hub accepts an exact-match lowercase
vocabulary — `{info, warning, error, critical}` — and **coerces anything else to `info`**
(`felhom.eu/hub/internal/api/handler.go:2121-2126`); `severityNotifies`
(`felhom.eu/hub/internal/notify/dispatcher.go:89-96`) then routes only warning/error/critical. `"warn"`
is not in that set. **Every Figyelmeztetés-level disk alert this product has ever produced was filed as
an informational notice and emailed to nobody**, on the customer leg and the operator leg alike. The
function's own doc comment said *"The hub applies its own per-event-type cooldown"* — an invariant
asserted in a comment, with nothing pinning it. Now `"warning"`, and `DiskAlertKind.Severity()` is
exported so the contract is checkable from any package rather than duplicated as a literal.
**PROVEN LIVE, side by side, through the real hub endpoint** (2026-08-14, demo-hp, synthetic events):
pushed at `"warning"` → stored `warning`, `notification_log` id 689, channel `operator`, status
**`sent`**. The same push at `"warn"` → stored **`info`**, and **no `notification_log` row exists at
all**. That pair is the whole task in two rows.
**A drive can never fail its own verdict on bad sectors, so the ladder stopped trusting it.** Attributes
187/197/198 all carry `thresh: 0` and a normalized SMART value floors at 1, so `smart_status.passed`
is **structurally incapable** of failing on unreadable sectors — the real drive stayed `PASSED` at 352
pending sectors with 1001 reported-uncorrectable reads. `DiskVerdictFor` now takes an
`agentapi.DiskPrior` and implements a 14-row top-down truth table (`internal/agentapi/diskverdict.go`).
Hiba is reached by: sustained unreadable sectors (present again at the next check), unreadable +
remapping together, a count >= 64, overheating (>= 60 °C), NVMe's own critical flag, or spent rated
endurance. **No fourth label** — predicted failure is **"Hiba"**, the word a broken drive already gets;
a fourth word sharing a root with "Figyelmeztetés" would make the more severe state read as the milder
one. `DegradedAttributes` now names the counters behind a Hiba reached from counters (it returns nil
only for a drive-reported FAILING, which has no single triggering counter).
**Provenance of the numbers, recorded in the code because a number without a reason becomes permanent.**
64 — the observed benign excursion peaked at 16 and cleared completely inside an hour; the terminal run
passed 64 at 13 Aug 11:28 and never came back. Sustain sits ABOVE the count in the table because on the
real drive it fires a full day earlier; the count is the backstop for a box that was powered off across
the sustain window. 55/60 °C — the operator's existing Prometheus bands on DooPlex, adopted unchanged.
**It spoke once, and forgot on restart.** The baseline was in-memory, so a controller that restarted
while a disk was failing re-baselined it silently and never alerted again; and between 8 and 352
sectors the verdict never changed level, so nothing further was emitted. State is now persisted to
`disk-health-state.json` in `cfg.Paths.DataDir` (atomic tmp+rename, the `selfupdate.SaveState` shape;
a missing file is normal, a corrupt one is LOGGED and treated as no-prior — never fatal). The decision
compares against the **last ALERTED** verdict, not the last observed one, which collapses a
Warn→OK→Warn flap to a single alert while leaving a genuine escalation free to fire immediately; and a
disk already at Hiba re-alerts once it has BOTH doubled its unreadable-sector count AND waited out a
24h cooldown (an AND — clearing one bar emits nothing).
**The card replays the prior that produced the verdict, not the one the check went on to write.**
`diskRecord.PriorSawUncorrectable`. Without it the dashboard chip read one level MORE severe than the
alert for the same disk — caught by the Scenario B test, not by review. The shared-verdict-function
guarantee is now pinned rather than asserted.
**Hourly, and that was measured rather than preferred.** On demo-hp the real `/disks` fetch costs
min 0.805s / median 0.821s / max 0.841s over 10 calls, all HTTP 200, 3 physical disk rows — 6x under
the 5s bar that would have kept it at 6h. The benign excursion lasted about ONE HOUR, and a 6-hourly
sampler can land either side of one and then catch the terminal run half a day late.
`cmd/controller/main.go`. The check also logs a POSITIVE observable every cycle
(`disk-health check complete: N disk(s) evaluated, M alert(s)`) — zero events from a check that never
ran is not evidence of health.
**Message shapes.** A customer told "the drive overheated" must not be told to arrange a replacement.
Five shapes: Figyelmeztetés (wording unchanged, now actually delivered), drive-reported FAILING,
Hiba-from-sector-count, Hiba-from-heat, and the still-worsening re-alert.
**Tests.** 12 scenario groups A-L, 1391 -> 1413 test functions, suite green. **11 of 12 companion
red-proofs failed as required; red-proof A PASSED and is reported as a finding** — removing truth-table
row 6 leaves the real drive tripping row 8 at 352 sectors, so that mutation cannot fail a test built on
the real drive's values. Row 6 IS pinned, by `TestLadder_SustainIsWhatFires` and
`TestDiskLadder_SustainDrivesTheEscalation`, which hold the counters at 8 and vary only the prior; both
fail under that mutation. Group L builds the Server through `web.NewServer` — the same call
`main.go` makes — over a real file, and runs TWO checks after the restart, because one cannot
distinguish a loaded state from a silent re-baseline.
**Not live-validated:** the Fail-from-counters path has never fired on real hardware (R-332).
Evidence: `felhom.eu/documentation/audits/DIAG-smart-passed-trap-2026-08-14.md` + two fixtures.
## v0.214.0 — the recovery screen stops hedging about a code it can now check (2026-08-12, R-311)
**MinAgent: 0.129.0**