From 022db18ef737f87ee36b666a2724a1b85bfa42af Mon Sep 17 00:00:00 2001 From: rcourtman Date: Sat, 22 Aug 2026 06:24:04 +0100 Subject: [PATCH] Let a per-resource severity override re-enable disabled offline alerts applyThresholdOverride only ever copied DisableConnectivity=true from an override, so once the global default disabled connectivity alerts no override could turn them back on. A guest row set to Warning or Critical saved its severity, showed as Custom, and never fired, while the frontend already encodes re-enablement by pairing an explicit severity with the row offline control (off stores DisableConnectivity=true). Treat an override that carries an explicit powered-off severity as that row offline control set to warning or critical and clear the inherited disable, matching the Docker container state path which already works this way. Refs #1738 Contract-Neutral: Refs #1738: behavioral bugfix in alerts override merge, no API or contract shape change --- internal/alerts/config_runtime.go | 6 ++ .../powered_off_severity_override_test.go | 59 +++++++++++++++++++ 2 files changed, 65 insertions(+) diff --git a/internal/alerts/config_runtime.go b/internal/alerts/config_runtime.go index 6e67d0d7a..bb02bc53f 100644 --- a/internal/alerts/config_runtime.go +++ b/internal/alerts/config_runtime.go @@ -676,6 +676,12 @@ func (m *Manager) applyThresholdOverride(base ThresholdConfig, override Threshol } if override.DisableConnectivity { result.DisableConnectivity = true + } else if override.PoweredOffSeverity != "" { + // An override carrying an explicit powered-off severity is that row's + // offline control set to warning/critical, so it re-enables + // connectivity alerts over a disabled default. A per-row "off" is + // stored as DisableConnectivity=true and takes the branch above. + result.DisableConnectivity = false } if override.PoweredOffSeverity != "" { result.PoweredOffSeverity = NormalizePoweredOffSeverity(override.PoweredOffSeverity) diff --git a/internal/alerts/powered_off_severity_override_test.go b/internal/alerts/powered_off_severity_override_test.go index 6d803028f..eeea5207a 100644 --- a/internal/alerts/powered_off_severity_override_test.go +++ b/internal/alerts/powered_off_severity_override_test.go @@ -42,3 +42,62 @@ func TestPoweredOffSeverityOverrideResolution(t *testing.T) { run(t, AlertLevelCritical, "", AlertLevelCritical) }) } + +// A per-guest severity override must re-enable powered-off alerts when the +// global default has connectivity alerts disabled, while a per-guest "off" +// (DisableConnectivity=true) must stay off regardless of any severity the +// override also carries. Refs #1738. +func TestPoweredOffOverrideReenablesDisabledDefault(t *testing.T) { + setup := func(t *testing.T, override ThresholdConfig) *Manager { + m := newTestManager(t) + + cfg := m.GetConfig() + cfg.GuestDefaults.DisableConnectivity = true + cfg.GuestDefaults.PoweredOffSeverity = AlertLevelCritical + if cfg.Overrides == nil { + cfg.Overrides = map[string]ThresholdConfig{} + } + cfg.Overrides["vm100"] = override + m.UpdateConfig(cfg) + + m.checkGuestPoweredOff("vm100", "TestVM", "pve-node1", "pve-instance", "VM", false) + m.checkGuestPoweredOff("vm100", "TestVM", "pve-node1", "pve-instance", "VM", false) + return m + } + + t.Run("severity override re-enables offline alerts", func(t *testing.T) { + m := setup(t, ThresholdConfig{PoweredOffSeverity: AlertLevelWarning}) + + m.mu.RLock() + alert := testRequireActiveAlert(t, m, "guest-powered-off-vm100") + m.mu.RUnlock() + + if alert.Level != AlertLevelWarning { + t.Fatalf("expected %s, got %s", AlertLevelWarning, alert.Level) + } + }) + + t.Run("per-guest off wins over its own stored severity", func(t *testing.T) { + m := setup(t, ThresholdConfig{DisableConnectivity: true, PoweredOffSeverity: AlertLevelWarning}) + + m.mu.RLock() + _, exists := m.activeAlerts["guest-powered-off-vm100"] + m.mu.RUnlock() + + if exists { + t.Fatal("expected no powered-off alert for per-guest disabled connectivity") + } + }) + + t.Run("override without severity keeps disabled default", func(t *testing.T) { + m := setup(t, ThresholdConfig{CPU: &HysteresisThreshold{Trigger: 90, Clear: 85}}) + + m.mu.RLock() + _, exists := m.activeAlerts["guest-powered-off-vm100"] + m.mu.RUnlock() + + if exists { + t.Fatal("expected no powered-off alert when override never touched the offline control") + } + }) +}