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**
|
||||
|
||||
|
||||
+58
-1
@@ -7,7 +7,64 @@
|
||||
>
|
||||
> Ask Claude Code: "Please update CONTEXT.md with what we did today"
|
||||
|
||||
Last updated: 2026-08-08 (v0.208.0 — R-254: the last two secrets leave the page source, and a gate against a fourth)
|
||||
Last updated: 2026-08-14 (v0.215.0 — R-328..R-333: the disk alert that never sent)
|
||||
|
||||
> **2026-08-14 — v0.215.0 (R-328..R-333). Disk health, phase 1: the alert that reached nobody.**
|
||||
>
|
||||
> ### A severity string is a WIRE CONTRACT with the hub, not a label we choose.
|
||||
>
|
||||
> The hub accepts exactly `{info, warning, error, critical}` and **silently coerces anything else to
|
||||
> `info`**, which `severityNotifies` then drops. `disk_health_degraded` shipped `"warn"` — one letter
|
||||
> short of the contract — so **every Figyelmeztetés-level disk alert this product ever produced was
|
||||
> emailed to nobody, on both legs.** Proven live side by side on 2026-08-14: `"warning"` →
|
||||
> `notification_log` status **`sent`**; `"warn"` → stored `info`, **no row at all**.
|
||||
> `app_start_failed` (`notifier.go` ~L546) carries the identical defect and was deliberately NOT
|
||||
> changed here — it needs its own decision on whether it should notify (**R-329**).
|
||||
>
|
||||
> ### A drive's own PASSED verdict cannot fail on bad sectors. Do not build on it.
|
||||
>
|
||||
> Attributes 187/197/198 all carry `thresh: 0`; a normalized SMART value floors at 1 and can never drop
|
||||
> to or below the threshold. The real drive (ST3000VX010, S/N Z6A07P2G) read `PASSED` at **352** pending
|
||||
> sectors and **1001** reported-uncorrectable reads. Felhom already read the raw counters, which is the
|
||||
> only reason it would have noticed at all. Evidence + fixtures:
|
||||
> `felhom.eu/documentation/audits/DIAG-smart-passed-trap-2026-08-14.md`.
|
||||
>
|
||||
> **DECISIONS MADE, so they are not re-litigated:**
|
||||
>
|
||||
> - **Predicted failure is labelled „Hiba" — there is no fourth verdict word.** A fourth Hungarian word
|
||||
> sharing a root with „Figyelmeztetés" would make the MORE severe state read as the milder one. Four
|
||||
> labels, final: Rendben / Figyelmeztetés / Hiba / Nincs adat.
|
||||
> - **Sustain is the primary rule; the count is the backstop.** Truth-table row 6 (unreadable sectors
|
||||
> present again at the next check) sits ABOVE row 8 (count >= 64) because on the real drive sustain
|
||||
> fires 12 Aug and the count not until 13 Aug. Row 8 exists only for a box powered off across the
|
||||
> sustain window.
|
||||
> - **The numbers and where they come from.** 64: the benign excursion peaked at 16 and cleared inside
|
||||
> an hour; the terminal run passed 64 at 13 Aug 11:28 and never returned. **It is a judgement from ONE
|
||||
> drive** — a static backstop, expected to be replaced by growth-rate detection in Phase 3. 55/60 °C:
|
||||
> the operator's existing Prometheus bands on DooPlex, adopted unchanged so the two systems cannot
|
||||
> disagree about the same drive. **These bands are SPINNING-DISK bands and are questionable for NVMe**
|
||||
> — demo-hp's healthy Toshiba NVMe idles at **53 °C**, 2 °C below Figyelmeztetés (**R-333**).
|
||||
> - **Phase 2 owns the new SMART attributes (187 Reported_Uncorrect, 199, 188).** They are a declared
|
||||
> wire change, so under the G-1 gate the hub must model them in the same session. Putting them here
|
||||
> would have turned a one-word severity fix into a three-repo change (**R-330**). Everything v0.215.0
|
||||
> needs was already on the wire.
|
||||
> - **Phase 1 state is one small record per disk, NOT a sample series.** `metrics.MetricsStore` is the
|
||||
> right home for Phase 2/3 history; using it now would have put a schema migration on the critical
|
||||
> path of the severity fix.
|
||||
>
|
||||
> **The trap this change nearly shipped, caught by a test and not by review:** the card and the check
|
||||
> share one verdict function so the chip and the email can never disagree — but the check CONSUMES the
|
||||
> prior and then overwrites it, so a card rendering afterwards read its own check's write and showed one
|
||||
> level MORE severe than the alert. Fixed by `diskRecord.PriorSawUncorrectable`, which replays the prior
|
||||
> that produced the stored verdict. The guarantee was previously asserted in a comment only.
|
||||
>
|
||||
> **Cadence is hourly, and it was MEASURED:** demo-hp `/disks` costs median 0.821s (min 0.805 / max
|
||||
> 0.841, 10 calls, 3 physical rows) — 6x under the 5s bar. Open question deliberately NOT acted on: the
|
||||
> agent runs bare `smartctl -a -j` with **no `-n standby`**, so an hourly poll would wake a spun-down
|
||||
> HDD. demo-hp is all-flash so the measurement could not show it (**R-333**).
|
||||
>
|
||||
> **NOT live-validated:** the Fail-from-counters path has never fired on real hardware — only against
|
||||
> the fixture's values in unit tests (**R-332**).
|
||||
|
||||
> **2026-08-08 — v0.208.0 (R-254). THE RULE, stated so it outlives this session:**
|
||||
>
|
||||
|
||||
@@ -159,15 +159,18 @@
|
||||
| `Client.AddNetStorage/ListNetStorage/RemoveNetStorage` | controller/internal/agentapi/client.go | NAS mounts (A1) | Network storage | Password passes through to agent's 0600 cred file; controller NEVER persists it |
|
||||
| `agentapi.StatusError` | controller/internal/agentapi/client.go | `{Path, Code}` typed non-2xx GET error | Distinguishing HTTP statuses from transport errors (`errors.As`) | NEVER string-match agent error text — the capability probe keys on `Code==404` |
|
||||
| `SupportCache.Supports` / `Client.Supports` | controller/internal/agentapi/features.go | `(ctx, prober, Feature) SupportState` | Agent-capability gate for COUPLED features (route probe, TTL 5m) | 404 ⇒ No; transport/5xx ⇒ Unknown (NEVER refuse on Unknown). New coupled feature = new `featureProbes` row + gate call at the entry point + `MinAgent:` in the CHANGELOG header (publish-train-rules.md). Web layer: `Server.netFeatures` through the `netAgent` seam |
|
||||
| `agentapi.DiskVerdictFor` / `DiskVerdict.Label` / `DegradedAttributes` | controller/internal/agentapi/diskverdict.go | `(*SmartSummary) DiskVerdict` | THE shared disk-health verdict (card chip + 6h check) — v0.169.0 | Pure; nil/UNKNOWN → `DiskVerdictUnknown` (Nincs adat, NEVER alarms); percentage_used threshold is **≥90**. Feature-detects the agent's `DiskInfo.Smart` (nil = old agent). Do NOT recompute the verdict inline anywhere |
|
||||
| `agentapi.DiskVerdictFor` / `DiskVerdict.Label` / `DegradedAttributes` / `UncorrectableSectors` / `DiskPrior` / `TemperatureFailC` | controller/internal/agentapi/diskverdict.go | `(*SmartSummary, DiskPrior) DiskVerdict` | THE shared disk-health verdict (card chip + hourly check) — v0.169.0, 14-row ladder v0.215.0 | Pure — no clock, no I/O; history arrives as `DiskPrior`. nil/UNKNOWN → `DiskVerdictUnknown` (Nincs adat, NEVER alarms, row 1 is first for that reason). **Never trust `smart_status.passed`**: attrs 187/197/198 carry `thresh: 0`, so it cannot fail on unreadable sectors. A zero `DiskPrior` is the fail-safe (first sighting can only reach Figyelmeztetés). **Four labels, no fifth** — predicted failure is „Hiba". Do NOT recompute the verdict inline anywhere, and do NOT re-literal 60 °C — use `TemperatureFailC` |
|
||||
| `Server.resolveBackupTargetState` / `backupTargetView` | controller/internal/web/backup_target_offer.go | `(ctx)` → state / `*BackupTargetView` (nil = render nothing) | The whole-system backup-target answer: healthy · degraded-never-configured · **TargetAbsent** (configured, drive gone) · unknown | Test seams `Server.tiersFn` + `Server.disksFn` (nil → the real client). **`degradedMessageFor` is the ONE place that decides customer copy** — add a state there, never in a template. `backupTargetView` returns **nil** for healthy AND unknown so a template typo cannot decorate a working box. R-112: this state had NO consumer for two releases; the render is server-side on `backups.html`, and the seam test drives `backupsHandler` and asserts rendered HTML |
|
||||
| `Server.cachedDisks` / `RunDiskHealthCheck` | controller/internal/web/disk_health.go | `(ctx)` | Card fetch (60s TTL) / the 6h degradation check | Card uses the 60s TTL cache (anti-smartctl-storm); the CHECK fetches FRESH (`fetchDisks`). Test seams: `Server.disksFn` (source) + `Server.diskNotifyFn` (sink). Baseline is in-memory (restart re-baselines) |
|
||||
| `Server.cachedDisks` / `RunDiskHealthCheck` | controller/internal/web/disk_health.go | `(ctx)` | Card fetch (60s TTL) / the hourly degradation check | Card uses the 60s TTL cache (anti-smartctl-storm); the CHECK fetches FRESH (`fetchDisks`). Test seams: `Server.disksFn` (source) + `Server.diskNotifyFn(notify.DiskAlert)` (sink). State is PERSISTED (v0.215.0) — a restart no longer re-baselines |
|
||||
| `diskAlertDecision` / `diskAlertKindFor` / `Server.priorFor` / `Server.cardPriorFor` | controller/internal/web/disk_health_state.go | pure + `(key) agentapi.DiskPrior` | Whether an observation emits, and which message shape | Compares against the **last ALERTED** verdict, not the last observed — that is what collapses a flap to one alert. Re-alert needs doubling **AND** 24h (an AND). **`priorFor` is for the CHECK, `cardPriorFor` for the CARD** — they differ by one observation and mixing them makes the chip read one level more severe than the email |
|
||||
| `diskRecord` / `writeDiskState` / `Server.loadDiskStateLocked` | controller/internal/web/disk_health_state.go | `disk-health-state.json` in `cfg.Paths.DataDir` | Persisted per-disk observation + alert history | Atomic tmp+rename (the `selfupdate.SaveState` shape, copied not imported). Missing file = normal; corrupt = LOG and fall back to no-prior, **never fatal**. Written ONCE per check run. Keyed by `diskKey`. **One record per disk, NOT a sample series** — history is Phase 2/3 in `metrics.MetricsStore` |
|
||||
|
||||
### Notifications / hub sync
|
||||
|
||||
| Symbol | File | Short signature | Use for | Gotchas |
|
||||
|---|---|---|---|---|
|
||||
| `Notifier.PushEvent` | controller/internal/notify/notifier.go | `(eventType, severity, message, details)` | Hub events | Async goroutine, 3 attempts/3s backoff. NEW event types MUST be added to hub `allowedEventTypes` or POST /event 400s; hub only emails `warning`/`error` from this path |
|
||||
| `Notifier.PushEvent` | controller/internal/notify/notifier.go | `(eventType, severity, message, details)` | Hub events | Async goroutine, 3 attempts/3s backoff. NEW event types MUST be added to hub `allowedEventTypes` or POST /event 400s. **SEVERITY IS AN EXACT WIRE CONTRACT: `{"info","warning","error","critical"}` and nothing else.** The hub silently COERCES any other string to `"info"` (`hub/internal/api/handler.go`, the ingest severity switch) and `severityNotifies` (`hub/internal/notify/dispatcher.go`) emails only warning/error/critical — so a typo'd severity is stored and delivered to NOBODY, with no error anywhere. **`"warn"` is not a severity.** It shipped on `disk_health_degraded` (fixed v0.215.0, R-328) and is STILL live on `app_start_failed` (R-329) |
|
||||
| `notify.DiskAlert` / `DiskAlertKind` / `DiskAlertKind.Severity()` | controller/internal/notify/notifier.go | `NotifyDiskHealthDegraded(DiskAlert)` | The disk-health alert payload + its five Hungarian message shapes | The notifier owns customer copy — pass a `DiskAlert`, never a pre-formatted string, or Hungarian scatters across packages. `Severity()` is the ONE mapping kind→hub severity and is exported so any package can assert the contract instead of duplicating the literal |
|
||||
| `Notifier.Notify*` convenience methods | controller/internal/notify/notifier.go | typed wrappers (backup/DB/storage/channel/DR…) | Standard events | Add a typed wrapper rather than raw PushEvent calls |
|
||||
| `report.BuildReport` / `Pusher.Push` | controller/internal/report/builder.go + pusher.go | periodic hub report | Box→hub reporting | ACK carries `config_version` → `ConfigRefresher.Reconcile` |
|
||||
| `report.Trigger` (`NewTrigger`/`Fire`/`Run`) | controller/internal/report/trigger.go | `Fire()` after a hub-relevant user action | THE out-of-cycle report push (v0.139.0) — fire via `api.Router.reportPushNow` / `web.Server.reportTriggerNow`, both nil-safe | Coalesce-and-eventually-fire (trailing edge; quiet 2s, min spacing 15s). NEVER add retries (Pusher owns them); NEVER reuse the `internal/sync` REFUSE-debounce for hub pushes (a refused fire loses the update until the next cycle). Fire only AFTER a successful local commit |
|
||||
|
||||
+40
-10
@@ -261,21 +261,51 @@ backups, monitoring and notifications. All Proxmox/disk operations are delegated
|
||||
|
||||
## Features
|
||||
|
||||
### Disk health — "Lemezek állapota" card + degradation alert (v0.169.0)
|
||||
### Disk health — "Lemezek állapota" card + degradation alert (v0.169.0; severity ladder v0.215.0)
|
||||
|
||||
The dashboard shows a per-physical-disk health card driven by the agent's SMART summary (serialized
|
||||
into `/disks` from agent v0.94.0 — no new smartctl load; the controller only reads it). One pure
|
||||
verdict function (`agentapi.DiskVerdictFor`) is the shared truth for the card chip and the check:
|
||||
verdict function (`agentapi.DiskVerdictFor`) is the shared truth for the card chip and the check.
|
||||
|
||||
**Why it does not trust `smart_status.passed`:** attributes 187/197/198 all carry `thresh: 0` and a
|
||||
normalized SMART value floors at 1, so a drive's own overall verdict is **structurally incapable** of
|
||||
failing on unreadable sectors — the real failing drive read `PASSED` at 352 pending sectors. The ladder
|
||||
reads the raw counters instead. Evidence:
|
||||
`felhom.eu/documentation/audits/DIAG-smart-passed-trap-2026-08-14.md`.
|
||||
|
||||
**Four labels, and there will not be a fifth** — a predicted failure is „Hiba", the same word a
|
||||
self-reported failure gets:
|
||||
|
||||
- **Rendben** — clean.
|
||||
- **Figyelmeztetés** — a first sighting of unreadable sectors below the bar, reallocated sectors, NVMe
|
||||
media errors, `percentage_used ≥ 90`, or `temperature_c ≥ 55`.
|
||||
- **Hiba** — the drive reports FAILING, **or** (v0.215.0) unreadable sectors are **sustained** across
|
||||
two consecutive checks, unreadable sectors sit alongside reallocation, the count reaches **64**,
|
||||
`temperature_c ≥ 60`, NVMe's `critical_warning` is set, or `percentage_used ≥ 100`.
|
||||
- **Nincs adat** — nil/UNKNOWN/old agent. Never alarms, and never erases a disk's stored history.
|
||||
|
||||
`DiskVerdictFor(smart, prior)` takes an `agentapi.DiskPrior`; a zero prior is the fail-safe, so a first
|
||||
sighting can only reach Figyelmeztetés. The full 14-row truth table, evaluated top-down, is documented
|
||||
in `internal/agentapi/diskverdict.go` together with the provenance of 64 / 55 / 60.
|
||||
|
||||
- **Rendben** (PASSED, clean) · **Figyelmeztetés** (PASSED but reallocated/pending/offline-uncorrectable
|
||||
or NVMe critical/media/percentage-used ≥ 90) · **Hiba** (FAILING) · **Nincs adat** (nil/UNKNOWN/old
|
||||
agent — never alarms).
|
||||
- The card fetches `/disks` through a **60 s TTL cache** (dashboard refresh-spam can't smartctl-storm
|
||||
the host); an unreachable agent renders "Nincs adat" and the page still loads.
|
||||
- A **6-hourly `disk-health-check`** emits `disk_health_degraded` (warn/critical) **only on a
|
||||
degradation** vs an in-memory baseline — first run baselines silently, recovery/UNKNOWN never notify,
|
||||
and a controller restart re-baselines silently. **No global banner** (deliberate): the card + email
|
||||
carry it. The hub allowlist must include `disk_health_degraded`.
|
||||
the host); an unreachable agent renders "Nincs adat" and the page still loads. The card replays the
|
||||
prior that produced the stored verdict (`diskRecord.PriorSawUncorrectable`) so the chip and the email
|
||||
cannot disagree.
|
||||
- An **hourly `disk-health-check`** (v0.215.0; was 6-hourly — the real drive's benign excursion lasted
|
||||
about one hour, and `/disks` measures ~0.82 s) emits `disk_health_degraded` against **persisted**
|
||||
per-disk state in `disk-health-state.json` under `cfg.Paths.DataDir`. It fires on an escalation
|
||||
against the **last alerted** verdict, and re-alerts a disk already at Hiba once it has **both**
|
||||
doubled its unreadable-sector count **and** waited out a 24 h cooldown. A disk's first verdict
|
||||
baselines silently; recovery and UNKNOWN never notify; a disappeared disk is forgotten. A restart no
|
||||
longer re-baselines — that is why the state is persisted. Every cycle logs a positive observable
|
||||
(`disk-health check complete: N disk(s) evaluated, M alert(s)`).
|
||||
- **Severity is a wire contract:** the alert emits `warning` / `critical` — the hub coerces anything
|
||||
outside `{info, warning, error, critical}` to `info` and never emails it. `DiskAlertKind.Severity()`
|
||||
is the single mapping. The hub allowlist must include `disk_health_degraded`.
|
||||
- Five message shapes (`notify.DiskAlert`), because the customer's action differs: Figyelmeztetés,
|
||||
drive-reported FAILING, Hiba-from-sector-count, Hiba-from-heat, and the still-worsening re-alert.
|
||||
- **No global banner** (deliberate): the card + email carry it.
|
||||
|
||||
### 1. App Management
|
||||
|
||||
|
||||
Reference in New Issue
Block a user