Preserve vzdump inventory when AI is enabled

This commit is contained in:
Vaso73
2026-09-17 11:44:49 +02:00
committed by VAIO73
parent 49cb8133ed
commit c83c68b4ee
2 changed files with 77 additions and 29 deletions
+52 -9
View File
@@ -308,18 +308,51 @@ class TelegramChannel(NotificationChannel):
return self._http_request(url, payload, {'Content-Type': 'application/json'}) return self._http_request(url, payload, {'Content-Type': 'application/json'})
def _split_message(self, text: str) -> list: def _split_message(self, text: str) -> list:
"""Split Telegram HTML without cutting entities or formatting tags.
Open formatting tags are closed at the end of a chunk and reopened in
the next one, so every API request is valid HTML on its own.
"""
if len(text) <= self.MAX_LENGTH: if len(text) <= self.MAX_LENGTH:
return [text] return [text]
token_re = re.compile(
r'&(?:#[0-9]+|#x[0-9A-Fa-f]+|[A-Za-z][A-Za-z0-9]+);|<[^<>]+>|.',
re.DOTALL,
)
tag_re = re.compile(r'<\s*(/?)\s*([A-Za-z0-9-]+)(?:\s[^<>]*)?>')
void_tags = {'br'}
def _advance(stack, token):
match = tag_re.fullmatch(token)
if not match:
return list(stack)
closing, name = match.groups()
name = name.lower()
next_stack = list(stack)
if closing:
if next_stack and next_stack[-1][0] == name:
next_stack.pop()
elif not token.rstrip().endswith('/>') and name not in void_tags:
next_stack.append((name, token))
return next_stack
def _closers(stack):
return ''.join(f'</{name}>' for name, _ in reversed(stack))
chunks = [] chunks = []
while text: current = ''
if len(text) <= self.MAX_LENGTH: open_tags = []
chunks.append(text) for token in token_re.findall(text):
break next_tags = _advance(open_tags, token)
split_at = text.rfind('\n', 0, self.MAX_LENGTH) if current and len(current) + len(token) + len(_closers(next_tags)) > self.MAX_LENGTH:
if split_at == -1: chunks.append(current + _closers(open_tags))
split_at = self.MAX_LENGTH current = ''.join(opener for _, opener in open_tags)
chunks.append(text[:split_at]) current += token
text = text[split_at:].lstrip('\n') open_tags = _advance(open_tags, token)
if current:
chunks.append(current + _closers(open_tags))
return chunks return chunks
@staticmethod @staticmethod
@@ -949,6 +982,16 @@ class EmailChannel(NotificationChannel):
# ── Build structured detail rows from known data fields ── # ── Build structured detail rows from known data fields ──
detail_rows = self._build_detail_rows(data, event_type, group, html_mod) detail_rows = self._build_detail_rows(data, event_type, group, html_mod)
# Vzdump bodies are authoritative multi-item inventories. Structured
# backup metadata is only a summary and previously replaced the body in
# the HTML alternative, hiding all guest rows. Render every body line
# exactly once for these events instead of mixing both representations.
if event_type in {'backup_complete', 'backup_fail'}:
detail_rows = [
('', html_mod.escape(line.strip()))
for line in body.split('\n') if line.strip()
]
# ── Fallback: if no structured rows, render body text lines ── # ── Fallback: if no structured rows, render body text lines ──
if not detail_rows: if not detail_rows:
for line in body.split('\n'): for line in body.split('\n'):
+25 -20
View File
@@ -1376,27 +1376,32 @@ class NotificationManager:
# raw template-formatted notification. Audit Tier 6 — # raw template-formatted notification. Audit Tier 6 —
# `_dispatch_to_channels`: AI failure dropped the notification. # `_dispatch_to_channels`: AI failure dropped the notification.
try: try:
enriched_context = enrich_context_for_ai( # Backup reports are authoritative inventories. A model can
title=ch_title, # neither be trusted to avoid repeating all guest rows nor to
body=ch_body, # preserve every value, so these two events bypass AI entirely.
event_type=event_type, ai_result = None
data=data, if event_type not in {'backup_complete', 'backup_fail'}:
journal_context=raw_journal_context, enriched_context = enrich_context_for_ai(
detail_level=detail_level title=ch_title,
) body=ch_body,
event_type=event_type,
data=data,
journal_context=raw_journal_context,
detail_level=detail_level
)
# Wrap the AI rewrite with a hard timeout so a slow Ollama # Wrap the AI rewrite with a hard timeout so a slow Ollama
# call (90-120 s on slow CPUs) doesn't stall the dispatch # call (90-120 s on slow CPUs) doesn't stall the dispatch
# thread and delay every other queued event. On timeout we # thread and delay every other queued event. On timeout we
# ship the non-AI title/body — the user still gets the # ship the non-AI title/body — the user still gets the
# notification, just without LLM polish. Audit Tier 3.2 #2. # notification, just without LLM polish. Audit Tier 3.2 #2.
ai_result = _format_with_ai_bounded( ai_result = _format_with_ai_bounded(
format_with_ai_full, format_with_ai_full,
ch_title, ch_body, severity, channel_ai_config, ch_title, ch_body, severity, channel_ai_config,
detail_level=detail_level, detail_level=detail_level,
journal_context=enriched_context, journal_context=enriched_context,
use_emojis=use_rich_format, use_emojis=use_rich_format,
) )
if ai_result is not None: if ai_result is not None:
ch_title = ai_result.get('title', ch_title) ch_title = ai_result.get('title', ch_title)
ch_body = ai_result.get('body', ch_body) ch_body = ai_result.get('body', ch_body)