mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-05-20 07:45:01 +00:00
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:
Binary file not shown.
@@ -1 +1 @@
|
|||||||
effc478f957e89f272f9e1bb92ae2eddc4a131eea0b4549eb78477164dd982e9 ProxMenux-1.2.1.1-beta.AppImage
|
0f5802ee95889df4ba011f6ae4a1897f08c1bad28d069db8b95714373c4b9426 ProxMenux-1.2.1.1-beta.AppImage
|
||||||
|
|||||||
@@ -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).
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user