Commit Graph

18 Commits

Author SHA1 Message Date
rcourtman 4f14ad6ab9 Use one primary CPU temperature rule everywhere
Three rules chose a node's single CPU temperature. Node history took
CPUMax first, agent history took cpu_package first, and the node drawer
recomputed the maximum of package, CPUMax and every core in the
browser. On an agent-fed node CPUMax is built from cores alone and can
sit below the package, and elsewhere a core can run above the package,
so the drawer's CPU temperature headline could disagree with the line
drawn beneath it and with the History tab. Node alerts and the CPU
low/record tracking used yet another package-first rule.

models.PrimaryCPUTemperatureCelsius is now the one rule: the hottest
usable package, CPU-maximum or core reading, with a sensor-map form for
agents. Node and agent history, the router's live fallbacks, the
unified resource temperature, node temperature alerts and the CPU
low/record tracking all call it. The drawer reads the resource value
through a new Node.cpuTemperature field instead of recomputing it, and
getCpuTemperature is removed.

Mock mode wrote a second package reading under a cpu.package key into
every agent host, including node-linked ones, from a series unrelated
to the node's static reading. A node and its linked agent now carry one
reading, sampled from the agent's series under the agent's real keys,
and both histories seed from that series, so the current reading
continues the line.
2026-10-13 20:15:36 +01:00
pulse-triage[bot] 56a5b2dba7 Keep node alerts covered through reported agent handovers
Adapt remaining PR2599/2586 behavior to current source: evidence-bound ownership, report-ordered link outcomes, immediate configuration revocation, direct/inherited override precedence and canonical/shadow pending/intent reset. Close node-side alerts as transfer to a real per-metric owner rather than observed recovery. Preserve volatile metric status, child identity and observation-gap interruption.

Actual entry-point controls cover overlap, unknown values, saved direct filesystem disable, missing thresholds and real monitor lifecycle/History transfers. Preserve per-type default normalization: this is not a new Off setting. PR2567 is already merged and held SMART collection/native work is not revived.

Source-Provenance: additive adaptation of PR2599 at 765ac23046 over bd3432ab22
Change-source: pulse-maintainer
2026-10-07 05:57:02 +01:00
pulse-triage[bot] eff676c746 Integrate reviewed metric observation continuity repair
Preserve Core commits 2d2ae4c8a1 and 796da48cf9 exactly. Resolve node dispatch overlap by retaining main's per-metric linked-agent ownership and Core's exclusion for separately evaluated temperature; unavailable observations interrupt timing without fabricating recovery.

Change-source: pulse-maintainer
2026-10-06 18:58:10 +01:00
pulse-triage[bot] 796da48cf9 Keep typed node temperature outside usage gap detection
The broader alert race suite identified that usage-only dispatch treated the separately evaluated node temperature as missing. Declare that separate owner, keep its existing continuous healthy and missing-read semantics, and pin the real CheckNode path.

Contract-Neutral: Restore the existing typed node temperature timing and ownership boundary; the private dispatch scope and contract clarification introduce no public API, canonical schema or intended policy delta.
Change-source: pulse-maintainer
2026-10-06 18:17:23 +01:00
pulse-triage[bot] 2d2ae4c8a1 Restart metric timing after unavailable observations
Drop pending activation and reset recovery timing across retained guest readings, rejected unified values and unverified rolling windows. Keep firing alerts and independent current metrics untouched, and clear interrupted intent checkpoints so a restart cannot accrue unknown time.

Change-source: pulse-maintainer
2026-10-06 18:07:51 +01:00
rcourtman 4775fcbf38 Base node usage ownership on agent config, all links, summary disk
A review of the previous commit found three ways the node and its
linked agent could still disagree about who alerts.

Memory ownership required a known reading on every report. When an
agent's memory reading went missing while its alert was open, the agent
kept that alert (a missing reading is not a recovery) but stopped
owning memory, so the node raised a second alert. It resolved again as
soon as the reading returned, with history and notifications for each
flip. Ownership now follows the agent's configured thresholds, not one
report's data.

With several agents linked to one node, only the lowest agent ID's
coverage counted. If that agent had CPU off and another linked agent
evaluated CPU, the node alerted on CPU as well. A metric is now owned
when any linked agent evaluates it.

Disk ownership needed any evaluated filesystem, but a linked node's
disk metric is the agent's summary filesystem (root when reported, see
models.SummaryDisk). With root disabled by a per-disk override and
/data still alerting, the node gave up its root disk alert and a full
root filesystem raised nothing. The agent now owns the node's disk
metric only while it evaluates that same filesystem.
2026-10-06 15:45:25 +01:00
rcourtman 9206486d56 Hand a node only the usage metrics its agent evaluates
CheckHost registered a host agent's node link before its disabled
returns. With agent alerts switched off, or a host override disabling
the agent, the node still released its CPU, memory and disk alerts to an
agent that evaluated nothing, so nothing alerted on that machine's
usage. The same happened per metric: an agent CPU threshold set to off
with the node CPU threshold on left the machine with no CPU alert.

The link now records which usage metrics the agent actually evaluated
on its last report: a live CPU threshold, a live memory threshold with
a known reading, and at least one filesystem with a live disk
threshold. CheckHost registers it after evaluation and removes it on
the disabled returns. CheckNode releases only the metrics the agent
owns and evaluates the rest itself, so deduplication can only remove a
duplicate, never the last alert for a machine.

The link also carries the agent's display name, which the handover
notification work builds on.
2026-10-06 15:07:08 +01:00
rcourtman 7ee8c0d1e2 Dedup node alerts by agent link and keep node thresholds on save
A review of the node temperature fix found three more ways node
alerts went wrong.

Node deduplication matched a host agent's hostname against node names
across every PVE instance. With two instances that each have a node
called pve and an agent on only one of them, both nodes released their
CPU, memory and disk alerts, so the second machine went unmonitored. An
agent reporting an FQDN never matched its node, so that machine alerted
twice, and an operator unlinking an agent did not hand the alerts back.
Dedup now keys on the agent's LinkedNodeID, which the monitor already
sets automatically or from an operator link. On pulse-prod the delly2
agent is linked to homelab-delly2, the node alert ID it should dedup.

Saving the alert config resolved node temperature alerts that were
still over their trigger. Reevaluation only treated an alert as a node
alert when its Instance equalled its node name, but current node alerts
keep the PVE instance name there, so they fell through to guest
thresholds, which have no temperature threshold. Metadata resourceType
"node" now decides, and the Instance heuristic only covers legacy alerts.

A missing temperature reading no longer resolves an open alert, but a
pending run still spanned the gap: one hot sample after ten minutes
without readings fired at once, and a recovery run could complete the
same way. The reducer gains InterruptMetricRun, which drops a pending
run or restarts a recovery run without resolving anything, and the node
temperature check calls it when the reading is missing.
2026-10-06 14:34:09 +01:00
rcourtman 33bcf698e0 Keep node temperature alerts when a host agent runs on the node
Installing a Pulse agent on a Proxmox node silently ended CPU
temperature alerting for it. CheckNode skipped every node metric,
temperature included, once a host agent with the node's hostname
registered, but CheckHost evaluates only CPU, memory and disk usage and
SMART disk temperatures, never CPU temperature. On pulse-prod the last
node temperature alert fired on 5 October; delly2 has since read 90°C
and minipc 78-87°C with no alert.

Temperature now stays with the node whether or not an agent is
registered. The node poll already merges the agent's sensors into
node.Temperature, so this gives one temperature alert per machine.

The dedup skip path also never cleared node alerts it stopped
evaluating, so a node alert open when the agent registered stayed
frozen until the agent went offline. pulse-prod shows this today: the
delly2 node memory alert, open since 29 September, beside the agent's
own memory alert. CheckNode now releases node CPU, memory and disk
alerts through the disabled-threshold path, which drops the pending run
and resolves the open alert, leaving the agent as the only owner.

A missing CPU reading also fed 0°C into the evaluator, which could
resolve an open temperature alert without any evidence that the node had
cooled. With a live trigger the check now skips the evaluation instead,
while a disabled threshold still clears.
2026-10-06 13:43:47 +01:00
Pulse Test 6db3cc8616 fix(alerts): correlate linked outage signals 2026-08-29 18:19:00 +01:00
rcourtman 8827defb63 feat(alerts): resolve alert policy through one declarative fold
Phase 3 of docs/ALERT_ENGINE_EVOLUTION.md: the effective alert policy for
a resource — type default thresholds, the type's DisableAll switches,
custom rules, the per-resource override — is now answered by one ordered
fold (effectiveAlertPolicyNoLock in alert_policy.go), translated from the
persisted AlertConfig. The config keeps its shape; the engine stops
reading it piecemeal.

Before this, every check path read its own DisableAll* boolean and picked
its own override lookup, and the scattered reads drifted (#1738 was an
override lookup that existed on some paths and not others;
connection.go hand-rolled its own type-to-switch mapping). Now:

- resolveResourceThresholds and getGuestThresholds delegate to the fold.
- All 40+ DisableAll* reads across the check paths, the config-change
  reconciliation, and the connection detector go through
  alertPolicyTypeSwitchesNoLock — the single place those booleans are
  read on behalf of evaluation.

Characterization first: alert_policy_test.go pins the fold against the
legacy resolution paths (per-type defaults, overrides, storage aliases,
guest custom rules by priority, every type's switch pair) before any
call site moved.
2026-08-27 01:10:26 +01:00
rcourtman 6eee5a1641 chore(alerts): delete the legacy transition-state maps
The reducer core became the authoritative transition state for every
per-observation family in the Phase 2 cutovers; the manager's tracking
maps (offlineConfirmations, offlineRecoveryConfirmations,
nodeOfflineCount, connectionDegradedCount, dockerOfflineCount,
dockerStateConfirm, pendingAlerts) had been reduced to write-only
mirrors. This deletes them, per the plan's retirement list
(docs/ALERT_ENGINE_EVOLUTION.md).

Hygiene the maps' cleanup loops used to provide moves into the core:

- reducer.PruneStalePending reaps pending runs whose resource stopped
  being observed (Cleanup at 10 minutes, cleanupStaleMaps at the stale
  threshold) — previously the pendingAlerts age sweep.
- Docker container cleanup drops core pending runs for containers no
  longer in the seen set (reducer.PendingResourceIDs +
  DropPendingForResource) — previously the dockerStateConfirm loops.
- HandleHostOnline / HandleDockerHostOnline apply a healthy
  observation to the core so an in-flight offline confirmation run
  ends — previously a map delete.

Two real gaps surfaced by the test conversion, fixed at the root:

- Config-change auto-resolution removed alerts without mirroring the
  forget into the core, leaving the incident firing after a policy
  disabled it.
- Intent pending state created by evaluateIntentNoLock carried no
  ResourceID/ResourceType/TrackingKey, so per-resource clears
  (guest suppression) could not match it.

Guest node-move migration no longer re-keys pending runs: a move
restarts an in-flight pending run (firing continuity still comes from
alert adoption). Deliberate simplification, noted in the helper.

Tests convert their map seeds and asserts to the core seams
(testCoreConfirmations / testCoreRecoveryCount / testCoreHasIncident /
testCoreIsPending, direct ApplyDiscrete seeding); assertions that only
tested the deleted maps' bookkeeping are removed.
2026-08-27 01:00:12 +01:00
rcourtman 6a04dcb84f feat(alerts): cut the canonical lifecycle family over to the reducer core
Phase 2 of docs/ALERT_ENGINE_EVOLUTION.md, first and largest cutover:
the deterministic reducer is now the AUTHORITATIVE transition state for
the entire match-spec family (connectivity, powered-state, discrete
state, provider incidents, health assessments, service gaps) and the
poll-driven recovery paths.

evaluateCanonicalLifecycleAlert no longer reconstructs previous state
from the confirmation-count maps and re-evaluates: it derives the
observation via the exported spec Match predicate, applies it to the
manager-owned reducer core (with the resolved intent context and the
monotonic intent tick), and translates the core's incidents and events
into the existing side effects — alert objects, dispatch, history,
recently-resolved, callbacks — synthesizing the evaluator-shaped result
its callers consume. The recovery paths (PBS/PMG/storage, node,
connection-degraded) feed healthy observations to the same core, whose
recovery gate replaces confirmOfflineRecovery. Guest powered-off clears
and suppressions clear the core run. User acks, unacks, and manual
clears mirror into the core; restore seeds it; an existing alert the
core does not know about is adopted as firing, exactly as the old
engine treated any active alert as previously firing.

Wall-clock immunity for intent grace is preserved deterministically:
the monotonic runtime tick is now an explicit reducer signal input
(with a validity flag — zero is a legitimate process-start reading), so
suspend and NTP jumps neither fire nor starve gated activations.

The legacy count maps are maintained as read-only mirrors of the core
during the transition and are no longer consulted by any engine logic;
tests that pre-seeded them now drive real observations, and tests that
pinned the reconstruct-from-maps workarounds are replaced by pins of
the core-owned behavior. lifecyclePreviousState and lifecycleFirstMatched
are deleted — the defect class they patched cannot exist when the run
state is owned rather than reconstructed.

All five parity harnesses, the shadow feed, and the full
characterization suite pass against the cutover engine.
2026-08-27 00:15:58 +01:00
rcourtman bfc1a81867 feat(alerts): shadow-mode reducer feed with always-on divergence telemetry
Phase 1 capstone of docs/ALERT_ENGINE_EVOLUTION.md. The deterministic
reducer now runs continuously inside the live manager against the same
production observations: the canonical lifecycle path (connectivity,
powered-state, discrete-state kinds) via a deferred hook that replays
each evaluation — including the resolved intent context — through the
reducer; the poll-driven recovery paths (PBS/PMG/storage, node,
connection-degraded); and manual acknowledge/unacknowledge/clear. The
feed seeds from active canonical alerts at enable so restarts do not
read as mass divergence.

Every state disagreement is counted (Manager.ShadowDivergences) and
recorded in the alert event log as a shadow_divergence event with both
engines' states, rate-limited to one report per key per ten minutes.
After each divergence the reducer resyncs to the manager, so one
divergence yields one event — including divergences caused by manager
mutations the feed does not observe. Appends never block evaluation and
a disabled feed is a nil-check no-op.

This converts the parity harnesses' test-time guarantee into an
always-on invariant; the production divergence rate becomes the
go/no-go evidence for each Phase 2 family cutover. The full
activation/ack/recovery/re-fire cycle runs divergence-free in tests.
2026-08-26 23:49:12 +01:00
rcourtman afc419ddab Add durable Proxmox node display names 2026-07-24 15:46:56 +01:00
rcourtman bf67ba9201 Fix cache-aware Linux memory fallbacks 2026-07-24 00:19:40 +01:00
rcourtman cf0486492e Build operational trust lifecycle foundation 2026-07-19 15:10:38 +01:00
rcourtman 3d8cb6c8a5 refactor: split node alert checker
Move Proxmox node metric, temperature, offline lifecycle, host-agent deduplication, and node display-name cache support into internal/alerts/node.go.

Keep the Manager API unchanged while recording the node checker owner in the alerts subsystem contract and adding a focused display-name cache key characterization.

Proof: go test ./internal/alerts/...
2026-05-06 13:47:54 +01:00