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'})
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:
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 = []
while text:
if len(text) <= self.MAX_LENGTH:
chunks.append(text)
break
split_at = text.rfind('\n', 0, self.MAX_LENGTH)
if split_at == -1:
split_at = self.MAX_LENGTH
chunks.append(text[:split_at])
text = text[split_at:].lstrip('\n')
current = ''
open_tags = []
for token in token_re.findall(text):
next_tags = _advance(open_tags, token)
if current and len(current) + len(token) + len(_closers(next_tags)) > self.MAX_LENGTH:
chunks.append(current + _closers(open_tags))
current = ''.join(opener for _, opener in open_tags)
current += token
open_tags = _advance(open_tags, token)
if current:
chunks.append(current + _closers(open_tags))
return chunks
@staticmethod
@@ -949,6 +982,16 @@ class EmailChannel(NotificationChannel):
# ── Build structured detail rows from known data fields ──
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 ──
if not detail_rows:
for line in body.split('\n'):
+25 -20
View File
@@ -1376,27 +1376,32 @@ class NotificationManager:
# raw template-formatted notification. Audit Tier 6 —
# `_dispatch_to_channels`: AI failure dropped the notification.
try:
enriched_context = enrich_context_for_ai(
title=ch_title,
body=ch_body,
event_type=event_type,
data=data,
journal_context=raw_journal_context,
detail_level=detail_level
)
# Backup reports are authoritative inventories. A model can
# neither be trusted to avoid repeating all guest rows nor to
# preserve every value, so these two events bypass AI entirely.
ai_result = None
if event_type not in {'backup_complete', 'backup_fail'}:
enriched_context = enrich_context_for_ai(
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
# call (90-120 s on slow CPUs) doesn't stall the dispatch
# thread and delay every other queued event. On timeout we
# ship the non-AI title/body — the user still gets the
# notification, just without LLM polish. Audit Tier 3.2 #2.
ai_result = _format_with_ai_bounded(
format_with_ai_full,
ch_title, ch_body, severity, channel_ai_config,
detail_level=detail_level,
journal_context=enriched_context,
use_emojis=use_rich_format,
)
# Wrap the AI rewrite with a hard timeout so a slow Ollama
# call (90-120 s on slow CPUs) doesn't stall the dispatch
# thread and delay every other queued event. On timeout we
# ship the non-AI title/body — the user still gets the
# notification, just without LLM polish. Audit Tier 3.2 #2.
ai_result = _format_with_ai_bounded(
format_with_ai_full,
ch_title, ch_body, severity, channel_ai_config,
detail_level=detail_level,
journal_context=enriched_context,
use_emojis=use_rich_format,
)
if ai_result is not None:
ch_title = ai_result.get('title', ch_title)
ch_body = ai_result.get('body', ch_body)