v0.308.0: controller batch 2026-10-12 — clear-address warning (R-922), secret sweep over every page (R-255), read_failed (R-266), no NVMe heat band (R-333), share globe (R-577), Hungarian dates and numbers (R-562), lost-drive option pinned (R-521)
Operator rulings 2026-10-12, 09 §3 decisions 196, 204-207, 223, 227. MinAgent 0.131.0 unchanged. R-255's sweep found one leak, fixed here: an optional-config secret_input was rendered into its settings page; it is now revealed on demand. Hungarian parity fixtures re-captured once for R-562. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
@@ -59,7 +59,7 @@ type DiskPrior struct {
|
||||
//
|
||||
// 1. nil / "" / UNKNOWN -> Nincs adat
|
||||
// 2. Health == FAILING -> Hiba (drive self-reports)
|
||||
// 3. temperature_c >= 60 -> Hiba
|
||||
// 3. temperature_c >= 60 (NOT for NVMe — R-333) -> Hiba
|
||||
// 4. critical_warning > 0 (NVMe's own flag: a declaration) -> Hiba
|
||||
// 5. percentage_used >= 100 -> Hiba
|
||||
// 6. unreadable > 0 AND prior.SawUncorrectable -> Hiba (SUSTAINED)
|
||||
@@ -69,7 +69,7 @@ type DiskPrior struct {
|
||||
// 10. reallocated > 0 -> Figyelmeztetés
|
||||
// 11. media_errors > 0 -> Figyelmeztetés
|
||||
// 12. percentage_used >= 90 -> Figyelmeztetés
|
||||
// 13. temperature_c >= 55 -> Figyelmeztetés
|
||||
// 13. temperature_c >= 55 (NOT for NVMe — R-333) -> Figyelmeztetés
|
||||
// 14. otherwise -> Rendben
|
||||
//
|
||||
// WHY rows 2-8 exist at all: smart_status.passed CANNOT fail on unreadable sectors. Attributes 187,
|
||||
@@ -82,6 +82,12 @@ type DiskPrior struct {
|
||||
// drive sustain fires a full day earlier (12 Aug) than the count threshold (13 Aug). Row 8 exists for
|
||||
// a box that was powered off or restarted across the sustain window and so has no prior.
|
||||
//
|
||||
// WHY rows 3 and 13 skip an NVMe drive (R-333, `09` §3 decision 206, v0.308.0): the 55/60 °C bands are
|
||||
// SPINNING-DISK bands. A healthy NVMe idles at 53 °C (measured on demo-hp) and routinely passes 60 °C
|
||||
// under load with no fault, so the bands would tell a household "Hiba" about a working drive. An NVMe
|
||||
// drive judges its own heat: critical_warning bit 1 is the device's temperature alarm, and row 4 still
|
||||
// turns any critical_warning bit into Hiba. Pinned by TestDiskVerdict_NVMeHasNoHeatBand.
|
||||
//
|
||||
// Pure: no clock, no I/O, no logging. Everything it needs arrives as an argument.
|
||||
func DiskVerdictFor(s *SmartSummary, prior DiskPrior) DiskVerdict {
|
||||
// 1 — no data. Never alarms.
|
||||
@@ -94,8 +100,9 @@ func DiskVerdictFor(s *SmartSummary, prior DiskPrior) DiskVerdict {
|
||||
}
|
||||
// Health == PASSED (or any non-empty non-FAILING value we treat as passing): inspect the counters,
|
||||
// because the overall verdict is structurally unable to report this class of fault.
|
||||
nvme := IsNVMe(s)
|
||||
switch {
|
||||
case atLeast(s.TemperatureC, TemperatureFailC): // 3
|
||||
case !nvme && atLeast(s.TemperatureC, TemperatureFailC): // 3
|
||||
return DiskVerdictFail
|
||||
case positive(s.CriticalWarning): // 4
|
||||
return DiskVerdictFail
|
||||
@@ -118,7 +125,7 @@ func DiskVerdictFor(s *SmartSummary, prior DiskPrior) DiskVerdict {
|
||||
return DiskVerdictWarn
|
||||
case atLeast(s.PercentageUsed, percentageUsedWarn): // 12
|
||||
return DiskVerdictWarn
|
||||
case atLeast(s.TemperatureC, temperatureWarnC): // 13
|
||||
case !nvme && atLeast(s.TemperatureC, temperatureWarnC): // 13
|
||||
return DiskVerdictWarn
|
||||
}
|
||||
return DiskVerdictOK // 14
|
||||
@@ -190,12 +197,21 @@ func DegradedAttributes(s *SmartSummary) []string {
|
||||
if atLeast(s.PercentageUsed, percentageUsedWarn) {
|
||||
out = append(out, "elhasználódás")
|
||||
}
|
||||
// Newly able to trigger a verdict on its own (rows 3 and 13), so it must be nameable.
|
||||
if atLeast(s.TemperatureC, temperatureWarnC) {
|
||||
// Newly able to trigger a verdict on its own (rows 3 and 13), so it must be nameable. Not for an
|
||||
// NVMe drive (R-333): its heat never triggers a verdict, so naming it would give a wrong reason.
|
||||
if !IsNVMe(s) && atLeast(s.TemperatureC, temperatureWarnC) {
|
||||
out = append(out, "hőmérséklet")
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// IsNVMe reports whether the summary came from an NVMe drive. The agent fills critical_warning,
|
||||
// percentage_used and media_errors ONLY from smartctl's nvme_smart_health_information_log
|
||||
// (felhom-agent internal/storage/smart.go), so any of them present means NVMe; a SATA or USB drive
|
||||
// carries none of them.
|
||||
func IsNVMe(s *SmartSummary) bool {
|
||||
return s != nil && (s.CriticalWarning != nil || s.PercentageUsed != nil || s.MediaErrors != nil)
|
||||
}
|
||||
|
||||
func positive(p *int) bool { return p != nil && *p > 0 }
|
||||
func atLeast(p *int, n int) bool { return p != nil && *p >= n }
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
package agentapi
|
||||
|
||||
import "testing"
|
||||
|
||||
// R-333 (`09` §3 decision 206): no heat band for an NVMe drive. Both directions, because "fixed" means
|
||||
// one kind of drive stopped alarming on heat and the other did not, and the drive's own alarm stays.
|
||||
func TestDiskVerdict_NVMeHasNoHeatBand(t *testing.T) {
|
||||
noPrior := DiskPrior{}
|
||||
cases := []struct {
|
||||
name string
|
||||
in *SmartSummary
|
||||
want DiskVerdict
|
||||
}{
|
||||
// A healthy NVMe under load: 72 °C, the device raises no flag. No heat alarm.
|
||||
{"hot NVMe, no critical warning → ok", &SmartSummary{Health: SmartPassed, TemperatureC: ip(72), CriticalWarning: ip(0), PercentageUsed: ip(3), MediaErrors: ip(0)}, DiskVerdictOK},
|
||||
{"warm NVMe 56 °C, no critical warning → ok", &SmartSummary{Health: SmartPassed, TemperatureC: ip(56), CriticalWarning: ip(0)}, DiskVerdictOK},
|
||||
// The same drive with its own temperature bit set (critical_warning bit 1 = 0x02): Hiba.
|
||||
{"hot NVMe with critical_warning temperature bit → fail", &SmartSummary{Health: SmartPassed, TemperatureC: ip(72), CriticalWarning: ip(2)}, DiskVerdictFail},
|
||||
{"cool NVMe with critical_warning → fail", &SmartSummary{Health: SmartPassed, TemperatureC: ip(40), CriticalWarning: ip(1)}, DiskVerdictFail},
|
||||
// Spinning/SATA drives keep their 55/60 °C bands, unchanged.
|
||||
{"SATA 56 °C → warn", &SmartSummary{Health: SmartPassed, TemperatureC: ip(56), ReallocatedSectors: ip(0)}, DiskVerdictWarn},
|
||||
{"SATA 60 °C → fail", &SmartSummary{Health: SmartPassed, TemperatureC: ip(60), ReallocatedSectors: ip(0)}, DiskVerdictFail},
|
||||
{"SATA 54 °C → ok", &SmartSummary{Health: SmartPassed, TemperatureC: ip(54)}, DiskVerdictOK},
|
||||
}
|
||||
for _, c := range cases {
|
||||
if got := DiskVerdictFor(c.in, noPrior); got != c.want {
|
||||
t.Errorf("%s: DiskVerdictFor = %s, want %s", c.name, got.Label(), c.want.Label())
|
||||
}
|
||||
}
|
||||
// The alert body must not name heat as the reason for an NVMe drive.
|
||||
for _, a := range DegradedAttributes(&SmartSummary{Health: SmartPassed, TemperatureC: ip(72), CriticalWarning: ip(2)}) {
|
||||
if a == "hőmérséklet" {
|
||||
t.Error("an NVMe alert names temperature as an attribute; the verdict fired on the drive's own warning")
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user