docs(v0.215.0): CHANGELOG, CONTEXT decisions, README feature, REUSE entries
gates / gates (push) Successful in 10s
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:
@@ -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**
|
||||
|
||||
|
||||
Reference in New Issue
Block a user