From 0d4a50cd357451504f2ab478a8b256d1c5a571bf Mon Sep 17 00:00:00 2001 From: PTah Date: Wed, 3 Jun 2026 09:15:21 +1000 Subject: [PATCH] fix: dedupe daily report Telegram in dual mode and show host in summary fallback Co-authored-by: Cursor --- backend/app/services/notify_dispatch.py | 7 ++- backend/app/services/telegram_templates.py | 10 +++- backend/tests/test_notify_dispatch.py | 58 ++++++++++++++++++++++ backend/tests/test_telegram_templates.py | 21 ++++++++ 4 files changed, 93 insertions(+), 3 deletions(-) diff --git a/backend/app/services/notify_dispatch.py b/backend/app/services/notify_dispatch.py index 6cc1fd5..b58eca9 100644 --- a/backend/app/services/notify_dispatch.py +++ b/backend/app/services/notify_dispatch.py @@ -75,11 +75,14 @@ def _dispatch_lifecycle_channels(event: Event, *, db: Session | None, policy) -> def notify_daily_report(event: Event, *, db: Session | None = None) -> None: - """Оповещение по суточному отчёту (severity=info, вне порога policy).""" + """Оповещение по суточному отчёту (severity=info, вне порога policy). + + При UseSAC=dual агент шлёт TG сам (telegram_via=agent) — SAC не дублирует. + """ policy = get_effective_notification_policy(db) if not should_notify_event(event, db): return - _dispatch_event_channels(event, db=db, policy=policy) + _dispatch_lifecycle_channels(event, db=db, policy=policy) def notify_lifecycle(event: Event, *, db: Session | None = None) -> None: diff --git a/backend/app/services/telegram_templates.py b/backend/app/services/telegram_templates.py index a776064..4898a53 100644 --- a/backend/app/services/telegram_templates.py +++ b/backend/app/services/telegram_templates.py @@ -405,7 +405,15 @@ def _format_event_body_html(event: Event) -> str: return re.sub(r"\n{3,}", "\n\n", text) body = _detail(details, "report_body", default=event.summary) if body != "-": - return html.escape(body) + header = ( + "📊 ЕЖЕДНЕВНЫЙ ОТЧЕТ МОНИТОРИНГА WINDOWS" + if event.type == "report.daily.rdp" + else "📊 ЕЖЕДНЕВНЫЙ ОТЧЕТ SSH МОНИТОРИНГА" + ) + msg = f"{html.escape(header)}\n" + msg += _line("🏢", "Сервер", host_label(event.host)) + msg += f"\n{html.escape(body)}" + return msg return format_generic_event_html(event) if event.type in ("rdp.login.success", "rdp.login.failed"): return format_rdp_login_html(event) diff --git a/backend/tests/test_notify_dispatch.py b/backend/tests/test_notify_dispatch.py index d250ef2..03c6b19 100644 --- a/backend/tests/test_notify_dispatch.py +++ b/backend/tests/test_notify_dispatch.py @@ -226,3 +226,61 @@ def test_notify_lifecycle_skips_telegram_when_via_agent(): notify_dispatch.notify_lifecycle(event) mock_tg.notify_event.assert_not_called() mock_wh.notify_event.assert_called_once() + + +def test_notify_daily_report_skips_telegram_when_via_agent(): + event = Event( + event_id="00000000-0000-4000-8000-000000000604", + host_id=1, + occurred_at=datetime(2026, 6, 3, 9, 0, tzinfo=timezone.utc), + category="report", + type="report.daily.rdp", + severity="info", + title="Ежедневный отчёт Windows", + summary="RDP 24ч: успех 0, неудач 0, банов 0", + details={"telegram_via": "agent", "report_body": "📊 body"}, + payload={}, + ) + policy = NotificationPolicyConfig( + min_severity="warning", + use_telegram=True, + use_webhook=True, + use_email=False, + source="db", + ) + with patch.object(notify_dispatch, "get_effective_notification_policy", return_value=policy): + with patch.object(notify_dispatch, "should_notify_event", return_value=True): + with patch.object(notify_dispatch, "telegram_notify") as mock_tg: + with patch.object(notify_dispatch, "webhook_notify") as mock_wh: + notify_dispatch.notify_daily_report(event) + mock_tg.notify_event.assert_not_called() + mock_wh.notify_event.assert_called_once() + + +def test_notify_daily_report_skips_telegram_when_via_agent(): + event = Event( + event_id="00000000-0000-4000-8000-000000000604", + host_id=1, + occurred_at=datetime(2026, 6, 3, 9, 0, tzinfo=timezone.utc), + category="report", + type="report.daily.rdp", + severity="info", + title="Ежедневный отчёт Windows", + summary="RDP 24ч: успех 0, неудач 0, банов 0", + details={"telegram_via": "agent", "report_body": "📊 body"}, + payload={}, + ) + policy = NotificationPolicyConfig( + min_severity="warning", + use_telegram=True, + use_webhook=True, + use_email=False, + source="db", + ) + with patch.object(notify_dispatch, "get_effective_notification_policy", return_value=policy): + with patch.object(notify_dispatch, "should_notify_event", return_value=True): + with patch.object(notify_dispatch, "telegram_notify") as mock_tg: + with patch.object(notify_dispatch, "webhook_notify") as mock_wh: + notify_dispatch.notify_daily_report(event) + mock_tg.notify_event.assert_not_called() + mock_wh.notify_event.assert_called_once() diff --git a/backend/tests/test_telegram_templates.py b/backend/tests/test_telegram_templates.py index 10b9873..f160cee 100644 --- a/backend/tests/test_telegram_templates.py +++ b/backend/tests/test_telegram_templates.py @@ -210,6 +210,27 @@ def test_lifecycle_footer_uses_telegram_via_agent_when_present(): assert "📡 Оповещение: агент (rdp-login-monitor 1.2.30-SAC)" in text +def test_daily_report_summary_fallback_includes_host(): + host = Host(hostname="srv1", display_name="K6A-DC5", ipv4="192.168.160.91", os_family="linux") + event = Event( + event_id="00000000-0000-4000-8000-000000000508", + host_id=1, + host=host, + occurred_at=datetime(2026, 6, 3, 9, 0, tzinfo=timezone.utc), + category="report", + type="report.daily.ssh", + severity="info", + title="Ежедневный отчёт SSH", + summary="SSH 24ч: успех 0, неудач 0, банов 0", + details={"generated_by": "sac"}, + payload={}, + ) + text = format_event_telegram_html(event) + assert "K6A-DC5" in text + assert "SSH 24ч: успех 0" in text + assert "ЕЖЕДНЕВНЫЙ ОТЧЕТ SSH" in text + + def test_rdp_shadow_control_template(): host = Host(hostname="RDS01", display_name="RDS Farm", ipv4="10.0.0.5", os_family="windows") event = Event(