diff --git a/backend/app/services/mobile_notify.py b/backend/app/services/mobile_notify.py index f5bb27d..5e238bd 100644 --- a/backend/app/services/mobile_notify.py +++ b/backend/app/services/mobile_notify.py @@ -16,6 +16,7 @@ from app.models import Event, Problem from app.models.mobile_device import MobileDevice from app.services.notification_severity import severity_meets_minimum from app.services.notification_policy import get_effective_notification_policy +from app.services.telegram_templates import format_event_mobile_push logger = logging.getLogger(__name__) @@ -170,8 +171,7 @@ def _send_fcm_data( def _event_payload(event: Event) -> tuple[str, str, dict[str, str]]: - title = f"[{event.severity}] {event.title}" - body = (event.summary or "")[:200] + title, body = format_event_mobile_push(event) data = { "kind": "event", "id": str(event.id), diff --git a/backend/app/services/telegram_templates.py b/backend/app/services/telegram_templates.py index 0537453..0391cfa 100644 --- a/backend/app/services/telegram_templates.py +++ b/backend/app/services/telegram_templates.py @@ -506,6 +506,43 @@ def format_event_telegram_html(event: Event) -> str: return _append_event_source(_format_event_body_html(event), event) +_TELEGRAM_INLINE_TAGS = re.compile(r"]*)?>", re.I) +_TELEGRAM_ANCHOR_OPEN = re.compile(r"]*>", re.I) +_TELEGRAM_ANCHOR_CLOSE = re.compile(r"", re.I) + +_MOBILE_PUSH_BODY_MAX = 3500 + + +def telegram_html_to_plaintext(text: str) -> str: + """Plain text for Seaca push from the same HTML templates as Telegram.""" + out = sanitize_telegram_html(text) + out = _TELEGRAM_INLINE_TAGS.sub("", out) + out = _TELEGRAM_ANCHOR_OPEN.sub("", out) + out = _TELEGRAM_ANCHOR_CLOSE.sub("", out) + out = html.unescape(out) + out = re.sub(r"\n{3,}", "\n\n", out) + return out.strip() + + +def format_event_mobile_push(event: Event) -> tuple[str, str]: + """Title + body for FCM data payload (mirrors Telegram wording).""" + plain = telegram_html_to_plaintext(format_event_telegram_html(event)) + lines = [line.strip() for line in plain.split("\n") if line.strip()] + if lines: + title = lines[0] + body = "\n".join(lines[1:]).strip() + if not body: + body = (event.summary or "").strip() + else: + title = f"[{event.severity}] {event.title}" + body = (event.summary or "").strip() + if len(title) > 120: + title = title[:117] + "…" + if len(body) > _MOBILE_PUSH_BODY_MAX: + body = body[: _MOBILE_PUSH_BODY_MAX - 1] + "…" + return title, body + + def _format_rdg_html(event: Event) -> str: details = _details_dict(event) if event.type.endswith(".success"): diff --git a/backend/app/version.py b/backend/app/version.py index 3af75ca..21e3042 100644 --- a/backend/app/version.py +++ b/backend/app/version.py @@ -1,5 +1,5 @@ """Единый источник версии SAC (API, health, логи, OpenAPI).""" APP_NAME = "Security Alert Center" -APP_VERSION = "0.9.10" +APP_VERSION = "0.9.11" APP_VERSION_LABEL = f"{APP_NAME} v.{APP_VERSION}" diff --git a/backend/tests/test_health.py b/backend/tests/test_health.py index cbf17d3..e2f888c 100644 --- a/backend/tests/test_health.py +++ b/backend/tests/test_health.py @@ -4,6 +4,6 @@ from app.version import APP_NAME, APP_VERSION, APP_VERSION_LABEL def test_version_constants(): - assert APP_VERSION == "0.9.10" + assert APP_VERSION == "0.9.11" assert APP_NAME == "Security Alert Center" - assert APP_VERSION_LABEL == "Security Alert Center v.0.9.10" + assert APP_VERSION_LABEL == "Security Alert Center v.0.9.11" diff --git a/backend/tests/test_mobile_notify.py b/backend/tests/test_mobile_notify.py index 9a0f7d1..8aca1ac 100644 --- a/backend/tests/test_mobile_notify.py +++ b/backend/tests/test_mobile_notify.py @@ -1,10 +1,33 @@ """FCM push must not break event ingest.""" +from datetime import datetime, timezone from unittest.mock import MagicMock, patch import pytest -from app.services.mobile_notify import MobileNotConfiguredError, _send_fcm_data +from app.models import Event, Host +from app.services.mobile_notify import MobileNotConfiguredError, _event_payload, _send_fcm_data + + +def test_event_payload_lifecycle_uses_telegram_wording(): + host = Host(hostname="WIN01", display_name="K6A-DC3", os_family="windows") + event = Event( + event_id="00000000-0000-4000-8000-000000000511", + host_id=1, + host=host, + occurred_at=datetime(2026, 5, 29, 9, 0, tzinfo=timezone.utc), + category="agent", + type="agent.lifecycle", + severity="info", + title="started", + summary="short", + details={"lifecycle": "started", "trigger": "settings_reload", "telegram_via": "sac"}, + payload={"source": {"product": "ssh-monitor", "product_version": "1.4.0-SAC"}}, + ) + title, body, data = _event_payload(event) + assert title == "✅ Агент запущен" + assert "graceful restart" in body + assert data["type"] == "agent.lifecycle" def test_send_fcm_data_swallows_token_errors_by_default(): diff --git a/backend/tests/test_telegram_templates.py b/backend/tests/test_telegram_templates.py index 342c35f..c7b5834 100644 --- a/backend/tests/test_telegram_templates.py +++ b/backend/tests/test_telegram_templates.py @@ -3,7 +3,13 @@ from datetime import datetime, timezone from app.models import Event, Host -from app.services.telegram_templates import format_event_telegram_html, format_rdp_login_html, html_escape +from app.services.telegram_templates import ( + format_event_mobile_push, + format_event_telegram_html, + format_rdp_login_html, + html_escape, + telegram_html_to_plaintext, +) def test_html_escape_special_chars(): @@ -145,6 +151,61 @@ def test_event_telegram_includes_sac_source_for_sac_daily_report(): assert "📡 Оповещение: SAC (Security Alert Center)" in text +def test_telegram_html_to_plaintext_strips_tags(): + assert telegram_html_to_plaintext("✅ Агент запущен\n🏢 Хост: WIN01") == ( + "✅ Агент запущен\n🏢 Хост: WIN01" + ) + + +def test_format_event_mobile_push_lifecycle_includes_trigger(): + host = Host(hostname="WIN01", display_name="K6A-DC3", os_family="windows") + event = Event( + event_id="00000000-0000-4000-8000-000000000509", + host_id=1, + host=host, + occurred_at=datetime(2026, 5, 29, 9, 0, tzinfo=timezone.utc), + category="agent", + type="agent.lifecycle", + severity="info", + title="started", + summary="Мониторинг запущен", + details={"lifecycle": "started", "trigger": "deploy_recycle", "telegram_via": "sac"}, + payload={"source": {"product": "rdp-login-monitor", "product_version": "1.2.30-SAC"}}, + ) + title, body = format_event_mobile_push(event) + assert title == "✅ Агент запущен" + assert "K6A-DC3" in body + assert "1.2.30-SAC" in body + assert "обновление скрипта" in body + assert "📡 Оповещение: SAC" in body + + +def test_format_event_mobile_push_uses_notification_body(): + host = Host(hostname="WIN01", display_name="K6A-DC3", os_family="windows") + body_text = "✅ Мониторинг логинов ЗАПУЩЕН\n🖥️ Сервер: K6A-DC3 (10.0.0.1)" + event = Event( + event_id="00000000-0000-4000-8000-000000000510", + host_id=1, + host=host, + occurred_at=datetime(2026, 5, 29, 9, 0, tzinfo=timezone.utc), + category="agent", + type="agent.lifecycle", + severity="info", + title="started", + summary="Мониторинг запущен", + details={ + "lifecycle": "started", + "trigger": "boot", + "notification_body": body_text, + "telegram_via": "sac", + }, + payload={"source": {"product": "rdp-login-monitor", "product_version": "1.2.31-SAC"}}, + ) + title, body = format_event_mobile_push(event) + assert title == "✅ Мониторинг логинов ЗАПУЩЕН" + assert "K6A-DC3 (10.0.0.1)" in body + + def test_lifecycle_started_template(): host = Host(hostname="WIN01", display_name="K6A-DC3", os_family="windows") event = Event( diff --git a/frontend/src/version.ts b/frontend/src/version.ts index 56f486f..6e238db 100644 --- a/frontend/src/version.ts +++ b/frontend/src/version.ts @@ -1,4 +1,4 @@ /** Fallback до загрузки /health; при релизе держите в sync с backend/app/version.py */ export const APP_NAME = "Security Alert Center"; -export const APP_VERSION = "0.9.10"; +export const APP_VERSION = "0.9.11"; export const APP_VERSION_LABEL = `${APP_NAME} v.${APP_VERSION}`;