diff --git a/backend/app/services/daily_report.py b/backend/app/services/daily_report.py
index 7f7bd09..31a651f 100644
--- a/backend/app/services/daily_report.py
+++ b/backend/app/services/daily_report.py
@@ -198,7 +198,8 @@ def _build_report_body_rdp(host: Host, stats: dict[str, Any], when_local: dateti
def _details_from_body(body: str, stats: dict[str, Any]) -> dict[str, Any]:
escaped = html.escape(body)
- report_html = '
' + escaped.replace("\n", "
\n") + "
"
+ # UI (Vue) допускает div; для единообразия — обёртка без br (Telegram читает через sanitize)
+ report_html = f'{escaped.replace(chr(10), "
")}
'
return {
"stats": stats,
"report_body": body,
diff --git a/backend/app/services/telegram_templates.py b/backend/app/services/telegram_templates.py
index abf841d..0907eeb 100644
--- a/backend/app/services/telegram_templates.py
+++ b/backend/app/services/telegram_templates.py
@@ -3,6 +3,7 @@
from __future__ import annotations
import html
+import re
from datetime import datetime
from typing import Any
@@ -20,6 +21,25 @@ LOGON_TYPE_NAMES: dict[int, str] = {
}
+# Telegram parse_mode=HTML: только теги из Bot API (без div/br/p и т.д.)
+_TELEGRAM_BR = re.compile(r"
", re.I)
+_TELEGRAM_DROP_TAGS = re.compile(
+ r"?(?:div|p|ul|ol|li|hr|h[1-6]|table|tr|td|th|thead|tbody|body|html|head)\b[^>]*>",
+ re.I,
+)
+_TELEGRAM_SPAN_OPEN = re.compile(r"]*\btg-spoiler\b)[^>]*>", re.I)
+_TELEGRAM_SPAN_CLOSE = re.compile(r"", re.I)
+
+
+def sanitize_telegram_html(text: str) -> str:
+ """Приводит HTML отчёта (UI/агент) к тегам, допустимым в Telegram."""
+ out = _TELEGRAM_BR.sub("\n", text)
+ out = _TELEGRAM_DROP_TAGS.sub("", out)
+ out = _TELEGRAM_SPAN_OPEN.sub("", out)
+ out = _TELEGRAM_SPAN_CLOSE.sub("", out)
+ return out.strip()
+
+
def html_escape(value: Any) -> str:
if value is None:
return ""
@@ -163,15 +183,11 @@ def format_daily_report_html(event: Event) -> str:
details = _details_dict(event)
report_html = details.get("report_html")
if isinstance(report_html, str) and report_html.strip():
- # Уже HTML от агента или SAC
- return report_html.strip()
+ return sanitize_telegram_html(report_html.strip())
body = _detail(details, "report_body", default=event.summary)
if body != "-":
escaped = html.escape(body)
- return (
- f"📊 {html_escape(event.title)}\n"
- f'{escaped.replace(chr(10), "
")}
'
- )
+ return f"📊 {html_escape(event.title)}\n{escaped}"
return format_generic_event_html(event)
diff --git a/backend/tests/test_daily_report.py b/backend/tests/test_daily_report.py
index 0921ecd..f300fe8 100644
--- a/backend/tests/test_daily_report.py
+++ b/backend/tests/test_daily_report.py
@@ -16,7 +16,7 @@ from app.services.daily_report import (
run_daily_reports,
)
from app.services.notification_policy import NotificationPolicyConfig
-from app.services.telegram_templates import format_event_telegram_html
+from app.services.telegram_templates import format_event_telegram_html, sanitize_telegram_html
def _ssh_host(db) -> Host:
@@ -209,4 +209,9 @@ def test_daily_report_telegram_html_uses_report_html(db_session):
details={"report_html": "📊 OK
line"},
payload={},
)
- assert format_event_telegram_html(event) == "📊 OK
line"
+ assert format_event_telegram_html(event) == "📊 OK\nline"
+
+
+def test_sanitize_telegram_html_strips_div_and_br():
+ raw = 'a
b
'
+ assert sanitize_telegram_html(raw) == "a\nb"
diff --git a/backend/tests/test_telegram_templates.py b/backend/tests/test_telegram_templates.py
index e4120f9..904e34a 100644
--- a/backend/tests/test_telegram_templates.py
+++ b/backend/tests/test_telegram_templates.py
@@ -10,6 +10,12 @@ def test_html_escape_special_chars():
assert html_escape("a&") == "a<b>&"
+def test_sanitize_telegram_html_keeps_bold():
+ from app.services.telegram_templates import sanitize_telegram_html
+
+ assert sanitize_telegram_html("title
line") == "title\nline"
+
+
def test_rdp_failed_template_includes_user_ip_logon():
host = Host(hostname="K6A-DC3", display_name="UNMS Kalina", os_family="windows")
event = Event(