fix(notifications): localize sensor alerts and preserve email context

This commit is contained in:
martino
2026-09-29 11:31:16 +02:00
parent 4c08ef6ea0
commit 25cb81d740
13 changed files with 279 additions and 47 deletions
+10 -3
View File
@@ -6446,8 +6446,8 @@
"label": "Hohe Speichernutzung"
},
"temp_high": {
"title": "{hostname}: Hohe CPU-Temperatur – {value}°C",
"body": "Die CPU-Temperatur hat {value}°C erreicht (Schwellenwert: {threshold}°C).\n{details}",
"title": "{hostname}: Hohe Sensortemperatur – {value}°C",
"body": "Die Sensortemperatur hat {value}°C erreicht (Schwellenwert: {threshold}°C).\n{details}",
"label": "Hohe Temperatur"
},
"disk_space_low": {
@@ -6781,7 +6781,11 @@
"fallback": {
"unknownTitle": "{hostname}: {event_type}",
"healthCheckDegraded": "Gesundheitsprüfung beeinträchtigt",
"none": "keine"
"none": "keine",
"temperatureAlertTitle": "{hostname}: Warnung zur Sensortemperatur",
"temperatureAlertBody": "Ein Temperaturalarm wurde ohne vollständige Messdaten erfasst.",
"recordedReason": "Erfasster Grund: {reason}",
"recordedDetails": "Erfasste Details: {details}"
},
"fields": {
"vmid": "VM/CT",
@@ -6981,6 +6985,9 @@
"host": "Gastgeber",
"footer": "ProxMenux Benachrichtigungsdienst"
}
},
"temperature": {
"sampleSpan": "Die hohen Messwerte erstrecken sich über {duration}."
}
}
}
File diff suppressed because one or more lines are too long
+10 -3
View File
@@ -6446,8 +6446,8 @@
"label": "Uso elevado de memoria"
},
"temp_high": {
"title": "{hostname}: Temperatura alta de la CPU: {value}°C",
"body": "La temperatura de la CPU ha alcanzado {value}°C (umbral: {threshold}°C).\n{details}",
"title": "{hostname}: Temperatura elevada del sensor — {value}°C",
"body": "La temperatura del sensor ha alcanzado {value}°C (umbral: {threshold}°C).\n{details}",
"label": "Alta temperatura"
},
"disk_space_low": {
@@ -6781,7 +6781,11 @@
"fallback": {
"unknownTitle": "{hostname}: {event_type}",
"healthCheckDegraded": "Comprobación de salud degradada",
"none": "ninguno"
"none": "ninguno",
"temperatureAlertTitle": "{hostname}: Alerta de temperatura del sensor",
"temperatureAlertBody": "Se registró una alerta de temperatura sin datos de medición completos.",
"recordedReason": "Motivo registrado: {reason}",
"recordedDetails": "Detalles registrados: {details}"
},
"fields": {
"vmid": "VM/CT",
@@ -6981,6 +6985,9 @@
"host": "Host",
"footer": "Servicio de notificaciones de ProxMenux"
}
},
"temperature": {
"sampleSpan": "Las muestras elevadas abarcan {duration}."
}
}
}
+10 -3
View File
@@ -6446,8 +6446,8 @@
"label": "Utilisation élevée de la mémoire"
},
"temp_high": {
"title": "{hostname} : température élevée du processeur – {value}°C",
"body": "La température du processeur a atteint {value}°C (seuil : {threshold}°C).\n{details}",
"title": "{hostname} : température élevée du capteur – {value}°C",
"body": "La température du capteur a atteint {value}°C (seuil : {threshold}°C).\n{details}",
"label": "Haute température"
},
"disk_space_low": {
@@ -6781,7 +6781,11 @@
"fallback": {
"unknownTitle": "{hostname} : {event_type}",
"healthCheckDegraded": "Bilan de santé dégradé",
"none": "aucun"
"none": "aucun",
"temperatureAlertTitle": "{hostname} : alerte de température du capteur",
"temperatureAlertBody": "Une alerte de température a été enregistrée sans données de mesure complètes.",
"recordedReason": "Motif enregistré : {reason}",
"recordedDetails": "Détails enregistrés : {details}"
},
"fields": {
"vmid": "VM/CT",
@@ -6981,6 +6985,9 @@
"host": "Hôte",
"footer": "ProxMenux Service de notification"
}
},
"temperature": {
"sampleSpan": "Les relevés élevés s'étendent sur {duration}."
}
}
}
+10 -3
View File
@@ -6446,8 +6446,8 @@
"label": "Utilizzo elevato della memoria"
},
"temp_high": {
"title": "{hostname}: temperatura elevata della CPU — {value}°C",
"body": "La temperatura della CPU ha raggiunto {value}°C (soglia: {threshold}°C).\n{details}",
"title": "{hostname}: temperatura elevata del sensore — {value}°C",
"body": "La temperatura del sensore ha raggiunto {value}°C (soglia: {threshold}°C).\n{details}",
"label": "Alta temperatura"
},
"disk_space_low": {
@@ -6781,7 +6781,11 @@
"fallback": {
"unknownTitle": "{hostname}: {event_type}",
"healthCheckDegraded": "Controllo sanitario degradato",
"none": "nessuno"
"none": "nessuno",
"temperatureAlertTitle": "{hostname}: avviso sulla temperatura del sensore",
"temperatureAlertBody": "È stato registrato un avviso di temperatura senza dati di misurazione completi.",
"recordedReason": "Motivo registrato: {reason}",
"recordedDetails": "Dettagli registrati: {details}"
},
"fields": {
"vmid": "VM/CT",
@@ -6981,6 +6985,9 @@
"host": "Ospite",
"footer": "ProxMenux Servizio di notifica"
}
},
"temperature": {
"sampleSpan": "Intervallo dei campioni sopra soglia: {duration}."
}
}
}
+10 -3
View File
@@ -6446,8 +6446,8 @@
"label": "Alto uso de memória"
},
"temp_high": {
"title": "{hostname}: Alta temperatura da CPU - {value}°C",
"body": "A temperatura da CPU atingiu {value}°C (limite: {threshold}°C).\n{details}",
"title": "{hostname}: Temperatura elevada do sensor — {value}°C",
"body": "A temperatura do sensor atingiu {value}°C (limite: {threshold}°C).\n{details}",
"label": "Alta temperatura"
},
"disk_space_low": {
@@ -6781,7 +6781,11 @@
"fallback": {
"unknownTitle": "{hostname}: {event_type}",
"healthCheckDegraded": "Exame de saúde degradado",
"none": "nenhum"
"none": "nenhum",
"temperatureAlertTitle": "{hostname}: Alerta de temperatura do sensor",
"temperatureAlertBody": "Foi registado um alerta de temperatura sem dados de medição completos.",
"recordedReason": "Motivo registado: {reason}",
"recordedDetails": "Detalhes registados: {details}"
},
"fields": {
"vmid": "VM/CT",
@@ -6981,6 +6985,9 @@
"host": "Anfitrião",
"footer": "Serviço de Notificação ProxMenux"
}
},
"temperature": {
"sampleSpan": "As amostras elevadas abrangem {duration}."
}
}
}
+10 -3
View File
@@ -6445,8 +6445,8 @@
"label": "Vysoké využitie pamäte"
},
"temp_high": {
"title": "{hostname}: Vysoká teplota CPU — {value}°C",
"body": "Teplota CPU dosiahla {value}°C (prahová hodnota: {threshold}°C).\n{details}",
"title": "{hostname}: Vysoká teplota snímača — {value}°C",
"body": "Teplota snímača dosiahla {value}°C (prahová hodnota: {threshold}°C).\n{details}",
"label": "Vysoká teplota"
},
"disk_space_low": {
@@ -6780,7 +6780,11 @@
"fallback": {
"unknownTitle": "{hostname}: {event_type}",
"healthCheckDegraded": "Zhoršený stav kontroly",
"none": "žiadne"
"none": "žiadne",
"temperatureAlertTitle": "{hostname}: Upozornenie na teplotu snímača",
"temperatureAlertBody": "Upozornenie na teplotu bolo zaznamenané bez úplných údajov o meraní.",
"recordedReason": "Zaznamenaný dôvod: {reason}",
"recordedDetails": "Zaznamenané podrobnosti: {details}"
},
"fields": {
"vmid": "VM/CT",
@@ -6980,6 +6984,9 @@
"started": "Spustené"
}
}
},
"temperature": {
"sampleSpan": "Vysoké vzorky pokrývajú interval {duration}."
}
}
}
+10 -3
View File
@@ -6446,8 +6446,8 @@
"label": "Hög minnesanvändning"
},
"temp_high": {
"title": "{hostname}: Hög CPU-temperatur — {value}°C",
"body": "CPU-temperaturen har nått {value}°C (tröskel: {threshold}°C).\n{details}",
"title": "{hostname}: Hög sensortemperatur — {value}°C",
"body": "Sensortemperaturen har nått {value}°C (tröskel: {threshold}°C).\n{details}",
"label": "Hög temperatur"
},
"disk_space_low": {
@@ -6781,7 +6781,11 @@
"fallback": {
"unknownTitle": "{hostname}: {event_type}",
"healthCheckDegraded": "Hälsokontroll försämrad",
"none": "ingen"
"none": "ingen",
"temperatureAlertTitle": "{hostname}: Varning om sensortemperatur",
"temperatureAlertBody": "En temperaturvarning registrerades utan fullständiga mätdata.",
"recordedReason": "Registrerad orsak: {reason}",
"recordedDetails": "Registrerade detaljer: {details}"
},
"fields": {
"vmid": "VM/CT",
@@ -6981,6 +6985,9 @@
"host": "Värd",
"footer": "ProxMenux Notification Service"
}
},
"temperature": {
"sampleSpan": "De höga mätvärdena sträcker sig över {duration}."
}
}
}
+3
View File
@@ -1586,6 +1586,9 @@ class HealthMonitor:
'temperature': max_temp, 'duration': actual_duration,
'dismissable': True, 'value': max_temp, 'threshold': 80,
'details': f'High samples span {duration_str}.',
# Explicit provenance for render-time localization;
# older/manual detail strings remain verbatim.
'temperature_detail_kind': 'high_samples_span',
}
)
elif len(recovery_samples) >= 3:
+8 -1
View File
@@ -1049,6 +1049,13 @@ class EmailChannel(NotificationChannel):
# ── Build structured detail rows from known data fields ──
detail_rows = self._build_detail_rows(data, event_type, group, html_mod)
# The temperature renderer has already validated whether both
# measurements exist and localized its sampled-span detail. Its body
# is authoritative: raw fields can otherwise print a misleading °C
# value for an incomplete manual/persisted payload and hide the body.
if event_type == 'temp_high':
detail_rows = []
# Vzdump bodies are authoritative multi-item inventories. Keep their
# lines exactly once, but retain the localized structured status row;
# the remaining structured backup metadata only duplicates the report.
@@ -1093,7 +1100,7 @@ class EmailChannel(NotificationChannel):
# ── Reason / details block (long text, displayed separately) ──
reason = data.get('reason', '')
reason_html = ''
if reason and len(reason) > 80:
if reason and len(reason) > 80 and not (event_type == 'temp_high' and reason in body):
reason_html = f'''
<div style="margin:16px 0 0;padding:12px 16px;border:1px solid #d1d5db;border-radius:6px;">
<p style="margin:0 0 4px;font-size:11px;font-weight:600;color:#374151;text-transform:uppercase;letter-spacing:0.05em;">{_runtime_text('email.details', data)}</p>
+14 -2
View File
@@ -1899,13 +1899,25 @@ def render_template(event_type: str, data: Dict[str, Any],
except (OverflowError, ValueError, TypeError):
return False
# Only the known sampler-generated detail is eligible: duration alone
# cannot establish provenance and manual/legacy text is not rewritten.
seconds = data.get('duration')
if (data.get('temperature_detail_kind') == 'high_samples_span'
and type(seconds) is int and 0 <= seconds <= 240):
span = (f'{seconds // 60}m {seconds % 60}s'
if seconds >= 60 else f'{seconds}s')
if data.get('details') == f'High samples span {span}.':
variables['details'] = runtime_message(
'temperature.sampleSpan', language, duration=span)
if not all(finite_measurement(key) for key in ('value', 'threshold')):
template['title'] = runtime_message(
variables['_temperature_fallback_title'] = runtime_message(
'fallback.temperatureAlertTitle', language, hostname=variables['hostname'],
)
template['title'] = '{_temperature_fallback_title}'
lines = [runtime_message('fallback.temperatureAlertBody', language)]
reason = data.get('reason')
details = data.get('details')
details = variables.get('details')
reason = reason.strip() if isinstance(reason, str) else ''
details = details.strip() if isinstance(details, str) else ''
if reason:
@@ -110,7 +110,12 @@ class RuntimeCatalogTests(unittest.TestCase):
values.update({"hostname": "HOST-ŽILINA", "severity": "WARNING"})
with mock.patch.object(notification_templates, "_get_hostname", return_value="HOST-ŽILINA"):
for event_type in notification_templates.TEMPLATES:
rendered = notification_templates.render_template(event_type, values, language="sk")
event_values = dict(values)
if event_type == "temp_high":
# Temperature is a measured numeric contract; arbitrary
# DYNAMIC_VALUE is correctly rejected by its fallback.
event_values.update(value="89", threshold="80")
rendered = notification_templates.render_template(event_type, event_values, language="sk")
combined = rendered["title"] + "\n" + rendered["body"]
if notification_templates.TEMPLATES[event_type].get("formatter"):
continue
@@ -119,7 +124,7 @@ class RuntimeCatalogTests(unittest.TestCase):
+ notification_templates.TEMPLATES[event_type]["body"]
):
if name not in {"entity_suffix", "title_or_default"}:
self.assertIn(str(values[name]), combined, f"{event_type}:{name}")
self.assertIn(str(event_values[name]), combined, f"{event_type}:{name}")
def test_lxc_update_result_and_details_render_in_slovak(self):
data = {