fix: Seaca push uses same lifecycle text as Telegram (0.9.11)

Reuse telegram templates for FCM title/body so agent start/restart
reasons reach mobile without Seaca app changes.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
PTah
2026-06-17 11:35:17 +10:00
parent 8a97bbfb43
commit 55f047cd35
7 changed files with 129 additions and 8 deletions
+2 -2
View File
@@ -16,6 +16,7 @@ from app.models import Event, Problem
from app.models.mobile_device import MobileDevice from app.models.mobile_device import MobileDevice
from app.services.notification_severity import severity_meets_minimum from app.services.notification_severity import severity_meets_minimum
from app.services.notification_policy import get_effective_notification_policy from app.services.notification_policy import get_effective_notification_policy
from app.services.telegram_templates import format_event_mobile_push
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -170,8 +171,7 @@ def _send_fcm_data(
def _event_payload(event: Event) -> tuple[str, str, dict[str, str]]: def _event_payload(event: Event) -> tuple[str, str, dict[str, str]]:
title = f"[{event.severity}] {event.title}" title, body = format_event_mobile_push(event)
body = (event.summary or "")[:200]
data = { data = {
"kind": "event", "kind": "event",
"id": str(event.id), "id": str(event.id),
@@ -506,6 +506,43 @@ def format_event_telegram_html(event: Event) -> str:
return _append_event_source(_format_event_body_html(event), event) return _append_event_source(_format_event_body_html(event), event)
_TELEGRAM_INLINE_TAGS = re.compile(r"</?(?:b|i|u|s|code|pre|tg-spoiler)(\s[^>]*)?>", re.I)
_TELEGRAM_ANCHOR_OPEN = re.compile(r"<a\b[^>]*>", re.I)
_TELEGRAM_ANCHOR_CLOSE = re.compile(r"</a>", 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: def _format_rdg_html(event: Event) -> str:
details = _details_dict(event) details = _details_dict(event)
if event.type.endswith(".success"): if event.type.endswith(".success"):
+1 -1
View File
@@ -1,5 +1,5 @@
"""Единый источник версии SAC (API, health, логи, OpenAPI).""" """Единый источник версии SAC (API, health, логи, OpenAPI)."""
APP_NAME = "Security Alert Center" APP_NAME = "Security Alert Center"
APP_VERSION = "0.9.10" APP_VERSION = "0.9.11"
APP_VERSION_LABEL = f"{APP_NAME} v.{APP_VERSION}" APP_VERSION_LABEL = f"{APP_NAME} v.{APP_VERSION}"
+2 -2
View File
@@ -4,6 +4,6 @@ from app.version import APP_NAME, APP_VERSION, APP_VERSION_LABEL
def test_version_constants(): def test_version_constants():
assert APP_VERSION == "0.9.10" assert APP_VERSION == "0.9.11"
assert APP_NAME == "Security Alert Center" 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"
+24 -1
View File
@@ -1,10 +1,33 @@
"""FCM push must not break event ingest.""" """FCM push must not break event ingest."""
from datetime import datetime, timezone
from unittest.mock import MagicMock, patch from unittest.mock import MagicMock, patch
import pytest 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(): def test_send_fcm_data_swallows_token_errors_by_default():
+62 -1
View File
@@ -3,7 +3,13 @@
from datetime import datetime, timezone from datetime import datetime, timezone
from app.models import Event, Host 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(): 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 assert "📡 Оповещение: SAC (Security Alert Center)" in text
def test_telegram_html_to_plaintext_strips_tags():
assert telegram_html_to_plaintext("<b>✅ Агент запущен</b>\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(): def test_lifecycle_started_template():
host = Host(hostname="WIN01", display_name="K6A-DC3", os_family="windows") host = Host(hostname="WIN01", display_name="K6A-DC3", os_family="windows")
event = Event( event = Event(
+1 -1
View File
@@ -1,4 +1,4 @@
/** Fallback до загрузки /health; при релизе держите в sync с backend/app/version.py */ /** Fallback до загрузки /health; при релизе держите в sync с backend/app/version.py */
export const APP_NAME = "Security Alert Center"; 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}`; export const APP_VERSION_LABEL = `${APP_NAME} v.${APP_VERSION}`;