Reset update-type cooldowns on NotificationManager.start()

When the user reinstalls or restarts the Monitor (deploy of a new
beta AppImage), they expect to see a fresh "what's available now"
summary in Telegram/Gotify/etc. instead of silence — even if the
24h anti-spam cooldown for `update_summary` etc. hasn't expired yet.

Without this, the operator had to wait up to 24h after every
deploy before the next `update_summary`, `proxmenux_update`,
`post_install_update`, `pve_update`, `update_available`,
`nvidia_driver_update_available` or `secure_gateway_update_available`
notification fired. The 24h cooldown is the right default for steady
state (don't pester the user every poll cycle with the same "177
packages pending" reminder), but a service restart is an explicit
signal that the user wants a fresh status report.

- New _UPDATE_EVENT_TYPES_RESET_ON_START tuple lists the event types
  to clear (everything in the "*_update*" + "update_*" family).
- New _reset_update_cooldowns_on_start() runs at start() right after
  the running flag flips, before watchers/dispatcher come up.
- Patterns match both fingerprint shapes:
    "<host>:<entity>:<event_type>:"               trailing-colon form
    "<host>:<entity>:<event_type>"                no-suffix form (managed installs)
- In-memory `_cooldowns` cache is also pruned so the live dispatcher
  picks up the reset immediately, without waiting for the next
  `_load_cooldowns_from_db()` cycle.

Non-update cooldowns (auth_fail, log_critical_*, disk errors, …) are
preserved so a restart doesn't unleash a backlog of stale alerts.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
MacRimi
2026-05-19 01:03:44 +02:00
parent 06e6ae417e
commit 4e26c5942f
3 changed files with 68 additions and 2 deletions
Binary file not shown.
+1 -1
View File
@@ -1 +1 @@
effc478f957e89f272f9e1bb92ae2eddc4a131eea0b4549eb78477164dd982e9 ProxMenux-1.2.1.1-beta.AppImage 0f5802ee95889df4ba011f6ae4a1897f08c1bad28d069db8b95714373c4b9426 ProxMenux-1.2.1.1-beta.AppImage
+67 -1
View File
@@ -911,7 +911,18 @@ class NotificationManager:
self._running = True self._running = True
self._stats['started_at'] = datetime.now().isoformat() self._stats['started_at'] = datetime.now().isoformat()
# Reset cooldowns for update-summary event types so the operator
# gets a fresh "what's available now" report after every Monitor
# deploy/restart. The 24h anti-spam cooldown serves the
# steady-state use case (don't pester the user with the same
# "177 packages pending" reminder every poll cycle); the
# explicit service restart is the signal that "I want to see
# the current state, not yesterday's silence". Non-update
# cooldowns (auth_fail, log_critical, disk errors, …) are kept
# so a restart doesn't unleash an inbox flood for the user.
self._reset_update_cooldowns_on_start()
# Ensure PVE webhook is configured (repairs priv config if missing) # Ensure PVE webhook is configured (repairs priv config if missing)
try: try:
from flask_notification_routes import setup_pve_webhook_core from flask_notification_routes import setup_pve_webhook_core
@@ -1616,6 +1627,61 @@ class NotificationManager:
self._cooldowns[fingerprint] = now self._cooldowns[fingerprint] = now
self._persist_cooldown(fingerprint, now) self._persist_cooldown(fingerprint, now)
# Event types whose cooldown should be cleared at every service start,
# so the user sees a fresh "what's available right now" report after
# any deploy. Anything not in this list keeps its 24h cooldown across
# restarts (auth_fail, log_critical_*, disk errors, …) — preserving
# the anti-flood guarantee for high-volume sources.
_UPDATE_EVENT_TYPES_RESET_ON_START = (
'update_summary',
'proxmenux_update',
'post_install_update',
'pve_update',
'update_available',
'nvidia_driver_update_available',
'secure_gateway_update_available',
)
def _reset_update_cooldowns_on_start(self):
"""Clear DB rows in notification_last_sent for update-type events.
Fingerprint format used by `_passes_cooldown` is
`<host>:<entity>:<event_type>[:<entity_id>]`. We match by the
event_type segment with LIKE patterns covering both the
trailing-colon case (`…:update_summary:`) and the no-suffix case
(`…:nvidia_driver_update_available`) for managed-install events.
Also clear the in-memory cache so the running dispatcher
immediately sees the reset, without waiting for the next
`_load_cooldowns_from_db()`.
"""
try:
if not DB_PATH.exists():
return
patterns = []
for et in self._UPDATE_EVENT_TYPES_RESET_ON_START:
patterns.append(f'%:{et}:%') # entity_id non-empty form
patterns.append(f'%:{et}') # entity_id empty / managed-install form
where = ' OR '.join('fingerprint LIKE ?' for _ in patterns)
conn = sqlite3.connect(str(DB_PATH), timeout=10)
conn.execute('PRAGMA journal_mode=WAL')
cursor = conn.cursor()
cursor.execute(f'DELETE FROM notification_last_sent WHERE {where}', patterns)
deleted = cursor.rowcount
conn.commit()
conn.close()
# Mirror the DB delete in the in-memory cache so the
# dispatch thread doesn't keep ghost cooldowns until the
# next reload.
for fp in list(self._cooldowns.keys()):
for et in self._UPDATE_EVENT_TYPES_RESET_ON_START:
if f':{et}:' in fp or fp.endswith(f':{et}'):
self._cooldowns.pop(fp, None)
break
if deleted > 0:
print(f"[NotificationManager] Reset {deleted} update-type cooldowns on startup")
except Exception as e:
print(f"[NotificationManager] Failed to reset update cooldowns on start: {e}")
def _load_cooldowns_from_db(self): def _load_cooldowns_from_db(self):
"""Load persistent cooldown state from SQLite (up to 48h). """Load persistent cooldown state from SQLite (up to 48h).