notification delivery gaps and locale corrections

This commit is contained in:
MacRimi
2026-09-04 11:38:44 +02:00
parent ab5767fe94
commit 4f38d0e2e7
12 changed files with 247 additions and 117 deletions
+24
View File
@@ -2586,6 +2586,11 @@ class PollingCollector:
# once for each genuinely new available version. The persistent
# history is stored in updates_available.json beside the scan.
self._last_post_install_check = 0
# Re-open that announcement once per service start, so a host
# carrying optimizations it never applied surfaces them again
# after a ProxMenux update or a node reboot instead of staying
# silent forever. Consumed by the first check cycle.
self._post_install_startup_reset_pending = True
# Sprint 14.7: fingerprint (item_id → latest_version) of the
# last managed-installs update notification, across all types
# in the registry. A new notification fires when the
@@ -3511,6 +3516,22 @@ class PollingCollector:
# ── Post-install function updates check (Sprint 12D) ────────────
def _reset_post_install_announcements(self):
"""Re-open the optimization announcement after a service start.
``post_install_update`` sits behind a second gate the other
update events don't have: a per-version history that keeps a
pending optimization from being announced twice. The manager's
``_EVENT_TYPES_RESET_ON_START`` already clears the delivery
cooldown, so only that history has to be dropped here for the
first cycle to report whatever is still pending.
"""
try:
import post_install_versions
post_install_versions.reset_notified_versions()
except Exception as e:
print(f"[PollingCollector] post-install history reset failed: {e}")
def _check_post_install_updates(self):
"""Notify the operator when post-install functions have new versions.
@@ -3522,6 +3543,9 @@ class PollingCollector:
shrinks the pending set and must not produce a second notification.
"""
now = time.time()
if self._post_install_startup_reset_pending:
self._post_install_startup_reset_pending = False
self._reset_post_install_announcements()
if now - self._last_post_install_check < self.UPDATE_CHECK_INTERVAL:
return
self._last_post_install_check = now
+28 -6
View File
@@ -1608,7 +1608,7 @@ class NotificationManager:
showed zero digest entries even when the schedule was firing
(issue #233).
"""
host = _hostname(self._config)
host = _resolve_display_hostname(self._config)
summary_title = (
f"{host}: 24h summary ({now.strftime('%Y-%m-%d %H:%M')})"
)
@@ -1847,23 +1847,44 @@ class NotificationManager:
if not rows:
return
host = _hostname(self._config)
host = _resolve_display_hostname(self._config)
summary_title = (
f"{host}: {len(rows)} events buffered during Quiet Hours"
)
summary_body = self._compose_digest_body(rows)
result: dict = {'success': False, 'error': ''}
try:
channel.send(summary_title, summary_body, severity='INFO',
data={'_quiet_hours_summary': True, '_count': len(rows)})
result = channel.send(
summary_title, summary_body, severity='INFO',
data={'_quiet_hours_summary': True, '_count': len(rows)},
) or result
except Exception as e:
print(f"[NotificationManager] quiet send failed for "
f"{ch_name}: {e}")
return
result = {'success': False, 'error': str(e)}
if result.get('success'):
self._stats['total_sent'] += 1
self._stats['last_sent_at'] = datetime.now().isoformat()
else:
self._stats['total_errors'] += 1
# Mirrors the digest path: the release is a real delivery, so it
# belongs in the history and the counters the operator reads.
self._record_history(
'quiet_hours', ch_name, summary_title, summary_body, 'INFO',
result.get('success', False), result.get('error', '') or '',
'quiet_scheduler',
)
# Only drop the rows after a successful send so a transient
# transport failure (Telegram timeout, SMTP outage) doesn't
# lose the user's overnight context.
# lose the user's overnight context. A channel reporting failure
# without raising counts as a failure here too — otherwise the
# buffer is wiped for a summary the operator never received.
if not result.get('success'):
return
try:
ids = [r[0] for r in rows]
conn = sqlite3.connect(str(DB_PATH), timeout=10)
@@ -2082,6 +2103,7 @@ class NotificationManager:
'secure_gateway_update_available',
'app_update_available',
'docker_stack_update_available',
'post_install_update',
# Security events that must not be silenced by stale cooldowns
# following a Monitor reinstall (Pedro Rico, 19/05).
'auth_fail',
+23
View File
@@ -434,6 +434,29 @@ def scan(persist: bool = True) -> dict[str, Any]:
return snapshot
def reset_notified_versions() -> None:
"""Forget which optimization versions have already been announced.
Each version is announced once, so a host that never applies a
pending optimization would otherwise stay silent about it forever.
Clearing the history reopens that single announcement.
The caller owns the timing: the notification collector runs this on
its first cycle after a service start, together with clearing the
matching delivery cooldown, because forgetting the history while the
cooldown still suppresses delivery would consume the announcement
without ever sending it.
"""
try:
with _cache_lock:
scanned_at = float(_cache.get("scanned_at", 0.0) or 0.0)
updates = list(_cache.get("updates", []))
_write_persisted_snapshot(scanned_at, updates, {})
except OSError as e:
# Read-only host: de-duplication stays best-effort, as elsewhere.
print(f"[post_install_versions] could not reset notified versions: {e}")
def scan_at_startup() -> dict[str, Any]:
"""Convenience wrapper called from flask_server startup.