fix(disk-health): the alert that never sent — severity, a real Hiba level, and a memory that survives a restart
Three defects made the disk-health feature silent in exactly the case it
exists for. Evidence: felhom.eu documentation/audits/DIAG-smart-passed-trap-2026-08-14.md
1. SEVERITY (the one that changes whether anything arrives at all).
NotifyDiskHealthDegraded emitted severity "warn", which is NOT in the
hub's accepted set {info,warning,error,critical}. The hub coerced it to
"info" (hub/internal/api/handler.go) and severityNotifies dropped it
(hub/internal/notify/dispatcher.go), so every Figyelmeztetes-level disk
alert was filed as an informational notice and emailed to NOBODY, on the
customer and the operator leg alike. Now "warning". DiskAlertKind.Severity()
is exported so the contract is checkable from any package.
2. NO LEVEL ABOVE "worth an eye". smart_status.passed CANNOT fail on
unreadable sectors (attrs 187/197/198 all carry thresh 0 and a normalized
value floors at 1), so Hiba was unreachable for this whole fault class.
DiskVerdictFor now takes a DiskPrior and implements a 14-row top-down
ladder: sustained unreadable sectors, a count too large to be a blip (64),
unreadable+remapping together, overheating, NVMe critical flag or spent
endurance all reach Hiba. No fourth label — predicted failure is "Hiba".
3. IT SPOKE ONCE, AND FORGOT ON RESTART. The baseline was in-memory, so a box
that rebooted while a disk was failing never alerted again; and between 8
and 352 sectors nothing was emitted at all. State is now persisted
(disk-health-state.json, atomic tmp+rename), the decision compares against
the last ALERTED verdict (collapsing flaps to one alert while letting a
genuine escalation fire immediately), and a disk already at Hiba re-alerts
once it has BOTH doubled its count and waited out a 24h cooldown.
The card replays the same prior the check used (diskRecord.PriorSawUncorrectable)
so the chip and the email cannot disagree — the property the shared verdict
function exists to guarantee, now pinned rather than asserted.
Tests: 12 scenario groups A-L. Group L builds the Server through web.NewServer,
the same call main.go makes, over a real file.
This commit is contained in:
@@ -180,12 +180,13 @@ type Server struct {
|
||||
// manual trigger goes through the loop (stop stacks → backup → resume), never a bare agent call.
|
||||
backupTrigger BackupTrigger
|
||||
|
||||
// Disk-health card + 6h degradation check (v0.169.0). diskHealth holds the 60s /disks TTL cache +
|
||||
// the in-memory verdict baseline. disksFn / diskNotifyFn are test seams (nil → the real agent
|
||||
// client Disks() / the real notifier).
|
||||
// Disk-health card + periodic degradation check (v0.169.0; ladder + persistence v0.215.0).
|
||||
// diskHealth holds the 60s /disks TTL cache + the PERSISTED per-disk observation/alert history
|
||||
// (disk_health_state.go). disksFn / diskNotifyFn are test seams (nil → the real agent client
|
||||
// Disks() / the real notifier).
|
||||
diskHealth diskHealthState
|
||||
disksFn func(context.Context) (agentapi.DisksResponse, error)
|
||||
diskNotifyFn func(label string, attrs []string, critical bool)
|
||||
diskNotifyFn func(notify.DiskAlert)
|
||||
|
||||
// tiersFn is the sibling test seam for the agent's backup-tier view (nil → the real client's
|
||||
// BackupTiers()). Added with R-114 so the backup-target state — which is the source of a
|
||||
|
||||
Reference in New Issue
Block a user