mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-09-14 18:56:52 +00:00
notification delivery gaps and locale corrections
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user