From 63e72abb27f5d0e51584ac57fab527d6b8ffab93 Mon Sep 17 00:00:00 2001 From: VAIO73 <50487331+Vaso73@users.noreply.github.com> Date: Thu, 17 Sep 2026 14:26:18 +0200 Subject: [PATCH] fix: deduplicate identical digest events --- AppImage/scripts/notification_manager.py | 23 +++++++++- .../tests/test_notification_runtime_i18n.py | 46 +++++++++++++++++++ 2 files changed, 68 insertions(+), 1 deletion(-) diff --git a/AppImage/scripts/notification_manager.py b/AppImage/scripts/notification_manager.py index 11c3d55c..765942ce 100644 --- a/AppImage/scripts/notification_manager.py +++ b/AppImage/scripts/notification_manager.py @@ -1602,6 +1602,10 @@ class NotificationManager: 'vm_fail', 'ct_fail', 'system_shutdown', 'system_reboot', }) + # A task completion can be observed twice through adjacent collectors. + # Keep the daily digest useful by coalescing only byte-for-byte identical + # buffered INFO entries that arrive close together. + _DIGEST_DUPLICATE_WINDOW = 300 # seconds def _should_buffer_for_digest(self, ch_name: str, severity: str, event_type: str) -> bool: @@ -1632,12 +1636,29 @@ class NotificationManager: conn = sqlite3.connect(str(DB_PATH), timeout=10) conn.execute('PRAGMA journal_mode=WAL') conn.execute('PRAGMA busy_timeout=5000') + # Adjacent collectors can observe the same completed task. Only + # coalesce byte-for-byte identical rows, so a different version, + # source or result stays visible in the same digest. + now = int(time.time()) + duplicate = conn.execute( + 'SELECT 1 FROM digest_pending ' + 'WHERE channel = ? AND event_type = ? AND event_group = ? ' + 'AND severity = ? AND title = ? AND body = ? AND ts >= ? ' + 'LIMIT 1', + ( + ch_name, event_type, event_group, severity, title, body, + now - self._DIGEST_DUPLICATE_WINDOW, + ), + ).fetchone() + if duplicate: + conn.close() + return conn.execute( 'INSERT INTO digest_pending ' '(channel, event_type, event_group, severity, ts, title, body) ' 'VALUES (?, ?, ?, ?, ?, ?, ?)', (ch_name, event_type, event_group, severity, - int(time.time()), title, body), + now, title, body), ) conn.commit() conn.close() diff --git a/AppImage/scripts/tests/test_notification_runtime_i18n.py b/AppImage/scripts/tests/test_notification_runtime_i18n.py index dde2f917..c792d85d 100644 --- a/AppImage/scripts/tests/test_notification_runtime_i18n.py +++ b/AppImage/scripts/tests/test_notification_runtime_i18n.py @@ -707,6 +707,52 @@ class RuntimeCatalogTests(unittest.TestCase): self.assertNotIn("System", channel.payload[1]) self.assertTrue(channel.payload[3]["_quiet_hours_summary"]) + def test_digest_buffer_coalesces_only_identical_recent_events(self): + manager = notification_manager.NotificationManager() + with tempfile.TemporaryDirectory() as directory: + db_path = Path(directory) / "settings.db" + conn = sqlite3.connect(db_path) + conn.execute( + "CREATE TABLE digest_pending (" + "id INTEGER PRIMARY KEY, channel TEXT, event_type TEXT, " + "event_group TEXT, severity TEXT, ts INTEGER, title TEXT, body TEXT)" + ) + conn.commit() + conn.close() + + with mock.patch.object(notification_manager, "DB_PATH", db_path), \ + mock.patch.object(notification_manager.time, "time", side_effect=(1000, 1060, 1070, 1301)): + manager._buffer_digest_event( + "telegram", "lxc_update_applied", "vm_ct", "INFO", + "pve01: LXC wireguard (101) update completed", "Source: Manual", + ) + # Same completed task from an adjacent collector: do not list it twice. + manager._buffer_digest_event( + "telegram", "lxc_update_applied", "vm_ct", "INFO", + "pve01: LXC wireguard (101) update completed", "Source: Manual", + ) + # Different result/body for the same LXC remains visible. + manager._buffer_digest_event( + "telegram", "lxc_update_applied", "vm_ct", "INFO", + "pve01: LXC wireguard (101) update completed", "Source: Scheduled", + ) + # The same result is allowed again outside the short window. + manager._buffer_digest_event( + "telegram", "lxc_update_applied", "vm_ct", "INFO", + "pve01: LXC wireguard (101) update completed", "Source: Manual", + ) + + conn = sqlite3.connect(db_path) + rows = conn.execute( + "SELECT ts, body FROM digest_pending ORDER BY id" + ).fetchall() + conn.close() + + self.assertEqual( + rows, + [(1000, "Source: Manual"), (1070, "Source: Scheduled"), (1301, "Source: Manual")], + ) + def test_visible_templates_use_known_backend_and_frontend_groups(self): visible_groups = { template.get("group", "other")