diff --git a/AppImage/messages/de/common.json b/AppImage/messages/de/common.json
index bc45faf4..b50df385 100644
--- a/AppImage/messages/de/common.json
+++ b/AppImage/messages/de/common.json
@@ -1820,6 +1820,12 @@
"updates": {
"label": "Aktualisierungen"
},
+ "hardware": {
+ "label": "Hardware"
+ },
+ "system": {
+ "label": "System"
+ },
"other": {
"label": "Andere"
}
@@ -1831,6 +1837,8 @@
"error_escalated": "Gesundheitsproblem eskalierte",
"health_degraded": "Gesundheitscheck verschlechtert",
"lxc_updates_available": "LXC-Updates verfügbar (experimentell)",
+ "lxc_update_applied": "LXC update applied",
+ "docker_stack_update_available": "Docker updates available",
"vm_start": "VM gestartet",
"vm_start_warning": "VM gestartet (Warnungen)",
"vm_stop": "VM gestoppt",
@@ -1982,7 +1990,10 @@
"systemHostname": "System-Hostname",
"displayNameHint": "Name, der in Benachrichtigungen angezeigt wird. Bearbeiten Sie es oder lassen Sie es leer, um den System-Hostnamen zu verwenden.",
"activeFooter": "Benachrichtigungen sind aktiv. Jeder Kanal folgt seiner eigenen Kategorie und Ereignisauswahl.",
- "inactiveFooter": "Aktivieren Sie Benachrichtigungen, um System-, Gesundheits- und Sicherheitswarnungen über die konfigurierten Kanäle zu erhalten."
+ "inactiveFooter": "Aktivieren Sie Benachrichtigungen, um System-, Gesundheits- und Sicherheitswarnungen über die konfigurierten Kanäle zu erhalten.",
+ "notificationLanguage": "Notification language",
+ "selectNotificationLanguage": "Select notification language",
+ "notificationLanguageHint": "Used for deterministic runtime notifications even when AI is disabled."
},
"ai": {
"title": "KI-Verbesserung",
@@ -6018,5 +6029,6 @@
"withoutLynis": "Run without Lynis",
"cancel": "Cancel"
}
- }
+ },
+ "runtime": {"notifications":{"templates":{"state_change":{"title":"{hostname}: {category} changed to {current}{entity_suffix}","body":"{category} status changed from {previous} to {current}.\n{reason}","label":"Health state changed"},"new_error":{"title":"{hostname}: New {severity} - {category}{entity_suffix}","body":"{reason}","label":"New health issue"},"error_resolved":{"title":"{hostname}: Resolved - {category}{entity_suffix}","body":"The {category} issue has been resolved.\n{reason}\n🚦 Previous severity: {original_severity}\n⏱️ Duration: {duration}","label":"Recovery notification"},"error_escalated":{"title":"{hostname}: Escalated to {severity} - {category}{entity_suffix}","body":"{reason}","label":"Health issue escalated"},"health_degraded":{"title":"{title_or_default}","body":"{reason}","label":"Health check degraded"},"lxc_updates_available":{"title":"{hostname}: {count} LXC(s) with package updates available","body":"📊 {count} LXC(s) with pending package updates (📦 {total_packages} total, 🔒 {security_count} security):\n\n{ct_list}","label":"LXC updates available (experimental)"},"lxc_update_applied":{"title":"{hostname}: LXC {ct_name} ({vmid}) update {result}","body":"{details}","label":"LXC update applied"},"app_update_available":{"title":"{hostname}: {app_name} update available on CT {vmid}","body":"{app_name} on CT {vmid} ({ct_name}) has a new version:\n {installed} → {latest}","label":"App update available"},"docker_stack_update_available":{"title":"{hostname}: Docker updates available on CT {vmid}","body":"Container {ct_name} (CT {vmid}) has {count} Docker update(s):\n{details}","label":"Docker updates available"},"vm_start":{"title":"{hostname}: VM {vmname} ({vmid}) started","body":"Virtual machine {vmname} (ID: {vmid}) is now running.","label":"VM started"},"vm_start_warning":{"title":"{hostname}: VM {vmname} ({vmid}) started with warnings","body":"Virtual machine {vmname} (ID: {vmid}) started successfully but has warnings.\nWarnings: {reason}","label":"VM started (warnings)"},"vm_stop":{"title":"{hostname}: VM {vmname} ({vmid}) stopped","body":"Virtual machine {vmname} (ID: {vmid}) has been stopped.","label":"VM stopped"},"vm_shutdown":{"title":"{hostname}: VM {vmname} ({vmid}) shut down","body":"Virtual machine {vmname} (ID: {vmid}) has been cleanly shut down.","label":"VM shutdown"},"vm_fail":{"title":"{hostname}: VM {vmname} ({vmid}) FAILED","body":"Virtual machine {vmname} (ID: {vmid}) has crashed or failed to start.\nReason: {reason}","label":"VM FAILED"},"vm_restart":{"title":"{hostname}: VM {vmname} ({vmid}) restarted","body":"Virtual machine {vmname} (ID: {vmid}) has been restarted.","label":"VM restarted"},"ct_start":{"title":"{hostname}: CT {vmname} ({vmid}) started","body":"Container {vmname} (ID: {vmid}) is now running.","label":"CT started"},"ct_start_warning":{"title":"{hostname}: CT {vmname} ({vmid}) started with warnings","body":"Container {vmname} (ID: {vmid}) started successfully but has warnings.\nWarnings: {reason}","label":"CT started (warnings)"},"ct_stop":{"title":"{hostname}: CT {vmname} ({vmid}) stopped","body":"Container {vmname} (ID: {vmid}) has been stopped.","label":"CT stopped"},"ct_shutdown":{"title":"{hostname}: CT {vmname} ({vmid}) shut down","body":"Container {vmname} (ID: {vmid}) has been cleanly shut down.","label":"CT shutdown"},"ct_restart":{"title":"{hostname}: CT {vmname} ({vmid}) restarted","body":"Container {vmname} (ID: {vmid}) has been restarted.","label":"CT restarted"},"ct_fail":{"title":"{hostname}: CT {vmname} ({vmid}) FAILED","body":"Container {vmname} (ID: {vmid}) has crashed or failed to start.\nReason: {reason}","label":"CT FAILED"},"migration_start":{"title":"{hostname}: Migration started — {vmname} ({vmid})","body":"Live migration of {vmname} (ID: {vmid}) to node {target_node} has started.","label":"Migration started"},"migration_complete":{"title":"{hostname}: Migration complete — {vmname} ({vmid})","body":"{vmname} (ID: {vmid}) successfully migrated to node {target_node}.","label":"Migration complete"},"migration_warning":{"title":"{hostname}: Migration complete with warnings — {vmname} ({vmid})","body":"{vmname} (ID: {vmid}) migrated to node {target_node} but encountered warnings.\nWarnings: {reason}","label":"Migration (warnings)"},"migration_fail":{"title":"{hostname}: Migration FAILED — {vmname} ({vmid})","body":"Migration of {vmname} (ID: {vmid}) to node {target_node} failed.\nReason: {reason}","label":"Migration FAILED"},"replication_fail":{"title":"{hostname}: Replication FAILED — {vmname} ({vmid})","body":"Replication of {vmname} (ID: {vmid}) failed.\nReason: {reason}","label":"Replication FAILED"},"replication_complete":{"title":"{hostname}: Replication complete — {vmname} ({vmid})","body":"Replication of {vmname} (ID: {vmid}) completed successfully.","label":"Replication complete"},"backup_start":{"title":"{hostname}: Backup started on {storage}","body":"Backup job started on storage {storage}.\n{reason}","label":"Backup started"},"backup_complete":{"title":"{hostname} → {storage}: Backup complete — {vmname} ({vmid})","body":"Backup of {vmname} (ID: {vmid}) completed successfully on {storage}.\nSize: {size}","label":"Backup complete"},"backup_warning":{"title":"{hostname} → {storage}: Backup complete with warnings — {vmname} ({vmid})","body":"Backup of {vmname} (ID: {vmid}) on {storage} completed but encountered warnings.\nWarnings: {reason}","label":"Backup (warnings)"},"backup_fail":{"title":"{hostname} → {storage}: Backup FAILED — {vmname} ({vmid})","body":"Backup of {vmname} (ID: {vmid}) failed on {storage}.\nReason: {reason}","label":"Backup FAILED"},"host_backup_start":{"title":"{hostname}: Host backup started → {backend_label}","body":"Job: {job_id}\nBackend: {backend_label}\nDestination: {destination}\nProfile: {profile_mode}","label":"Host backup started"},"host_backup_complete":{"title":"{hostname}: Host backup complete → {backend_label}","body":"Job: {job_id}\nBackend: {backend_label}\nDestination: {destination}\nData size: {data_size}\nArchive size: {archive_size}\nDuration: {duration}","label":"Host backup complete"},"host_backup_fail":{"title":"{hostname}: Host backup FAILED → {backend_label}","body":"Job: {job_id}\nBackend: {backend_label}\nDestination: {destination}\nDuration before failure: {duration}\nReason: {reason}\nLog: {log_file}","label":"Host backup FAILED"},"snapshot_complete":{"title":"{hostname}: Snapshot created — {vmname} ({vmid})","body":"Snapshot \"{snapshot_name}\" created for {vmname} (ID: {vmid}).","label":"Snapshot created"},"snapshot_fail":{"title":"{hostname}: Snapshot FAILED — {vmname} ({vmid})","body":"Snapshot creation for {vmname} (ID: {vmid}) failed.\nReason: {reason}","label":"Snapshot FAILED"},"cpu_high":{"title":"{hostname}: High CPU usage — {value}%","body":"CPU usage has reached {value}% on {cores} cores.\n{details}","label":"High CPU usage"},"ram_high":{"title":"{hostname}: High memory usage — {value}%","body":"Memory usage: {used} / {total} ({value}%).\n{details}","label":"High memory usage"},"temp_high":{"title":"{hostname}: High CPU temperature — {value}°C","body":"CPU temperature has reached {value}°C (threshold: {threshold}°C).\n{details}","label":"High temperature"},"disk_space_low":{"title":"{hostname}: Low disk space on {mount}","body":"Filesystem {mount}: {used}% used ({available} available).\nFree up disk space to avoid service disruption.","label":"Low disk space"},"disk_io_error":{"title":"{hostname}: Disk failure detected — {device}","body":"I/O error or disk failure detected on device {device}.\n{reason}","label":"Disk failure / I/O error"},"storage_unavailable":{"title":"{hostname}: Storage unavailable — {storage_name}","body":"PVE storage \"{storage_name}\" (type: {storage_type}) is not accessible.\nReason: {reason}","label":"Storage unavailable"},"smart_test_complete":{"title":"{hostname}: SMART test completed — {device}","body":"SMART {test_type} test on /dev/{device} has completed.\nResult: {result}\nDuration: {duration}","label":"SMART test completed"},"smart_test_failed":{"title":"{hostname}: SMART test FAILED — {device}","body":"SMART {test_type} test on /dev/{device} has failed.\nResult: {result}\nReason: {reason}","label":"SMART test FAILED"},"gpu_mode_switch":{"title":"{hostname}: GPU mode changed to {new_mode}","body":"GPU passthrough mode has been switched.\nGPU: {gpu_name} ({gpu_pci})\nPrevious mode: {old_mode}\nNew mode: {new_mode}\n{details}","label":"GPU mode switched"},"gpu_passthrough_blocked":{"title":"{hostname}: {guest_type} {guest_id} blocked at startup","body":"PCIe passthrough guard prevented {guest_type} {guest_id} ({guest_name}) from starting.\nReason: {reason}\n{details}","label":"GPU passthrough blocked"},"pci_passthrough_conflict":{"title":"{hostname}: PCIe device conflict detected — {device_pci}","body":"A PCIe device is assigned to multiple guests.\nDevice: {device_pci}\nConflicting guests: {guest_list}\nAction required: Stop one of the guests or reassign the device.","label":"PCIe device conflict"},"load_high":{"title":"{hostname}: High system load — {value}","body":"System load average is {value} on {cores} cores.\n{details}","label":"High system load"},"network_down":{"title":"{hostname}: Network connectivity lost{entity_suffix}","body":"The node has lost network connectivity.\nReason: {reason}","label":"Network connectivity lost"},"network_latency":{"title":"{hostname}: High network latency — {value}ms","body":"Latency to gateway: {value}ms (threshold: {threshold}ms).\nThis may indicate network congestion or hardware issues.","label":"High network latency"},"auth_fail":{"title":"{hostname}: Authentication failure","body":"Failed login attempt detected.\nSource IP: {source_ip}\nUser: {username}\nService: {service}","label":"Authentication failure"},"ip_block":{"title":"{hostname}: IP blocked by Fail2Ban","body":"IP address {source_ip} has been banned.\nJail: {jail}\nFailed attempts: {failures}","label":"IP blocked by Fail2Ban"},"firewall_issue":{"title":"{hostname}: Firewall issue detected{entity_suffix}","body":"A firewall configuration issue has been detected.\nReason: {reason}","label":"Firewall issue detected"},"user_permission_change":{"title":"{hostname}: User permission changed","body":"User: {username}\nChange: {change_details}","label":"User permission changed"},"split_brain":{"title":"{hostname}: SPLIT-BRAIN detected","body":"A cluster split-brain condition has been detected. Quorum may be lost.\nQuorum status: {quorum}","label":"SPLIT-BRAIN detected"},"node_disconnect":{"title":"{hostname}: Node {node_name} disconnected","body":"Node {node_name} has disconnected from the cluster.","label":"Node disconnected"},"node_reconnect":{"title":"{hostname}: Node {node_name} reconnected","body":"Node {node_name} has rejoined the cluster successfully.","label":"Node reconnected"},"system_startup":{"title":"{hostname}: {reason}","body":"{summary}","label":"System startup report"},"system_shutdown":{"title":"{hostname}: System shutting down","body":"The node is shutting down.\n{reason}","label":"System shutting down"},"system_reboot":{"title":"{hostname}: System rebooting","body":"The node is rebooting.\n{reason}","label":"System rebooting"},"system_restore_completed":{"title":"{hostname}: Host restore finished","body":"Post-restore tasks completed in background.\n\nGuests applied: {guests}\nBind-mount stubs: {stubs}\nStale node dirs removed: {stale_nodes}\nComponents reinstalled: {components}\nDuration: {duration}\n{warnings_block}\nThe node is now fully ready to use.","label":"Host restore completed"},"system_problem":{"title":"{hostname}: System problem detected{entity_suffix}","body":"A system-level problem has been detected.\nReason: {reason}","label":"System problem detected"},"service_fail":{"title":"{hostname}: Service failed — {service_name}","body":"System service \"{service_name}\" has failed.\nReason: {reason}","label":"Service failed"},"oom_kill":{"title":"{hostname}: OOM Kill — {process}","body":"Process \"{process}\" was killed by the Out-of-Memory manager.\n{reason}","label":"Out of memory kill"},"service_fail_batch":{"title":"{hostname}: {service_count} services failed{entity_suffix}","body":"{reason}","label":"Service fail batch"},"cron_output":{"title":"{hostname}: {pve_title}","body":"{reason}","label":"Cron job output (per-cron stdout via mail)"},"system_mail":{"title":"{hostname}: {pve_title}","body":"{reason}","label":"Smartd / mail bounces (PVE system mail)"},"apt_listchanges":{"title":"{hostname}: {pve_title}","body":"Upstream package information forwarded by Proxmox VE through apt-listchanges. The following text comes from the package maintainer and is not a ProxMenux recommendation.\n\n{reason}","label":"apt-listchanges package notices"},"webhook_test":{"title":"{hostname}: Webhook test received","body":"PVE webhook connectivity test successful.\n{reason}","label":"Webhook test"},"update_available":{"title":"{hostname}: Updates available","body":"Total updates: {total_count}\nSecurity: {security_count}\nProxmox: {pve_count}\nKernel: {kernel_count}\nImportant packages:\n{important_list}","label":"Updates available (legacy)"},"unknown_persistent":{"title":"{hostname}: Check unavailable - {category}{entity_suffix}","body":"Health check for {category} has been unavailable for 3+ cycles.\n{reason}","label":"Check unavailable"},"health_persistent":{"title":"{hostname}: {count} active health issue(s){entity_suffix}","body":"The following health issues remain unresolved:\n{issue_list}\n\nThis digest is sent once every 24 hours while issues persist.","label":"Active health issues (daily)"},"health_issue_new":{"title":"{hostname}: New health issue — {category}{entity_suffix}","body":"New {severity} issue detected in: {category}\nDetails: {reason}","label":"New health issue"},"health_issue_resolved":{"title":"{hostname}: Resolved - {category}{entity_suffix}","body":"{category} issue has been resolved.\n{reason}\nDuration: {duration}","label":"Health issue resolved"},"update_summary":{"title":"{hostname}: Updates available","body":"Total updates: {total_count}\nSecurity updates: {security_count}\nProxmox-related updates: {pve_count}\nKernel updates: {kernel_count}\nImportant packages:\n{important_list}","label":"Host package updates"},"pve_update":{"title":"{hostname}: Proxmox VE {new_version} available","body":"A new Proxmox VE release is available.\nCurrent: {current_version}\nNew: {new_version}\n{details}","label":"Proxmox VE update available"},"update_complete":{"title":"{hostname}: System update completed","body":"System packages have been successfully updated.\n{details}","label":"Host update completed"},"ai_model_migrated":{"title":"{hostname}: AI model updated","body":"The AI model for notifications has been automatically updated.\nProvider: {provider}\nPrevious model: {old_model}\nNew model: {new_model}\n\n{message}","label":"AI model auto-updated"},"proxmenux_update":{"title":"{hostname}: ProxMenux {new_version} available","body":"A new version of ProxMenux is available.\nCurrent: {current_version}\nNew: {new_version}","label":"ProxMenux update available"},"mount_stale":{"title":"{hostname}: stale remote mount {mount_target}","body":"Remote mount {mount_target} ({fstype}) from {mount_source} is stale{lxc_scope}.\nStat timed out or returned an error: {error}\n\nApps writing to this path will silently land on the underlying filesystem and may fill the disk. Remount or fix connectivity ASAP.","label":"Remote mount stale"},"mount_readonly":{"title":"{hostname}: remote mount {mount_target} is read-only","body":"Remote mount {mount_target} ({fstype}) from {mount_source} is mounted read-only{lxc_scope}. Writes will fail. If this was unintentional, remount with rw.","label":"Remote mount read-only"},"lxc_disk_low":{"title":"{hostname}: CT {vmid} rootfs at {usage_percent}%","body":"CT {vmid} ({name}) rootfs is at {usage_percent}% ({disk_bytes_human} / {maxdisk_bytes_human}).\n\nA full LXC rootfs prevents the container from booting cleanly. Either expand the rootfs (pct resize {vmid} rootfs +1G) or free space inside the container.","label":"LXC rootfs near full"},"vm_disk_low":{"title":"{hostname}: VM {vmid} filesystems at {usage_percent}%","body":"VM {vmid} ({name}) guest filesystems are at {usage_percent}% ({disk_bytes_human} / {maxdisk_bytes_human}).\n\nReported by the QEMU guest agent. Includes every persistent filesystem the guest mounts on a block device — virtual disks and PCI-passthrough drives alike. Free up space inside the guest or expand the affected storage before writes start to fail.","label":"VM filesystems near full"},"lxc_mount_low":{"title":"{hostname}: CT {vmid} mount {mount} at {usage_percent}%","body":"Mount {mount} inside CT {vmid} ({name}) is at {usage_percent}% used.\nFilesystem type: {fstype}\n\nA full mount inside a container often blocks the application silently — writes either fail or, worse, land on the rootfs and trigger the rootfs alert next. Free up space on the mount or expand it.","label":"LXC mount near full"},"pve_storage_full":{"title":"{hostname}: PVE storage {storage_name} at {usage_percent}%","body":"Proxmox storage \"{storage_name}\" (type: {storage_type}) is at {usage_percent}% used.\n\nOnce full, no new VM/CT can be provisioned and existing guests may fail to write. Move/delete unused volumes or expand the underlying pool/LV/RBD image.","label":"PVE storage near full"},"zfs_pool_full":{"title":"{hostname}: ZFS pool {pool_name} at {usage_percent}%","body":"ZFS pool \"{pool_name}\" is at {usage_percent}% capacity.\n\nZFS performance and write reliability degrade sharply above ~80% capacity (CoW needs free space for new blocks). Free up snapshots, prune old datasets, or add more vdevs to the pool.","label":"ZFS pool near full"},"post_install_update":{"title":"{hostname}: {count} ProxMenux optimization update(s) available","body":"{count} ProxMenux optimization update(s) available on this host.\n\n🛠️ Available versions:\n{tool_list}\n\n💡 Apply from:\n • ProxMenux Monitor → Settings → ProxMenux Optimizations\n • Or run the post-install menu (option 2) → \"Apply available updates\"","label":"ProxMenux optimization updates available"},"secure_gateway_update_available":{"title":"{hostname}: {app_name} update available{update_title_suffix}","body":"{app_name} (managed by ProxMenux) has 📦 {package_count} package update(s) pending in its container.\n{version_line}\n\n💡 Open ProxMenux Monitor > Settings > Secure Gateway and click \"Update\" to apply.\n\n🗂️ Packages:\n{package_list}","label":"Secure Gateway update available"},"nvidia_driver_update_available":{"title":"{hostname}: NVIDIA driver update available — v{latest_version}","body":"A newer maintenance release is available for the installed NVIDIA driver branch.\n🔹 Currently installed: v{current_version}\n🟢 Latest available: v{latest_version}\n\n{upgrade_reason}\n\n💡 To reinstall:\n • From the ProxMenux post-install menu: {menu_label}\n\nReinstalling rebuilds the DKMS module against the running kernel and requires a reboot to load the new driver.","label":"NVIDIA driver update available"},"coral_driver_update_available":{"title":"{hostname}: Coral TPU driver update available — {latest_version}","body":"A newer {variant_label} is available.\n🔹 Currently installed: {current_version}\n🟢 Latest available: {latest_version}\n\n{upgrade_reason}\n\n💡 To reinstall:\n • From the ProxMenux post-install menu: {menu_label}\n\n{reboot_note}","label":"Coral TPU driver update available"},"burst_auth_fail":{"title":"{hostname}: +{count} more auth failures in {window}","body":"+{count} additional authentication failures detected in {window} ({total_count} total).\nSources: {entity_list}","label":"Auth failures burst"},"burst_ip_block":{"title":"{hostname}: Fail2Ban banned +{count} more IPs in {window}","body":"+{count} additional IPs banned by Fail2Ban in {window} ({total_count} total).\nIPs: {entity_list}","label":"IP block burst"},"burst_disk_io":{"title":"{hostname}: +{count} more disk I/O errors on {entity_list}","body":"+{count} additional I/O errors detected in {window} ({total_count} total).\nDevices: {entity_list}","label":"Disk I/O burst"},"burst_cluster":{"title":"{hostname}: Cluster flapping detected (+{count} more changes)","body":"Cluster state changed +{count} more times in {window} ({total_count} total).\nNodes: {entity_list}","label":"Cluster flapping burst"},"burst_service_fail":{"title":"{hostname}: +{count} more services failed in {window}","body":"+{count} additional service failures detected in {window} ({total_count} total).\nThis typically indicates a node reboot or PVE service restart.\n\nAdditional failures:\n{details}","label":"Service fail burst"},"burst_system":{"title":"{hostname}: +{count} more system problems in {window}","body":"+{count} additional system problems detected in {window} ({total_count} total).\n\nAdditional issues:\n{details}","label":"System problems burst"},"burst_generic":{"title":"{hostname}: +{count} more {event_type} events in {window}","body":"+{count} additional events of type {event_type} in {window} ({total_count} total).\n\nAdditional events:\n{details}","label":"Generic burst"},"kernel_warning":{"title":"{hostname}: Kernel diagnostic event detected","body":"The kernel recorded a diagnostic event: {kernel_details}","label":"Kernel warnings and diagnostic traces"}},"lxcUpdate":{"status":{"success":"succeeded","failure":"failed","partial":"completed partially","deferred":"deferred","skipped":"skipped"},"source":{"scheduled":"Scheduled","manual":"Manual"},"sourceLabel":"Source","targets":"Targets","osPending":"OS packages pending: {before} → {after}","osUnverified":"OS packages: update command executed; result not verified","applications":"Applications: {items}","applicationsUnverified":"Applications: updater executed; no tracked version change was observed","dockerEngineChange":"Docker Engine: {before} → {after}","dockerEngineVerified":"Docker Engine: verified at {version}","dockerEngineUnverified":"Docker Engine: update command executed; version not verified","dockerImagesPending":"Docker images pending: {before} → {after}","dockerImagesChanged":"Docker images changed: {images}","deferredTargets":"Deferred targets: {targets}","reason":"Reason: {reason}","restartRequired":"Restart required: {value}","yes":"yes","no":"no","verificationPending":"Verification pending until the container is running","verificationWarning":"Verification warning: {error}","duration":"Duration: {duration}"},"fallback":{"unknownTitle":"{hostname}: {event_type}","healthCheckDegraded":"Health check degraded","none":"none"},"fields":{"vmid":"VM/CT","name":"Name","device":"Device","sourceIp":"Source IP","node":"Node","category":"Category","service":"Service","jail":"Jail","user":"User","count":"Count","window":"Window","affected":"Affected"},"vzdump":{"size":"Size: {value}","duration":"Duration: {value}","file":"File","backups":"{count} backups","failed":"{count} failed","total":"Total: {value}","time":"Time: {value}"},"startup":{"issuesTitle":"{hostname}: System startup - {count} issue(s) detected","completeTitle":"{hostname}: System startup completed","operational":"All systems operational.","vmCountOne":"{count} VM","vmCountMany":"{count} VMs","ctCountOne":"{count} CT","ctCountMany":"{count} CTs","started":"✅ {counts} started","vmFailed":"❌ VM failed: {name} - {reason}","ctFailed":"❌ CT failed: {name} - {reason}","unknownError":"unknown error","storageUnavailable":"⚠️ Storage: {count} unavailable ({names})","servicesFailed":"⚠️ Services: {count} failed ({names})","duration":"⏱️ Startup completed in {minutes} min"},"appUpdates":{"singleTitle":"{hostname}: {app_name} update available on CT {vmid}","singleBody":"{app_name} on CT {vmid} ({ct_name}) has a new version:\n {installed} → {latest}","emptyTitle":"{hostname}: Application updates available","emptyBody":"Application updates are available.","batchTitle":"{hostname}: {count} application updates available","batchLeadOneContainer":"{count} applications in {container_count} LXC container have a newer version:","batchLeadManyContainers":"{count} applications in {container_count} LXC containers have a newer version:","additional":"… {count} additional application(s)","app":"app","unknown":"unknown"},"healthDegraded":{"categories":{"cpu":"CPU usage and temperature","memory":"Memory and swap","storage":"Storage mounts and space","disks":"Disk I/O and errors","network":"Network interfaces","vms":"VMs and containers","services":"PVE services","logs":"System logs","updates":"System updates","security":"Security"},"severity":{"critical":"Critical","warning":"Warning"},"singleTitle":"{hostname}: {severity} health status – {category}","multipleTitle":"{hostname}: {count} health checks degraded","multipleLine":"• {severity}: {category}\n {reason}","reasons":{"cpuSustained":"CPU is above {threshold}% for {duration}s."}},"digest":{"title":"{hostname}: 24h summary ({timestamp})","readFailed":"digest read failed: {error}","empty":"No INFO events buffered for this digest window.","lead":"{count} INFO events grouped by category:\n","more":" • … and {count} more","footer":"(Critical/Warning events arrived at the time they happened, not in this digest.)","quietTitle":"{hostname}: {count} events buffered during Quiet Hours","groups":{"vm_ct":"VM a CT","backup":"Backup","resources":"Resources","storage":"Storage","network":"Network","security":"Security","cluster":"Cluster","services":"Services","health":"Health","updates":"Updates","hardware":"Hardware","system":"System","other":"Other"}},"test":{"title":"ProxMenux Test","welcome":"Welcome to ProxMenux Monitor!","verify":"This is a test message to verify your notification channel is working correctly.","configuration":"Channel configuration:","iconsEnabled":"Icons: enabled","iconsDisabled":"Icons: disabled","aiEnabled":"AI: enabled ({info})","aiDisabled":"AI: disabled","alerts":"You will receive alerts about system events, health status changes, and security incidents.","photoCaption":"You can use this image as the profile photo for your notification bot."},"channels":{"discord":{"category":"Category","host":"Host","severity":"Severity"},"email":{"report":"{group} Report","details":"Details","host":"Host","footer":"ProxMenux Notification Service","severity":{"critical":"Critical","warning":"Warning","info":"Information","ok":"Resolved","default":"Notice"},"groups":{"vm_ct":"Virtual Machine / Container","backup":"Backup & Snapshot","resources":"System Resources","storage":"Storage","network":"Network","security":"Security","cluster":"Cluster","services":"System Services","health":"Health Monitor","updates":"System Updates","system":"System","hardware":"Hardware","other":"System Notification"},"fields":{"vmCtId":"VM/CT ID","name":"Name","action":"Action","targetNode":"Target Node","reason":"Reason","storage":"Storage","status":"Status","size":"Size","duration":"Duration","snapshot":"Snapshot","metric":"Metric","currentValue":"Current Value","threshold":"Threshold","cpuCores":"CPU Cores","memory":"Memory","temperature":"Temperature","mountPoint":"Mount Point","usage":"Usage","available":"Available","device":"Device","severity":"Severity","storageName":"Storage Name","type":"Type","interface":"Interface","latency":"Latency","event":"Event","sourceIp":"Source IP","username":"Username","service":"Service","jail":"Jail","failures":"Failures","change":"Change","node":"Node","quorum":"Quorum","nodesAffected":"Nodes Affected","process":"Process","category":"Category","previousSeverity":"Previous Severity","activeIssues":"Active Issues","totalUpdates":"Total Updates","securityUpdates":"Security Updates","proxmoxUpdates":"Proxmox Updates","kernelUpdates":"Kernel Updates","importantPackages":"Important Packages","currentVersion":"Current Version","newVersion":"New Version"},"status":{"failed":"Failed","completed":"Completed","started":"Started"}}}}}
}
diff --git a/AppImage/messages/en/common.json b/AppImage/messages/en/common.json
index 51ca198d..1ac6db9f 100644
--- a/AppImage/messages/en/common.json
+++ b/AppImage/messages/en/common.json
@@ -1819,6 +1819,12 @@
"updates": {
"label": "Updates"
},
+ "hardware": {
+ "label": "Hardware"
+ },
+ "system": {
+ "label": "System"
+ },
"other": {
"label": "Other"
}
@@ -1830,6 +1836,8 @@
"error_escalated": "Health issue escalated",
"health_degraded": "Health check degraded",
"lxc_updates_available": "LXC updates available (experimental)",
+ "lxc_update_applied": "LXC update applied",
+ "docker_stack_update_available": "Docker updates available",
"vm_start": "VM started",
"vm_start_warning": "VM started (warnings)",
"vm_stop": "VM stopped",
@@ -1981,7 +1989,10 @@
"systemHostname": "System hostname",
"displayNameHint": "Name shown in notifications. Edit it or leave it empty to use the system hostname.",
"activeFooter": "Notifications are active. Each channel follows its own category and event selection.",
- "inactiveFooter": "Enable notifications to receive system, health and security alerts through the configured channels."
+ "inactiveFooter": "Enable notifications to receive system, health and security alerts through the configured channels.",
+ "notificationLanguage": "Notification language",
+ "selectNotificationLanguage": "Select notification language",
+ "notificationLanguageHint": "Used for deterministic runtime notifications even when AI is disabled."
},
"ai": {
"title": "AI enhancement",
@@ -6083,5 +6094,6 @@
"withoutLynis": "Run without Lynis",
"cancel": "Cancel"
}
- }
+ },
+ "runtime": {"notifications":{"templates":{"state_change":{"title":"{hostname}: {category} changed to {current}{entity_suffix}","body":"{category} status changed from {previous} to {current}.\n{reason}","label":"Health state changed"},"new_error":{"title":"{hostname}: New {severity} - {category}{entity_suffix}","body":"{reason}","label":"New health issue"},"error_resolved":{"title":"{hostname}: Resolved - {category}{entity_suffix}","body":"The {category} issue has been resolved.\n{reason}\n🚦 Previous severity: {original_severity}\n⏱️ Duration: {duration}","label":"Recovery notification"},"error_escalated":{"title":"{hostname}: Escalated to {severity} - {category}{entity_suffix}","body":"{reason}","label":"Health issue escalated"},"health_degraded":{"title":"{title_or_default}","body":"{reason}","label":"Health check degraded"},"lxc_updates_available":{"title":"{hostname}: {count} LXC(s) with package updates available","body":"📊 {count} LXC(s) with pending package updates (📦 {total_packages} total, 🔒 {security_count} security):\n\n{ct_list}","label":"LXC updates available (experimental)"},"lxc_update_applied":{"title":"{hostname}: LXC {ct_name} ({vmid}) update {result}","body":"{details}","label":"LXC update applied"},"app_update_available":{"title":"{hostname}: {app_name} update available on CT {vmid}","body":"{app_name} on CT {vmid} ({ct_name}) has a new version:\n {installed} → {latest}","label":"App update available"},"docker_stack_update_available":{"title":"{hostname}: Docker updates available on CT {vmid}","body":"Container {ct_name} (CT {vmid}) has {count} Docker update(s):\n{details}","label":"Docker updates available"},"vm_start":{"title":"{hostname}: VM {vmname} ({vmid}) started","body":"Virtual machine {vmname} (ID: {vmid}) is now running.","label":"VM started"},"vm_start_warning":{"title":"{hostname}: VM {vmname} ({vmid}) started with warnings","body":"Virtual machine {vmname} (ID: {vmid}) started successfully but has warnings.\nWarnings: {reason}","label":"VM started (warnings)"},"vm_stop":{"title":"{hostname}: VM {vmname} ({vmid}) stopped","body":"Virtual machine {vmname} (ID: {vmid}) has been stopped.","label":"VM stopped"},"vm_shutdown":{"title":"{hostname}: VM {vmname} ({vmid}) shut down","body":"Virtual machine {vmname} (ID: {vmid}) has been cleanly shut down.","label":"VM shutdown"},"vm_fail":{"title":"{hostname}: VM {vmname} ({vmid}) FAILED","body":"Virtual machine {vmname} (ID: {vmid}) has crashed or failed to start.\nReason: {reason}","label":"VM FAILED"},"vm_restart":{"title":"{hostname}: VM {vmname} ({vmid}) restarted","body":"Virtual machine {vmname} (ID: {vmid}) has been restarted.","label":"VM restarted"},"ct_start":{"title":"{hostname}: CT {vmname} ({vmid}) started","body":"Container {vmname} (ID: {vmid}) is now running.","label":"CT started"},"ct_start_warning":{"title":"{hostname}: CT {vmname} ({vmid}) started with warnings","body":"Container {vmname} (ID: {vmid}) started successfully but has warnings.\nWarnings: {reason}","label":"CT started (warnings)"},"ct_stop":{"title":"{hostname}: CT {vmname} ({vmid}) stopped","body":"Container {vmname} (ID: {vmid}) has been stopped.","label":"CT stopped"},"ct_shutdown":{"title":"{hostname}: CT {vmname} ({vmid}) shut down","body":"Container {vmname} (ID: {vmid}) has been cleanly shut down.","label":"CT shutdown"},"ct_restart":{"title":"{hostname}: CT {vmname} ({vmid}) restarted","body":"Container {vmname} (ID: {vmid}) has been restarted.","label":"CT restarted"},"ct_fail":{"title":"{hostname}: CT {vmname} ({vmid}) FAILED","body":"Container {vmname} (ID: {vmid}) has crashed or failed to start.\nReason: {reason}","label":"CT FAILED"},"migration_start":{"title":"{hostname}: Migration started — {vmname} ({vmid})","body":"Live migration of {vmname} (ID: {vmid}) to node {target_node} has started.","label":"Migration started"},"migration_complete":{"title":"{hostname}: Migration complete — {vmname} ({vmid})","body":"{vmname} (ID: {vmid}) successfully migrated to node {target_node}.","label":"Migration complete"},"migration_warning":{"title":"{hostname}: Migration complete with warnings — {vmname} ({vmid})","body":"{vmname} (ID: {vmid}) migrated to node {target_node} but encountered warnings.\nWarnings: {reason}","label":"Migration (warnings)"},"migration_fail":{"title":"{hostname}: Migration FAILED — {vmname} ({vmid})","body":"Migration of {vmname} (ID: {vmid}) to node {target_node} failed.\nReason: {reason}","label":"Migration FAILED"},"replication_fail":{"title":"{hostname}: Replication FAILED — {vmname} ({vmid})","body":"Replication of {vmname} (ID: {vmid}) failed.\nReason: {reason}","label":"Replication FAILED"},"replication_complete":{"title":"{hostname}: Replication complete — {vmname} ({vmid})","body":"Replication of {vmname} (ID: {vmid}) completed successfully.","label":"Replication complete"},"backup_start":{"title":"{hostname}: Backup started on {storage}","body":"Backup job started on storage {storage}.\n{reason}","label":"Backup started"},"backup_complete":{"title":"{hostname} → {storage}: Backup complete — {vmname} ({vmid})","body":"Backup of {vmname} (ID: {vmid}) completed successfully on {storage}.\nSize: {size}","label":"Backup complete"},"backup_warning":{"title":"{hostname} → {storage}: Backup complete with warnings — {vmname} ({vmid})","body":"Backup of {vmname} (ID: {vmid}) on {storage} completed but encountered warnings.\nWarnings: {reason}","label":"Backup (warnings)"},"backup_fail":{"title":"{hostname} → {storage}: Backup FAILED — {vmname} ({vmid})","body":"Backup of {vmname} (ID: {vmid}) failed on {storage}.\nReason: {reason}","label":"Backup FAILED"},"host_backup_start":{"title":"{hostname}: Host backup started → {backend_label}","body":"Job: {job_id}\nBackend: {backend_label}\nDestination: {destination}\nProfile: {profile_mode}","label":"Host backup started"},"host_backup_complete":{"title":"{hostname}: Host backup complete → {backend_label}","body":"Job: {job_id}\nBackend: {backend_label}\nDestination: {destination}\nData size: {data_size}\nArchive size: {archive_size}\nDuration: {duration}","label":"Host backup complete"},"host_backup_fail":{"title":"{hostname}: Host backup FAILED → {backend_label}","body":"Job: {job_id}\nBackend: {backend_label}\nDestination: {destination}\nDuration before failure: {duration}\nReason: {reason}\nLog: {log_file}","label":"Host backup FAILED"},"snapshot_complete":{"title":"{hostname}: Snapshot created — {vmname} ({vmid})","body":"Snapshot \"{snapshot_name}\" created for {vmname} (ID: {vmid}).","label":"Snapshot created"},"snapshot_fail":{"title":"{hostname}: Snapshot FAILED — {vmname} ({vmid})","body":"Snapshot creation for {vmname} (ID: {vmid}) failed.\nReason: {reason}","label":"Snapshot FAILED"},"cpu_high":{"title":"{hostname}: High CPU usage — {value}%","body":"CPU usage has reached {value}% on {cores} cores.\n{details}","label":"High CPU usage"},"ram_high":{"title":"{hostname}: High memory usage — {value}%","body":"Memory usage: {used} / {total} ({value}%).\n{details}","label":"High memory usage"},"temp_high":{"title":"{hostname}: High CPU temperature — {value}°C","body":"CPU temperature has reached {value}°C (threshold: {threshold}°C).\n{details}","label":"High temperature"},"disk_space_low":{"title":"{hostname}: Low disk space on {mount}","body":"Filesystem {mount}: {used}% used ({available} available).\nFree up disk space to avoid service disruption.","label":"Low disk space"},"disk_io_error":{"title":"{hostname}: Disk failure detected — {device}","body":"I/O error or disk failure detected on device {device}.\n{reason}","label":"Disk failure / I/O error"},"storage_unavailable":{"title":"{hostname}: Storage unavailable — {storage_name}","body":"PVE storage \"{storage_name}\" (type: {storage_type}) is not accessible.\nReason: {reason}","label":"Storage unavailable"},"smart_test_complete":{"title":"{hostname}: SMART test completed — {device}","body":"SMART {test_type} test on /dev/{device} has completed.\nResult: {result}\nDuration: {duration}","label":"SMART test completed"},"smart_test_failed":{"title":"{hostname}: SMART test FAILED — {device}","body":"SMART {test_type} test on /dev/{device} has failed.\nResult: {result}\nReason: {reason}","label":"SMART test FAILED"},"gpu_mode_switch":{"title":"{hostname}: GPU mode changed to {new_mode}","body":"GPU passthrough mode has been switched.\nGPU: {gpu_name} ({gpu_pci})\nPrevious mode: {old_mode}\nNew mode: {new_mode}\n{details}","label":"GPU mode switched"},"gpu_passthrough_blocked":{"title":"{hostname}: {guest_type} {guest_id} blocked at startup","body":"PCIe passthrough guard prevented {guest_type} {guest_id} ({guest_name}) from starting.\nReason: {reason}\n{details}","label":"GPU passthrough blocked"},"pci_passthrough_conflict":{"title":"{hostname}: PCIe device conflict detected — {device_pci}","body":"A PCIe device is assigned to multiple guests.\nDevice: {device_pci}\nConflicting guests: {guest_list}\nAction required: Stop one of the guests or reassign the device.","label":"PCIe device conflict"},"load_high":{"title":"{hostname}: High system load — {value}","body":"System load average is {value} on {cores} cores.\n{details}","label":"High system load"},"network_down":{"title":"{hostname}: Network connectivity lost{entity_suffix}","body":"The node has lost network connectivity.\nReason: {reason}","label":"Network connectivity lost"},"network_latency":{"title":"{hostname}: High network latency — {value}ms","body":"Latency to gateway: {value}ms (threshold: {threshold}ms).\nThis may indicate network congestion or hardware issues.","label":"High network latency"},"auth_fail":{"title":"{hostname}: Authentication failure","body":"Failed login attempt detected.\nSource IP: {source_ip}\nUser: {username}\nService: {service}","label":"Authentication failure"},"ip_block":{"title":"{hostname}: IP blocked by Fail2Ban","body":"IP address {source_ip} has been banned.\nJail: {jail}\nFailed attempts: {failures}","label":"IP blocked by Fail2Ban"},"firewall_issue":{"title":"{hostname}: Firewall issue detected{entity_suffix}","body":"A firewall configuration issue has been detected.\nReason: {reason}","label":"Firewall issue detected"},"user_permission_change":{"title":"{hostname}: User permission changed","body":"User: {username}\nChange: {change_details}","label":"User permission changed"},"split_brain":{"title":"{hostname}: SPLIT-BRAIN detected","body":"A cluster split-brain condition has been detected. Quorum may be lost.\nQuorum status: {quorum}","label":"SPLIT-BRAIN detected"},"node_disconnect":{"title":"{hostname}: Node {node_name} disconnected","body":"Node {node_name} has disconnected from the cluster.","label":"Node disconnected"},"node_reconnect":{"title":"{hostname}: Node {node_name} reconnected","body":"Node {node_name} has rejoined the cluster successfully.","label":"Node reconnected"},"system_startup":{"title":"{hostname}: {reason}","body":"{summary}","label":"System startup report"},"system_shutdown":{"title":"{hostname}: System shutting down","body":"The node is shutting down.\n{reason}","label":"System shutting down"},"system_reboot":{"title":"{hostname}: System rebooting","body":"The node is rebooting.\n{reason}","label":"System rebooting"},"system_restore_completed":{"title":"{hostname}: Host restore finished","body":"Post-restore tasks completed in background.\n\nGuests applied: {guests}\nBind-mount stubs: {stubs}\nStale node dirs removed: {stale_nodes}\nComponents reinstalled: {components}\nDuration: {duration}\n{warnings_block}\nThe node is now fully ready to use.","label":"Host restore completed"},"system_problem":{"title":"{hostname}: System problem detected{entity_suffix}","body":"A system-level problem has been detected.\nReason: {reason}","label":"System problem detected"},"service_fail":{"title":"{hostname}: Service failed — {service_name}","body":"System service \"{service_name}\" has failed.\nReason: {reason}","label":"Service failed"},"oom_kill":{"title":"{hostname}: OOM Kill — {process}","body":"Process \"{process}\" was killed by the Out-of-Memory manager.\n{reason}","label":"Out of memory kill"},"service_fail_batch":{"title":"{hostname}: {service_count} services failed{entity_suffix}","body":"{reason}","label":"Service fail batch"},"cron_output":{"title":"{hostname}: {pve_title}","body":"{reason}","label":"Cron job output (per-cron stdout via mail)"},"system_mail":{"title":"{hostname}: {pve_title}","body":"{reason}","label":"Smartd / mail bounces (PVE system mail)"},"apt_listchanges":{"title":"{hostname}: {pve_title}","body":"Upstream package information forwarded by Proxmox VE through apt-listchanges. The following text comes from the package maintainer and is not a ProxMenux recommendation.\n\n{reason}","label":"apt-listchanges package notices"},"webhook_test":{"title":"{hostname}: Webhook test received","body":"PVE webhook connectivity test successful.\n{reason}","label":"Webhook test"},"update_available":{"title":"{hostname}: Updates available","body":"Total updates: {total_count}\nSecurity: {security_count}\nProxmox: {pve_count}\nKernel: {kernel_count}\nImportant packages:\n{important_list}","label":"Updates available (legacy)"},"unknown_persistent":{"title":"{hostname}: Check unavailable - {category}{entity_suffix}","body":"Health check for {category} has been unavailable for 3+ cycles.\n{reason}","label":"Check unavailable"},"health_persistent":{"title":"{hostname}: {count} active health issue(s){entity_suffix}","body":"The following health issues remain unresolved:\n{issue_list}\n\nThis digest is sent once every 24 hours while issues persist.","label":"Active health issues (daily)"},"health_issue_new":{"title":"{hostname}: New health issue — {category}{entity_suffix}","body":"New {severity} issue detected in: {category}\nDetails: {reason}","label":"New health issue"},"health_issue_resolved":{"title":"{hostname}: Resolved - {category}{entity_suffix}","body":"{category} issue has been resolved.\n{reason}\nDuration: {duration}","label":"Health issue resolved"},"update_summary":{"title":"{hostname}: Updates available","body":"Total updates: {total_count}\nSecurity updates: {security_count}\nProxmox-related updates: {pve_count}\nKernel updates: {kernel_count}\nImportant packages:\n{important_list}","label":"Host package updates"},"pve_update":{"title":"{hostname}: Proxmox VE {new_version} available","body":"A new Proxmox VE release is available.\nCurrent: {current_version}\nNew: {new_version}\n{details}","label":"Proxmox VE update available"},"update_complete":{"title":"{hostname}: System update completed","body":"System packages have been successfully updated.\n{details}","label":"Host update completed"},"ai_model_migrated":{"title":"{hostname}: AI model updated","body":"The AI model for notifications has been automatically updated.\nProvider: {provider}\nPrevious model: {old_model}\nNew model: {new_model}\n\n{message}","label":"AI model auto-updated"},"proxmenux_update":{"title":"{hostname}: ProxMenux {new_version} available","body":"A new version of ProxMenux is available.\nCurrent: {current_version}\nNew: {new_version}","label":"ProxMenux update available"},"mount_stale":{"title":"{hostname}: stale remote mount {mount_target}","body":"Remote mount {mount_target} ({fstype}) from {mount_source} is stale{lxc_scope}.\nStat timed out or returned an error: {error}\n\nApps writing to this path will silently land on the underlying filesystem and may fill the disk. Remount or fix connectivity ASAP.","label":"Remote mount stale"},"mount_readonly":{"title":"{hostname}: remote mount {mount_target} is read-only","body":"Remote mount {mount_target} ({fstype}) from {mount_source} is mounted read-only{lxc_scope}. Writes will fail. If this was unintentional, remount with rw.","label":"Remote mount read-only"},"lxc_disk_low":{"title":"{hostname}: CT {vmid} rootfs at {usage_percent}%","body":"CT {vmid} ({name}) rootfs is at {usage_percent}% ({disk_bytes_human} / {maxdisk_bytes_human}).\n\nA full LXC rootfs prevents the container from booting cleanly. Either expand the rootfs (pct resize {vmid} rootfs +1G) or free space inside the container.","label":"LXC rootfs near full"},"vm_disk_low":{"title":"{hostname}: VM {vmid} filesystems at {usage_percent}%","body":"VM {vmid} ({name}) guest filesystems are at {usage_percent}% ({disk_bytes_human} / {maxdisk_bytes_human}).\n\nReported by the QEMU guest agent. Includes every persistent filesystem the guest mounts on a block device — virtual disks and PCI-passthrough drives alike. Free up space inside the guest or expand the affected storage before writes start to fail.","label":"VM filesystems near full"},"lxc_mount_low":{"title":"{hostname}: CT {vmid} mount {mount} at {usage_percent}%","body":"Mount {mount} inside CT {vmid} ({name}) is at {usage_percent}% used.\nFilesystem type: {fstype}\n\nA full mount inside a container often blocks the application silently — writes either fail or, worse, land on the rootfs and trigger the rootfs alert next. Free up space on the mount or expand it.","label":"LXC mount near full"},"pve_storage_full":{"title":"{hostname}: PVE storage {storage_name} at {usage_percent}%","body":"Proxmox storage \"{storage_name}\" (type: {storage_type}) is at {usage_percent}% used.\n\nOnce full, no new VM/CT can be provisioned and existing guests may fail to write. Move/delete unused volumes or expand the underlying pool/LV/RBD image.","label":"PVE storage near full"},"zfs_pool_full":{"title":"{hostname}: ZFS pool {pool_name} at {usage_percent}%","body":"ZFS pool \"{pool_name}\" is at {usage_percent}% capacity.\n\nZFS performance and write reliability degrade sharply above ~80% capacity (CoW needs free space for new blocks). Free up snapshots, prune old datasets, or add more vdevs to the pool.","label":"ZFS pool near full"},"post_install_update":{"title":"{hostname}: {count} ProxMenux optimization update(s) available","body":"{count} ProxMenux optimization update(s) available on this host.\n\n🛠️ Available versions:\n{tool_list}\n\n💡 Apply from:\n • ProxMenux Monitor → Settings → ProxMenux Optimizations\n • Or run the post-install menu (option 2) → \"Apply available updates\"","label":"ProxMenux optimization updates available"},"secure_gateway_update_available":{"title":"{hostname}: {app_name} update available{update_title_suffix}","body":"{app_name} (managed by ProxMenux) has 📦 {package_count} package update(s) pending in its container.\n{version_line}\n\n💡 Open ProxMenux Monitor > Settings > Secure Gateway and click \"Update\" to apply.\n\n🗂️ Packages:\n{package_list}","label":"Secure Gateway update available"},"nvidia_driver_update_available":{"title":"{hostname}: NVIDIA driver update available — v{latest_version}","body":"A newer maintenance release is available for the installed NVIDIA driver branch.\n🔹 Currently installed: v{current_version}\n🟢 Latest available: v{latest_version}\n\n{upgrade_reason}\n\n💡 To reinstall:\n • From the ProxMenux post-install menu: {menu_label}\n\nReinstalling rebuilds the DKMS module against the running kernel and requires a reboot to load the new driver.","label":"NVIDIA driver update available"},"coral_driver_update_available":{"title":"{hostname}: Coral TPU driver update available — {latest_version}","body":"A newer {variant_label} is available.\n🔹 Currently installed: {current_version}\n🟢 Latest available: {latest_version}\n\n{upgrade_reason}\n\n💡 To reinstall:\n • From the ProxMenux post-install menu: {menu_label}\n\n{reboot_note}","label":"Coral TPU driver update available"},"burst_auth_fail":{"title":"{hostname}: +{count} more auth failures in {window}","body":"+{count} additional authentication failures detected in {window} ({total_count} total).\nSources: {entity_list}","label":"Auth failures burst"},"burst_ip_block":{"title":"{hostname}: Fail2Ban banned +{count} more IPs in {window}","body":"+{count} additional IPs banned by Fail2Ban in {window} ({total_count} total).\nIPs: {entity_list}","label":"IP block burst"},"burst_disk_io":{"title":"{hostname}: +{count} more disk I/O errors on {entity_list}","body":"+{count} additional I/O errors detected in {window} ({total_count} total).\nDevices: {entity_list}","label":"Disk I/O burst"},"burst_cluster":{"title":"{hostname}: Cluster flapping detected (+{count} more changes)","body":"Cluster state changed +{count} more times in {window} ({total_count} total).\nNodes: {entity_list}","label":"Cluster flapping burst"},"burst_service_fail":{"title":"{hostname}: +{count} more services failed in {window}","body":"+{count} additional service failures detected in {window} ({total_count} total).\nThis typically indicates a node reboot or PVE service restart.\n\nAdditional failures:\n{details}","label":"Service fail burst"},"burst_system":{"title":"{hostname}: +{count} more system problems in {window}","body":"+{count} additional system problems detected in {window} ({total_count} total).\n\nAdditional issues:\n{details}","label":"System problems burst"},"burst_generic":{"title":"{hostname}: +{count} more {event_type} events in {window}","body":"+{count} additional events of type {event_type} in {window} ({total_count} total).\n\nAdditional events:\n{details}","label":"Generic burst"},"kernel_warning":{"title":"{hostname}: Kernel diagnostic event detected","body":"The kernel recorded a diagnostic event: {kernel_details}","label":"Kernel warnings and diagnostic traces"}},"lxcUpdate":{"status":{"success":"succeeded","failure":"failed","partial":"completed partially","deferred":"deferred","skipped":"skipped"},"source":{"scheduled":"Scheduled","manual":"Manual"},"sourceLabel":"Source","targets":"Targets","osPending":"OS packages pending: {before} → {after}","osUnverified":"OS packages: update command executed; result not verified","applications":"Applications: {items}","applicationsUnverified":"Applications: updater executed; no tracked version change was observed","dockerEngineChange":"Docker Engine: {before} → {after}","dockerEngineVerified":"Docker Engine: verified at {version}","dockerEngineUnverified":"Docker Engine: update command executed; version not verified","dockerImagesPending":"Docker images pending: {before} → {after}","dockerImagesChanged":"Docker images changed: {images}","deferredTargets":"Deferred targets: {targets}","reason":"Reason: {reason}","restartRequired":"Restart required: {value}","yes":"yes","no":"no","verificationPending":"Verification pending until the container is running","verificationWarning":"Verification warning: {error}","duration":"Duration: {duration}"},"fallback":{"unknownTitle":"{hostname}: {event_type}","healthCheckDegraded":"Health check degraded","none":"none"},"fields":{"vmid":"VM/CT","name":"Name","device":"Device","sourceIp":"Source IP","node":"Node","category":"Category","service":"Service","jail":"Jail","user":"User","count":"Count","window":"Window","affected":"Affected"},"vzdump":{"size":"Size: {value}","duration":"Duration: {value}","file":"File","backups":"{count} backups","failed":"{count} failed","total":"Total: {value}","time":"Time: {value}"},"startup":{"issuesTitle":"{hostname}: System startup - {count} issue(s) detected","completeTitle":"{hostname}: System startup completed","operational":"All systems operational.","vmCountOne":"{count} VM","vmCountMany":"{count} VMs","ctCountOne":"{count} CT","ctCountMany":"{count} CTs","started":"✅ {counts} started","vmFailed":"❌ VM failed: {name} - {reason}","ctFailed":"❌ CT failed: {name} - {reason}","unknownError":"unknown error","storageUnavailable":"⚠️ Storage: {count} unavailable ({names})","servicesFailed":"⚠️ Services: {count} failed ({names})","duration":"⏱️ Startup completed in {minutes} min"},"appUpdates":{"singleTitle":"{hostname}: {app_name} update available on CT {vmid}","singleBody":"{app_name} on CT {vmid} ({ct_name}) has a new version:\n {installed} → {latest}","emptyTitle":"{hostname}: Application updates available","emptyBody":"Application updates are available.","batchTitle":"{hostname}: {count} application updates available","batchLeadOneContainer":"{count} applications in {container_count} LXC container have a newer version:","batchLeadManyContainers":"{count} applications in {container_count} LXC containers have a newer version:","additional":"… {count} additional application(s)","app":"app","unknown":"unknown"},"healthDegraded":{"categories":{"cpu":"CPU usage and temperature","memory":"Memory and swap","storage":"Storage mounts and space","disks":"Disk I/O and errors","network":"Network interfaces","vms":"VMs and containers","services":"PVE services","logs":"System logs","updates":"System updates","security":"Security"},"severity":{"critical":"Critical","warning":"Warning"},"singleTitle":"{hostname}: {severity} health status – {category}","multipleTitle":"{hostname}: {count} health checks degraded","multipleLine":"• {severity}: {category}\n {reason}","reasons":{"cpuSustained":"CPU is above {threshold}% for {duration}s."}},"digest":{"title":"{hostname}: 24h summary ({timestamp})","readFailed":"digest read failed: {error}","empty":"No INFO events buffered for this digest window.","lead":"{count} INFO events grouped by category:\n","more":" • … and {count} more","footer":"(Critical/Warning events arrived at the time they happened, not in this digest.)","quietTitle":"{hostname}: {count} events buffered during Quiet Hours","groups":{"vm_ct":"VM a CT","backup":"Backup","resources":"Resources","storage":"Storage","network":"Network","security":"Security","cluster":"Cluster","services":"Services","health":"Health","updates":"Updates","hardware":"Hardware","system":"System","other":"Other"}},"test":{"title":"ProxMenux Test","welcome":"Welcome to ProxMenux Monitor!","verify":"This is a test message to verify your notification channel is working correctly.","configuration":"Channel configuration:","iconsEnabled":"Icons: enabled","iconsDisabled":"Icons: disabled","aiEnabled":"AI: enabled ({info})","aiDisabled":"AI: disabled","alerts":"You will receive alerts about system events, health status changes, and security incidents.","photoCaption":"You can use this image as the profile photo for your notification bot."},"channels":{"discord":{"category":"Category","host":"Host","severity":"Severity"},"email":{"report":"{group} Report","details":"Details","host":"Host","footer":"ProxMenux Notification Service","severity":{"critical":"Critical","warning":"Warning","info":"Information","ok":"Resolved","default":"Notice"},"groups":{"vm_ct":"Virtual Machine / Container","backup":"Backup & Snapshot","resources":"System Resources","storage":"Storage","network":"Network","security":"Security","cluster":"Cluster","services":"System Services","health":"Health Monitor","updates":"System Updates","system":"System","hardware":"Hardware","other":"System Notification"},"fields":{"vmCtId":"VM/CT ID","name":"Name","action":"Action","targetNode":"Target Node","reason":"Reason","storage":"Storage","status":"Status","size":"Size","duration":"Duration","snapshot":"Snapshot","metric":"Metric","currentValue":"Current Value","threshold":"Threshold","cpuCores":"CPU Cores","memory":"Memory","temperature":"Temperature","mountPoint":"Mount Point","usage":"Usage","available":"Available","device":"Device","severity":"Severity","storageName":"Storage Name","type":"Type","interface":"Interface","latency":"Latency","event":"Event","sourceIp":"Source IP","username":"Username","service":"Service","jail":"Jail","failures":"Failures","change":"Change","node":"Node","quorum":"Quorum","nodesAffected":"Nodes Affected","process":"Process","category":"Category","previousSeverity":"Previous Severity","activeIssues":"Active Issues","totalUpdates":"Total Updates","securityUpdates":"Security Updates","proxmoxUpdates":"Proxmox Updates","kernelUpdates":"Kernel Updates","importantPackages":"Important Packages","currentVersion":"Current Version","newVersion":"New Version"},"status":{"failed":"Failed","completed":"Completed","started":"Started"}}}}}
}
diff --git a/AppImage/messages/es/common.json b/AppImage/messages/es/common.json
index d4e10417..982e1deb 100644
--- a/AppImage/messages/es/common.json
+++ b/AppImage/messages/es/common.json
@@ -1820,6 +1820,12 @@
"updates": {
"label": "Actualizaciones"
},
+ "hardware": {
+ "label": "Hardware"
+ },
+ "system": {
+ "label": "System"
+ },
"other": {
"label": "Otro"
}
@@ -1831,6 +1837,8 @@
"error_escalated": "El problema de salud se agravó",
"health_degraded": "Control de salud degradado",
"lxc_updates_available": "Actualizaciones de LXC disponibles (experimentales)",
+ "lxc_update_applied": "LXC update applied",
+ "docker_stack_update_available": "Docker updates available",
"vm_start": "VM iniciada",
"vm_start_warning": "VM iniciada (advertencias)",
"vm_stop": "VM detenida",
@@ -1982,7 +1990,10 @@
"systemHostname": "Nombre de host del sistema",
"displayNameHint": "Nombre que se muestra en las notificaciones. Edítelo o déjelo vacío para usar el nombre de host del sistema.",
"activeFooter": "Las notificaciones están activas. Cada canal sigue su propia categoría y selección de eventos.",
- "inactiveFooter": "Habilite notificaciones para recibir alertas del sistema, de salud y de seguridad a través de los canales configurados."
+ "inactiveFooter": "Habilite notificaciones para recibir alertas del sistema, de salud y de seguridad a través de los canales configurados.",
+ "notificationLanguage": "Notification language",
+ "selectNotificationLanguage": "Select notification language",
+ "notificationLanguageHint": "Used for deterministic runtime notifications even when AI is disabled."
},
"ai": {
"title": "mejora de la IA",
@@ -6018,5 +6029,6 @@
"withoutLynis": "Ejecutar sin Lynis",
"cancel": "Cancelar"
}
- }
+ },
+ "runtime": {"notifications":{"templates":{"state_change":{"title":"{hostname}: {category} changed to {current}{entity_suffix}","body":"{category} status changed from {previous} to {current}.\n{reason}","label":"Health state changed"},"new_error":{"title":"{hostname}: New {severity} - {category}{entity_suffix}","body":"{reason}","label":"New health issue"},"error_resolved":{"title":"{hostname}: Resolved - {category}{entity_suffix}","body":"The {category} issue has been resolved.\n{reason}\n🚦 Previous severity: {original_severity}\n⏱️ Duration: {duration}","label":"Recovery notification"},"error_escalated":{"title":"{hostname}: Escalated to {severity} - {category}{entity_suffix}","body":"{reason}","label":"Health issue escalated"},"health_degraded":{"title":"{title_or_default}","body":"{reason}","label":"Health check degraded"},"lxc_updates_available":{"title":"{hostname}: {count} LXC(s) with package updates available","body":"📊 {count} LXC(s) with pending package updates (📦 {total_packages} total, 🔒 {security_count} security):\n\n{ct_list}","label":"LXC updates available (experimental)"},"lxc_update_applied":{"title":"{hostname}: LXC {ct_name} ({vmid}) update {result}","body":"{details}","label":"LXC update applied"},"app_update_available":{"title":"{hostname}: {app_name} update available on CT {vmid}","body":"{app_name} on CT {vmid} ({ct_name}) has a new version:\n {installed} → {latest}","label":"App update available"},"docker_stack_update_available":{"title":"{hostname}: Docker updates available on CT {vmid}","body":"Container {ct_name} (CT {vmid}) has {count} Docker update(s):\n{details}","label":"Docker updates available"},"vm_start":{"title":"{hostname}: VM {vmname} ({vmid}) started","body":"Virtual machine {vmname} (ID: {vmid}) is now running.","label":"VM started"},"vm_start_warning":{"title":"{hostname}: VM {vmname} ({vmid}) started with warnings","body":"Virtual machine {vmname} (ID: {vmid}) started successfully but has warnings.\nWarnings: {reason}","label":"VM started (warnings)"},"vm_stop":{"title":"{hostname}: VM {vmname} ({vmid}) stopped","body":"Virtual machine {vmname} (ID: {vmid}) has been stopped.","label":"VM stopped"},"vm_shutdown":{"title":"{hostname}: VM {vmname} ({vmid}) shut down","body":"Virtual machine {vmname} (ID: {vmid}) has been cleanly shut down.","label":"VM shutdown"},"vm_fail":{"title":"{hostname}: VM {vmname} ({vmid}) FAILED","body":"Virtual machine {vmname} (ID: {vmid}) has crashed or failed to start.\nReason: {reason}","label":"VM FAILED"},"vm_restart":{"title":"{hostname}: VM {vmname} ({vmid}) restarted","body":"Virtual machine {vmname} (ID: {vmid}) has been restarted.","label":"VM restarted"},"ct_start":{"title":"{hostname}: CT {vmname} ({vmid}) started","body":"Container {vmname} (ID: {vmid}) is now running.","label":"CT started"},"ct_start_warning":{"title":"{hostname}: CT {vmname} ({vmid}) started with warnings","body":"Container {vmname} (ID: {vmid}) started successfully but has warnings.\nWarnings: {reason}","label":"CT started (warnings)"},"ct_stop":{"title":"{hostname}: CT {vmname} ({vmid}) stopped","body":"Container {vmname} (ID: {vmid}) has been stopped.","label":"CT stopped"},"ct_shutdown":{"title":"{hostname}: CT {vmname} ({vmid}) shut down","body":"Container {vmname} (ID: {vmid}) has been cleanly shut down.","label":"CT shutdown"},"ct_restart":{"title":"{hostname}: CT {vmname} ({vmid}) restarted","body":"Container {vmname} (ID: {vmid}) has been restarted.","label":"CT restarted"},"ct_fail":{"title":"{hostname}: CT {vmname} ({vmid}) FAILED","body":"Container {vmname} (ID: {vmid}) has crashed or failed to start.\nReason: {reason}","label":"CT FAILED"},"migration_start":{"title":"{hostname}: Migration started — {vmname} ({vmid})","body":"Live migration of {vmname} (ID: {vmid}) to node {target_node} has started.","label":"Migration started"},"migration_complete":{"title":"{hostname}: Migration complete — {vmname} ({vmid})","body":"{vmname} (ID: {vmid}) successfully migrated to node {target_node}.","label":"Migration complete"},"migration_warning":{"title":"{hostname}: Migration complete with warnings — {vmname} ({vmid})","body":"{vmname} (ID: {vmid}) migrated to node {target_node} but encountered warnings.\nWarnings: {reason}","label":"Migration (warnings)"},"migration_fail":{"title":"{hostname}: Migration FAILED — {vmname} ({vmid})","body":"Migration of {vmname} (ID: {vmid}) to node {target_node} failed.\nReason: {reason}","label":"Migration FAILED"},"replication_fail":{"title":"{hostname}: Replication FAILED — {vmname} ({vmid})","body":"Replication of {vmname} (ID: {vmid}) failed.\nReason: {reason}","label":"Replication FAILED"},"replication_complete":{"title":"{hostname}: Replication complete — {vmname} ({vmid})","body":"Replication of {vmname} (ID: {vmid}) completed successfully.","label":"Replication complete"},"backup_start":{"title":"{hostname}: Backup started on {storage}","body":"Backup job started on storage {storage}.\n{reason}","label":"Backup started"},"backup_complete":{"title":"{hostname} → {storage}: Backup complete — {vmname} ({vmid})","body":"Backup of {vmname} (ID: {vmid}) completed successfully on {storage}.\nSize: {size}","label":"Backup complete"},"backup_warning":{"title":"{hostname} → {storage}: Backup complete with warnings — {vmname} ({vmid})","body":"Backup of {vmname} (ID: {vmid}) on {storage} completed but encountered warnings.\nWarnings: {reason}","label":"Backup (warnings)"},"backup_fail":{"title":"{hostname} → {storage}: Backup FAILED — {vmname} ({vmid})","body":"Backup of {vmname} (ID: {vmid}) failed on {storage}.\nReason: {reason}","label":"Backup FAILED"},"host_backup_start":{"title":"{hostname}: Host backup started → {backend_label}","body":"Job: {job_id}\nBackend: {backend_label}\nDestination: {destination}\nProfile: {profile_mode}","label":"Host backup started"},"host_backup_complete":{"title":"{hostname}: Host backup complete → {backend_label}","body":"Job: {job_id}\nBackend: {backend_label}\nDestination: {destination}\nData size: {data_size}\nArchive size: {archive_size}\nDuration: {duration}","label":"Host backup complete"},"host_backup_fail":{"title":"{hostname}: Host backup FAILED → {backend_label}","body":"Job: {job_id}\nBackend: {backend_label}\nDestination: {destination}\nDuration before failure: {duration}\nReason: {reason}\nLog: {log_file}","label":"Host backup FAILED"},"snapshot_complete":{"title":"{hostname}: Snapshot created — {vmname} ({vmid})","body":"Snapshot \"{snapshot_name}\" created for {vmname} (ID: {vmid}).","label":"Snapshot created"},"snapshot_fail":{"title":"{hostname}: Snapshot FAILED — {vmname} ({vmid})","body":"Snapshot creation for {vmname} (ID: {vmid}) failed.\nReason: {reason}","label":"Snapshot FAILED"},"cpu_high":{"title":"{hostname}: High CPU usage — {value}%","body":"CPU usage has reached {value}% on {cores} cores.\n{details}","label":"High CPU usage"},"ram_high":{"title":"{hostname}: High memory usage — {value}%","body":"Memory usage: {used} / {total} ({value}%).\n{details}","label":"High memory usage"},"temp_high":{"title":"{hostname}: High CPU temperature — {value}°C","body":"CPU temperature has reached {value}°C (threshold: {threshold}°C).\n{details}","label":"High temperature"},"disk_space_low":{"title":"{hostname}: Low disk space on {mount}","body":"Filesystem {mount}: {used}% used ({available} available).\nFree up disk space to avoid service disruption.","label":"Low disk space"},"disk_io_error":{"title":"{hostname}: Disk failure detected — {device}","body":"I/O error or disk failure detected on device {device}.\n{reason}","label":"Disk failure / I/O error"},"storage_unavailable":{"title":"{hostname}: Storage unavailable — {storage_name}","body":"PVE storage \"{storage_name}\" (type: {storage_type}) is not accessible.\nReason: {reason}","label":"Storage unavailable"},"smart_test_complete":{"title":"{hostname}: SMART test completed — {device}","body":"SMART {test_type} test on /dev/{device} has completed.\nResult: {result}\nDuration: {duration}","label":"SMART test completed"},"smart_test_failed":{"title":"{hostname}: SMART test FAILED — {device}","body":"SMART {test_type} test on /dev/{device} has failed.\nResult: {result}\nReason: {reason}","label":"SMART test FAILED"},"gpu_mode_switch":{"title":"{hostname}: GPU mode changed to {new_mode}","body":"GPU passthrough mode has been switched.\nGPU: {gpu_name} ({gpu_pci})\nPrevious mode: {old_mode}\nNew mode: {new_mode}\n{details}","label":"GPU mode switched"},"gpu_passthrough_blocked":{"title":"{hostname}: {guest_type} {guest_id} blocked at startup","body":"PCIe passthrough guard prevented {guest_type} {guest_id} ({guest_name}) from starting.\nReason: {reason}\n{details}","label":"GPU passthrough blocked"},"pci_passthrough_conflict":{"title":"{hostname}: PCIe device conflict detected — {device_pci}","body":"A PCIe device is assigned to multiple guests.\nDevice: {device_pci}\nConflicting guests: {guest_list}\nAction required: Stop one of the guests or reassign the device.","label":"PCIe device conflict"},"load_high":{"title":"{hostname}: High system load — {value}","body":"System load average is {value} on {cores} cores.\n{details}","label":"High system load"},"network_down":{"title":"{hostname}: Network connectivity lost{entity_suffix}","body":"The node has lost network connectivity.\nReason: {reason}","label":"Network connectivity lost"},"network_latency":{"title":"{hostname}: High network latency — {value}ms","body":"Latency to gateway: {value}ms (threshold: {threshold}ms).\nThis may indicate network congestion or hardware issues.","label":"High network latency"},"auth_fail":{"title":"{hostname}: Authentication failure","body":"Failed login attempt detected.\nSource IP: {source_ip}\nUser: {username}\nService: {service}","label":"Authentication failure"},"ip_block":{"title":"{hostname}: IP blocked by Fail2Ban","body":"IP address {source_ip} has been banned.\nJail: {jail}\nFailed attempts: {failures}","label":"IP blocked by Fail2Ban"},"firewall_issue":{"title":"{hostname}: Firewall issue detected{entity_suffix}","body":"A firewall configuration issue has been detected.\nReason: {reason}","label":"Firewall issue detected"},"user_permission_change":{"title":"{hostname}: User permission changed","body":"User: {username}\nChange: {change_details}","label":"User permission changed"},"split_brain":{"title":"{hostname}: SPLIT-BRAIN detected","body":"A cluster split-brain condition has been detected. Quorum may be lost.\nQuorum status: {quorum}","label":"SPLIT-BRAIN detected"},"node_disconnect":{"title":"{hostname}: Node {node_name} disconnected","body":"Node {node_name} has disconnected from the cluster.","label":"Node disconnected"},"node_reconnect":{"title":"{hostname}: Node {node_name} reconnected","body":"Node {node_name} has rejoined the cluster successfully.","label":"Node reconnected"},"system_startup":{"title":"{hostname}: {reason}","body":"{summary}","label":"System startup report"},"system_shutdown":{"title":"{hostname}: System shutting down","body":"The node is shutting down.\n{reason}","label":"System shutting down"},"system_reboot":{"title":"{hostname}: System rebooting","body":"The node is rebooting.\n{reason}","label":"System rebooting"},"system_restore_completed":{"title":"{hostname}: Host restore finished","body":"Post-restore tasks completed in background.\n\nGuests applied: {guests}\nBind-mount stubs: {stubs}\nStale node dirs removed: {stale_nodes}\nComponents reinstalled: {components}\nDuration: {duration}\n{warnings_block}\nThe node is now fully ready to use.","label":"Host restore completed"},"system_problem":{"title":"{hostname}: System problem detected{entity_suffix}","body":"A system-level problem has been detected.\nReason: {reason}","label":"System problem detected"},"service_fail":{"title":"{hostname}: Service failed — {service_name}","body":"System service \"{service_name}\" has failed.\nReason: {reason}","label":"Service failed"},"oom_kill":{"title":"{hostname}: OOM Kill — {process}","body":"Process \"{process}\" was killed by the Out-of-Memory manager.\n{reason}","label":"Out of memory kill"},"service_fail_batch":{"title":"{hostname}: {service_count} services failed{entity_suffix}","body":"{reason}","label":"Service fail batch"},"cron_output":{"title":"{hostname}: {pve_title}","body":"{reason}","label":"Cron job output (per-cron stdout via mail)"},"system_mail":{"title":"{hostname}: {pve_title}","body":"{reason}","label":"Smartd / mail bounces (PVE system mail)"},"apt_listchanges":{"title":"{hostname}: {pve_title}","body":"Upstream package information forwarded by Proxmox VE through apt-listchanges. The following text comes from the package maintainer and is not a ProxMenux recommendation.\n\n{reason}","label":"apt-listchanges package notices"},"webhook_test":{"title":"{hostname}: Webhook test received","body":"PVE webhook connectivity test successful.\n{reason}","label":"Webhook test"},"update_available":{"title":"{hostname}: Updates available","body":"Total updates: {total_count}\nSecurity: {security_count}\nProxmox: {pve_count}\nKernel: {kernel_count}\nImportant packages:\n{important_list}","label":"Updates available (legacy)"},"unknown_persistent":{"title":"{hostname}: Check unavailable - {category}{entity_suffix}","body":"Health check for {category} has been unavailable for 3+ cycles.\n{reason}","label":"Check unavailable"},"health_persistent":{"title":"{hostname}: {count} active health issue(s){entity_suffix}","body":"The following health issues remain unresolved:\n{issue_list}\n\nThis digest is sent once every 24 hours while issues persist.","label":"Active health issues (daily)"},"health_issue_new":{"title":"{hostname}: New health issue — {category}{entity_suffix}","body":"New {severity} issue detected in: {category}\nDetails: {reason}","label":"New health issue"},"health_issue_resolved":{"title":"{hostname}: Resolved - {category}{entity_suffix}","body":"{category} issue has been resolved.\n{reason}\nDuration: {duration}","label":"Health issue resolved"},"update_summary":{"title":"{hostname}: Updates available","body":"Total updates: {total_count}\nSecurity updates: {security_count}\nProxmox-related updates: {pve_count}\nKernel updates: {kernel_count}\nImportant packages:\n{important_list}","label":"Host package updates"},"pve_update":{"title":"{hostname}: Proxmox VE {new_version} available","body":"A new Proxmox VE release is available.\nCurrent: {current_version}\nNew: {new_version}\n{details}","label":"Proxmox VE update available"},"update_complete":{"title":"{hostname}: System update completed","body":"System packages have been successfully updated.\n{details}","label":"Host update completed"},"ai_model_migrated":{"title":"{hostname}: AI model updated","body":"The AI model for notifications has been automatically updated.\nProvider: {provider}\nPrevious model: {old_model}\nNew model: {new_model}\n\n{message}","label":"AI model auto-updated"},"proxmenux_update":{"title":"{hostname}: ProxMenux {new_version} available","body":"A new version of ProxMenux is available.\nCurrent: {current_version}\nNew: {new_version}","label":"ProxMenux update available"},"mount_stale":{"title":"{hostname}: stale remote mount {mount_target}","body":"Remote mount {mount_target} ({fstype}) from {mount_source} is stale{lxc_scope}.\nStat timed out or returned an error: {error}\n\nApps writing to this path will silently land on the underlying filesystem and may fill the disk. Remount or fix connectivity ASAP.","label":"Remote mount stale"},"mount_readonly":{"title":"{hostname}: remote mount {mount_target} is read-only","body":"Remote mount {mount_target} ({fstype}) from {mount_source} is mounted read-only{lxc_scope}. Writes will fail. If this was unintentional, remount with rw.","label":"Remote mount read-only"},"lxc_disk_low":{"title":"{hostname}: CT {vmid} rootfs at {usage_percent}%","body":"CT {vmid} ({name}) rootfs is at {usage_percent}% ({disk_bytes_human} / {maxdisk_bytes_human}).\n\nA full LXC rootfs prevents the container from booting cleanly. Either expand the rootfs (pct resize {vmid} rootfs +1G) or free space inside the container.","label":"LXC rootfs near full"},"vm_disk_low":{"title":"{hostname}: VM {vmid} filesystems at {usage_percent}%","body":"VM {vmid} ({name}) guest filesystems are at {usage_percent}% ({disk_bytes_human} / {maxdisk_bytes_human}).\n\nReported by the QEMU guest agent. Includes every persistent filesystem the guest mounts on a block device — virtual disks and PCI-passthrough drives alike. Free up space inside the guest or expand the affected storage before writes start to fail.","label":"VM filesystems near full"},"lxc_mount_low":{"title":"{hostname}: CT {vmid} mount {mount} at {usage_percent}%","body":"Mount {mount} inside CT {vmid} ({name}) is at {usage_percent}% used.\nFilesystem type: {fstype}\n\nA full mount inside a container often blocks the application silently — writes either fail or, worse, land on the rootfs and trigger the rootfs alert next. Free up space on the mount or expand it.","label":"LXC mount near full"},"pve_storage_full":{"title":"{hostname}: PVE storage {storage_name} at {usage_percent}%","body":"Proxmox storage \"{storage_name}\" (type: {storage_type}) is at {usage_percent}% used.\n\nOnce full, no new VM/CT can be provisioned and existing guests may fail to write. Move/delete unused volumes or expand the underlying pool/LV/RBD image.","label":"PVE storage near full"},"zfs_pool_full":{"title":"{hostname}: ZFS pool {pool_name} at {usage_percent}%","body":"ZFS pool \"{pool_name}\" is at {usage_percent}% capacity.\n\nZFS performance and write reliability degrade sharply above ~80% capacity (CoW needs free space for new blocks). Free up snapshots, prune old datasets, or add more vdevs to the pool.","label":"ZFS pool near full"},"post_install_update":{"title":"{hostname}: {count} ProxMenux optimization update(s) available","body":"{count} ProxMenux optimization update(s) available on this host.\n\n🛠️ Available versions:\n{tool_list}\n\n💡 Apply from:\n • ProxMenux Monitor → Settings → ProxMenux Optimizations\n • Or run the post-install menu (option 2) → \"Apply available updates\"","label":"ProxMenux optimization updates available"},"secure_gateway_update_available":{"title":"{hostname}: {app_name} update available{update_title_suffix}","body":"{app_name} (managed by ProxMenux) has 📦 {package_count} package update(s) pending in its container.\n{version_line}\n\n💡 Open ProxMenux Monitor > Settings > Secure Gateway and click \"Update\" to apply.\n\n🗂️ Packages:\n{package_list}","label":"Secure Gateway update available"},"nvidia_driver_update_available":{"title":"{hostname}: NVIDIA driver update available — v{latest_version}","body":"A newer maintenance release is available for the installed NVIDIA driver branch.\n🔹 Currently installed: v{current_version}\n🟢 Latest available: v{latest_version}\n\n{upgrade_reason}\n\n💡 To reinstall:\n • From the ProxMenux post-install menu: {menu_label}\n\nReinstalling rebuilds the DKMS module against the running kernel and requires a reboot to load the new driver.","label":"NVIDIA driver update available"},"coral_driver_update_available":{"title":"{hostname}: Coral TPU driver update available — {latest_version}","body":"A newer {variant_label} is available.\n🔹 Currently installed: {current_version}\n🟢 Latest available: {latest_version}\n\n{upgrade_reason}\n\n💡 To reinstall:\n • From the ProxMenux post-install menu: {menu_label}\n\n{reboot_note}","label":"Coral TPU driver update available"},"burst_auth_fail":{"title":"{hostname}: +{count} more auth failures in {window}","body":"+{count} additional authentication failures detected in {window} ({total_count} total).\nSources: {entity_list}","label":"Auth failures burst"},"burst_ip_block":{"title":"{hostname}: Fail2Ban banned +{count} more IPs in {window}","body":"+{count} additional IPs banned by Fail2Ban in {window} ({total_count} total).\nIPs: {entity_list}","label":"IP block burst"},"burst_disk_io":{"title":"{hostname}: +{count} more disk I/O errors on {entity_list}","body":"+{count} additional I/O errors detected in {window} ({total_count} total).\nDevices: {entity_list}","label":"Disk I/O burst"},"burst_cluster":{"title":"{hostname}: Cluster flapping detected (+{count} more changes)","body":"Cluster state changed +{count} more times in {window} ({total_count} total).\nNodes: {entity_list}","label":"Cluster flapping burst"},"burst_service_fail":{"title":"{hostname}: +{count} more services failed in {window}","body":"+{count} additional service failures detected in {window} ({total_count} total).\nThis typically indicates a node reboot or PVE service restart.\n\nAdditional failures:\n{details}","label":"Service fail burst"},"burst_system":{"title":"{hostname}: +{count} more system problems in {window}","body":"+{count} additional system problems detected in {window} ({total_count} total).\n\nAdditional issues:\n{details}","label":"System problems burst"},"burst_generic":{"title":"{hostname}: +{count} more {event_type} events in {window}","body":"+{count} additional events of type {event_type} in {window} ({total_count} total).\n\nAdditional events:\n{details}","label":"Generic burst"},"kernel_warning":{"title":"{hostname}: Kernel diagnostic event detected","body":"The kernel recorded a diagnostic event: {kernel_details}","label":"Kernel warnings and diagnostic traces"}},"lxcUpdate":{"status":{"success":"succeeded","failure":"failed","partial":"completed partially","deferred":"deferred","skipped":"skipped"},"source":{"scheduled":"Scheduled","manual":"Manual"},"sourceLabel":"Source","targets":"Targets","osPending":"OS packages pending: {before} → {after}","osUnverified":"OS packages: update command executed; result not verified","applications":"Applications: {items}","applicationsUnverified":"Applications: updater executed; no tracked version change was observed","dockerEngineChange":"Docker Engine: {before} → {after}","dockerEngineVerified":"Docker Engine: verified at {version}","dockerEngineUnverified":"Docker Engine: update command executed; version not verified","dockerImagesPending":"Docker images pending: {before} → {after}","dockerImagesChanged":"Docker images changed: {images}","deferredTargets":"Deferred targets: {targets}","reason":"Reason: {reason}","restartRequired":"Restart required: {value}","yes":"yes","no":"no","verificationPending":"Verification pending until the container is running","verificationWarning":"Verification warning: {error}","duration":"Duration: {duration}"},"fallback":{"unknownTitle":"{hostname}: {event_type}","healthCheckDegraded":"Health check degraded","none":"none"},"fields":{"vmid":"VM/CT","name":"Name","device":"Device","sourceIp":"Source IP","node":"Node","category":"Category","service":"Service","jail":"Jail","user":"User","count":"Count","window":"Window","affected":"Affected"},"vzdump":{"size":"Size: {value}","duration":"Duration: {value}","file":"File","backups":"{count} backups","failed":"{count} failed","total":"Total: {value}","time":"Time: {value}"},"startup":{"issuesTitle":"{hostname}: System startup - {count} issue(s) detected","completeTitle":"{hostname}: System startup completed","operational":"All systems operational.","vmCountOne":"{count} VM","vmCountMany":"{count} VMs","ctCountOne":"{count} CT","ctCountMany":"{count} CTs","started":"✅ {counts} started","vmFailed":"❌ VM failed: {name} - {reason}","ctFailed":"❌ CT failed: {name} - {reason}","unknownError":"unknown error","storageUnavailable":"⚠️ Storage: {count} unavailable ({names})","servicesFailed":"⚠️ Services: {count} failed ({names})","duration":"⏱️ Startup completed in {minutes} min"},"appUpdates":{"singleTitle":"{hostname}: {app_name} update available on CT {vmid}","singleBody":"{app_name} on CT {vmid} ({ct_name}) has a new version:\n {installed} → {latest}","emptyTitle":"{hostname}: Application updates available","emptyBody":"Application updates are available.","batchTitle":"{hostname}: {count} application updates available","batchLeadOneContainer":"{count} applications in {container_count} LXC container have a newer version:","batchLeadManyContainers":"{count} applications in {container_count} LXC containers have a newer version:","additional":"… {count} additional application(s)","app":"app","unknown":"unknown"},"healthDegraded":{"categories":{"cpu":"CPU usage and temperature","memory":"Memory and swap","storage":"Storage mounts and space","disks":"Disk I/O and errors","network":"Network interfaces","vms":"VMs and containers","services":"PVE services","logs":"System logs","updates":"System updates","security":"Security"},"severity":{"critical":"Critical","warning":"Warning"},"singleTitle":"{hostname}: {severity} health status – {category}","multipleTitle":"{hostname}: {count} health checks degraded","multipleLine":"• {severity}: {category}\n {reason}","reasons":{"cpuSustained":"CPU is above {threshold}% for {duration}s."}},"digest":{"title":"{hostname}: 24h summary ({timestamp})","readFailed":"digest read failed: {error}","empty":"No INFO events buffered for this digest window.","lead":"{count} INFO events grouped by category:\n","more":" • … and {count} more","footer":"(Critical/Warning events arrived at the time they happened, not in this digest.)","quietTitle":"{hostname}: {count} events buffered during Quiet Hours","groups":{"vm_ct":"VM a CT","backup":"Backup","resources":"Resources","storage":"Storage","network":"Network","security":"Security","cluster":"Cluster","services":"Services","health":"Health","updates":"Updates","hardware":"Hardware","system":"System","other":"Other"}},"test":{"title":"ProxMenux Test","welcome":"Welcome to ProxMenux Monitor!","verify":"This is a test message to verify your notification channel is working correctly.","configuration":"Channel configuration:","iconsEnabled":"Icons: enabled","iconsDisabled":"Icons: disabled","aiEnabled":"AI: enabled ({info})","aiDisabled":"AI: disabled","alerts":"You will receive alerts about system events, health status changes, and security incidents.","photoCaption":"You can use this image as the profile photo for your notification bot."},"channels":{"discord":{"category":"Category","host":"Host","severity":"Severity"},"email":{"report":"{group} Report","details":"Details","host":"Host","footer":"ProxMenux Notification Service","severity":{"critical":"Critical","warning":"Warning","info":"Information","ok":"Resolved","default":"Notice"},"groups":{"vm_ct":"Virtual Machine / Container","backup":"Backup & Snapshot","resources":"System Resources","storage":"Storage","network":"Network","security":"Security","cluster":"Cluster","services":"System Services","health":"Health Monitor","updates":"System Updates","system":"System","hardware":"Hardware","other":"System Notification"},"fields":{"vmCtId":"VM/CT ID","name":"Name","action":"Action","targetNode":"Target Node","reason":"Reason","storage":"Storage","status":"Status","size":"Size","duration":"Duration","snapshot":"Snapshot","metric":"Metric","currentValue":"Current Value","threshold":"Threshold","cpuCores":"CPU Cores","memory":"Memory","temperature":"Temperature","mountPoint":"Mount Point","usage":"Usage","available":"Available","device":"Device","severity":"Severity","storageName":"Storage Name","type":"Type","interface":"Interface","latency":"Latency","event":"Event","sourceIp":"Source IP","username":"Username","service":"Service","jail":"Jail","failures":"Failures","change":"Change","node":"Node","quorum":"Quorum","nodesAffected":"Nodes Affected","process":"Process","category":"Category","previousSeverity":"Previous Severity","activeIssues":"Active Issues","totalUpdates":"Total Updates","securityUpdates":"Security Updates","proxmoxUpdates":"Proxmox Updates","kernelUpdates":"Kernel Updates","importantPackages":"Important Packages","currentVersion":"Current Version","newVersion":"New Version"},"status":{"failed":"Failed","completed":"Completed","started":"Started"}}}}}
}
diff --git a/AppImage/messages/fr/common.json b/AppImage/messages/fr/common.json
index 5cd6a2c8..39665f9c 100644
--- a/AppImage/messages/fr/common.json
+++ b/AppImage/messages/fr/common.json
@@ -1820,6 +1820,12 @@
"updates": {
"label": "Mises à jour"
},
+ "hardware": {
+ "label": "Hardware"
+ },
+ "system": {
+ "label": "System"
+ },
"other": {
"label": "Autre"
}
@@ -1831,6 +1837,8 @@
"error_escalated": "Le problème de santé s'est aggravé",
"health_degraded": "Bilan de santé dégradé",
"lxc_updates_available": "Mises à jour LXC disponibles (expérimentales)",
+ "lxc_update_applied": "LXC update applied",
+ "docker_stack_update_available": "Docker updates available",
"vm_start": "VM démarrée",
"vm_start_warning": "VM démarrée (avertissements)",
"vm_stop": "VM arrêtée",
@@ -1982,7 +1990,10 @@
"systemHostname": "Nom d'hôte du système",
"displayNameHint": "Nom affiché dans les notifications. Modifiez-le ou laissez-le vide pour utiliser le nom d'hôte du système.",
"activeFooter": "Les notifications sont actives. Chaque chaîne suit sa propre catégorie et sa propre sélection d'événements.",
- "inactiveFooter": "Activez les notifications pour recevoir des alertes système, de santé et de sécurité via les canaux configurés."
+ "inactiveFooter": "Activez les notifications pour recevoir des alertes système, de santé et de sécurité via les canaux configurés.",
+ "notificationLanguage": "Notification language",
+ "selectNotificationLanguage": "Select notification language",
+ "notificationLanguageHint": "Used for deterministic runtime notifications even when AI is disabled."
},
"ai": {
"title": "Amélioration de l'IA",
@@ -6018,5 +6029,6 @@
"withoutLynis": "Run without Lynis",
"cancel": "Cancel"
}
- }
+ },
+ "runtime": {"notifications":{"templates":{"state_change":{"title":"{hostname}: {category} changed to {current}{entity_suffix}","body":"{category} status changed from {previous} to {current}.\n{reason}","label":"Health state changed"},"new_error":{"title":"{hostname}: New {severity} - {category}{entity_suffix}","body":"{reason}","label":"New health issue"},"error_resolved":{"title":"{hostname}: Resolved - {category}{entity_suffix}","body":"The {category} issue has been resolved.\n{reason}\n🚦 Previous severity: {original_severity}\n⏱️ Duration: {duration}","label":"Recovery notification"},"error_escalated":{"title":"{hostname}: Escalated to {severity} - {category}{entity_suffix}","body":"{reason}","label":"Health issue escalated"},"health_degraded":{"title":"{title_or_default}","body":"{reason}","label":"Health check degraded"},"lxc_updates_available":{"title":"{hostname}: {count} LXC(s) with package updates available","body":"📊 {count} LXC(s) with pending package updates (📦 {total_packages} total, 🔒 {security_count} security):\n\n{ct_list}","label":"LXC updates available (experimental)"},"lxc_update_applied":{"title":"{hostname}: LXC {ct_name} ({vmid}) update {result}","body":"{details}","label":"LXC update applied"},"app_update_available":{"title":"{hostname}: {app_name} update available on CT {vmid}","body":"{app_name} on CT {vmid} ({ct_name}) has a new version:\n {installed} → {latest}","label":"App update available"},"docker_stack_update_available":{"title":"{hostname}: Docker updates available on CT {vmid}","body":"Container {ct_name} (CT {vmid}) has {count} Docker update(s):\n{details}","label":"Docker updates available"},"vm_start":{"title":"{hostname}: VM {vmname} ({vmid}) started","body":"Virtual machine {vmname} (ID: {vmid}) is now running.","label":"VM started"},"vm_start_warning":{"title":"{hostname}: VM {vmname} ({vmid}) started with warnings","body":"Virtual machine {vmname} (ID: {vmid}) started successfully but has warnings.\nWarnings: {reason}","label":"VM started (warnings)"},"vm_stop":{"title":"{hostname}: VM {vmname} ({vmid}) stopped","body":"Virtual machine {vmname} (ID: {vmid}) has been stopped.","label":"VM stopped"},"vm_shutdown":{"title":"{hostname}: VM {vmname} ({vmid}) shut down","body":"Virtual machine {vmname} (ID: {vmid}) has been cleanly shut down.","label":"VM shutdown"},"vm_fail":{"title":"{hostname}: VM {vmname} ({vmid}) FAILED","body":"Virtual machine {vmname} (ID: {vmid}) has crashed or failed to start.\nReason: {reason}","label":"VM FAILED"},"vm_restart":{"title":"{hostname}: VM {vmname} ({vmid}) restarted","body":"Virtual machine {vmname} (ID: {vmid}) has been restarted.","label":"VM restarted"},"ct_start":{"title":"{hostname}: CT {vmname} ({vmid}) started","body":"Container {vmname} (ID: {vmid}) is now running.","label":"CT started"},"ct_start_warning":{"title":"{hostname}: CT {vmname} ({vmid}) started with warnings","body":"Container {vmname} (ID: {vmid}) started successfully but has warnings.\nWarnings: {reason}","label":"CT started (warnings)"},"ct_stop":{"title":"{hostname}: CT {vmname} ({vmid}) stopped","body":"Container {vmname} (ID: {vmid}) has been stopped.","label":"CT stopped"},"ct_shutdown":{"title":"{hostname}: CT {vmname} ({vmid}) shut down","body":"Container {vmname} (ID: {vmid}) has been cleanly shut down.","label":"CT shutdown"},"ct_restart":{"title":"{hostname}: CT {vmname} ({vmid}) restarted","body":"Container {vmname} (ID: {vmid}) has been restarted.","label":"CT restarted"},"ct_fail":{"title":"{hostname}: CT {vmname} ({vmid}) FAILED","body":"Container {vmname} (ID: {vmid}) has crashed or failed to start.\nReason: {reason}","label":"CT FAILED"},"migration_start":{"title":"{hostname}: Migration started — {vmname} ({vmid})","body":"Live migration of {vmname} (ID: {vmid}) to node {target_node} has started.","label":"Migration started"},"migration_complete":{"title":"{hostname}: Migration complete — {vmname} ({vmid})","body":"{vmname} (ID: {vmid}) successfully migrated to node {target_node}.","label":"Migration complete"},"migration_warning":{"title":"{hostname}: Migration complete with warnings — {vmname} ({vmid})","body":"{vmname} (ID: {vmid}) migrated to node {target_node} but encountered warnings.\nWarnings: {reason}","label":"Migration (warnings)"},"migration_fail":{"title":"{hostname}: Migration FAILED — {vmname} ({vmid})","body":"Migration of {vmname} (ID: {vmid}) to node {target_node} failed.\nReason: {reason}","label":"Migration FAILED"},"replication_fail":{"title":"{hostname}: Replication FAILED — {vmname} ({vmid})","body":"Replication of {vmname} (ID: {vmid}) failed.\nReason: {reason}","label":"Replication FAILED"},"replication_complete":{"title":"{hostname}: Replication complete — {vmname} ({vmid})","body":"Replication of {vmname} (ID: {vmid}) completed successfully.","label":"Replication complete"},"backup_start":{"title":"{hostname}: Backup started on {storage}","body":"Backup job started on storage {storage}.\n{reason}","label":"Backup started"},"backup_complete":{"title":"{hostname} → {storage}: Backup complete — {vmname} ({vmid})","body":"Backup of {vmname} (ID: {vmid}) completed successfully on {storage}.\nSize: {size}","label":"Backup complete"},"backup_warning":{"title":"{hostname} → {storage}: Backup complete with warnings — {vmname} ({vmid})","body":"Backup of {vmname} (ID: {vmid}) on {storage} completed but encountered warnings.\nWarnings: {reason}","label":"Backup (warnings)"},"backup_fail":{"title":"{hostname} → {storage}: Backup FAILED — {vmname} ({vmid})","body":"Backup of {vmname} (ID: {vmid}) failed on {storage}.\nReason: {reason}","label":"Backup FAILED"},"host_backup_start":{"title":"{hostname}: Host backup started → {backend_label}","body":"Job: {job_id}\nBackend: {backend_label}\nDestination: {destination}\nProfile: {profile_mode}","label":"Host backup started"},"host_backup_complete":{"title":"{hostname}: Host backup complete → {backend_label}","body":"Job: {job_id}\nBackend: {backend_label}\nDestination: {destination}\nData size: {data_size}\nArchive size: {archive_size}\nDuration: {duration}","label":"Host backup complete"},"host_backup_fail":{"title":"{hostname}: Host backup FAILED → {backend_label}","body":"Job: {job_id}\nBackend: {backend_label}\nDestination: {destination}\nDuration before failure: {duration}\nReason: {reason}\nLog: {log_file}","label":"Host backup FAILED"},"snapshot_complete":{"title":"{hostname}: Snapshot created — {vmname} ({vmid})","body":"Snapshot \"{snapshot_name}\" created for {vmname} (ID: {vmid}).","label":"Snapshot created"},"snapshot_fail":{"title":"{hostname}: Snapshot FAILED — {vmname} ({vmid})","body":"Snapshot creation for {vmname} (ID: {vmid}) failed.\nReason: {reason}","label":"Snapshot FAILED"},"cpu_high":{"title":"{hostname}: High CPU usage — {value}%","body":"CPU usage has reached {value}% on {cores} cores.\n{details}","label":"High CPU usage"},"ram_high":{"title":"{hostname}: High memory usage — {value}%","body":"Memory usage: {used} / {total} ({value}%).\n{details}","label":"High memory usage"},"temp_high":{"title":"{hostname}: High CPU temperature — {value}°C","body":"CPU temperature has reached {value}°C (threshold: {threshold}°C).\n{details}","label":"High temperature"},"disk_space_low":{"title":"{hostname}: Low disk space on {mount}","body":"Filesystem {mount}: {used}% used ({available} available).\nFree up disk space to avoid service disruption.","label":"Low disk space"},"disk_io_error":{"title":"{hostname}: Disk failure detected — {device}","body":"I/O error or disk failure detected on device {device}.\n{reason}","label":"Disk failure / I/O error"},"storage_unavailable":{"title":"{hostname}: Storage unavailable — {storage_name}","body":"PVE storage \"{storage_name}\" (type: {storage_type}) is not accessible.\nReason: {reason}","label":"Storage unavailable"},"smart_test_complete":{"title":"{hostname}: SMART test completed — {device}","body":"SMART {test_type} test on /dev/{device} has completed.\nResult: {result}\nDuration: {duration}","label":"SMART test completed"},"smart_test_failed":{"title":"{hostname}: SMART test FAILED — {device}","body":"SMART {test_type} test on /dev/{device} has failed.\nResult: {result}\nReason: {reason}","label":"SMART test FAILED"},"gpu_mode_switch":{"title":"{hostname}: GPU mode changed to {new_mode}","body":"GPU passthrough mode has been switched.\nGPU: {gpu_name} ({gpu_pci})\nPrevious mode: {old_mode}\nNew mode: {new_mode}\n{details}","label":"GPU mode switched"},"gpu_passthrough_blocked":{"title":"{hostname}: {guest_type} {guest_id} blocked at startup","body":"PCIe passthrough guard prevented {guest_type} {guest_id} ({guest_name}) from starting.\nReason: {reason}\n{details}","label":"GPU passthrough blocked"},"pci_passthrough_conflict":{"title":"{hostname}: PCIe device conflict detected — {device_pci}","body":"A PCIe device is assigned to multiple guests.\nDevice: {device_pci}\nConflicting guests: {guest_list}\nAction required: Stop one of the guests or reassign the device.","label":"PCIe device conflict"},"load_high":{"title":"{hostname}: High system load — {value}","body":"System load average is {value} on {cores} cores.\n{details}","label":"High system load"},"network_down":{"title":"{hostname}: Network connectivity lost{entity_suffix}","body":"The node has lost network connectivity.\nReason: {reason}","label":"Network connectivity lost"},"network_latency":{"title":"{hostname}: High network latency — {value}ms","body":"Latency to gateway: {value}ms (threshold: {threshold}ms).\nThis may indicate network congestion or hardware issues.","label":"High network latency"},"auth_fail":{"title":"{hostname}: Authentication failure","body":"Failed login attempt detected.\nSource IP: {source_ip}\nUser: {username}\nService: {service}","label":"Authentication failure"},"ip_block":{"title":"{hostname}: IP blocked by Fail2Ban","body":"IP address {source_ip} has been banned.\nJail: {jail}\nFailed attempts: {failures}","label":"IP blocked by Fail2Ban"},"firewall_issue":{"title":"{hostname}: Firewall issue detected{entity_suffix}","body":"A firewall configuration issue has been detected.\nReason: {reason}","label":"Firewall issue detected"},"user_permission_change":{"title":"{hostname}: User permission changed","body":"User: {username}\nChange: {change_details}","label":"User permission changed"},"split_brain":{"title":"{hostname}: SPLIT-BRAIN detected","body":"A cluster split-brain condition has been detected. Quorum may be lost.\nQuorum status: {quorum}","label":"SPLIT-BRAIN detected"},"node_disconnect":{"title":"{hostname}: Node {node_name} disconnected","body":"Node {node_name} has disconnected from the cluster.","label":"Node disconnected"},"node_reconnect":{"title":"{hostname}: Node {node_name} reconnected","body":"Node {node_name} has rejoined the cluster successfully.","label":"Node reconnected"},"system_startup":{"title":"{hostname}: {reason}","body":"{summary}","label":"System startup report"},"system_shutdown":{"title":"{hostname}: System shutting down","body":"The node is shutting down.\n{reason}","label":"System shutting down"},"system_reboot":{"title":"{hostname}: System rebooting","body":"The node is rebooting.\n{reason}","label":"System rebooting"},"system_restore_completed":{"title":"{hostname}: Host restore finished","body":"Post-restore tasks completed in background.\n\nGuests applied: {guests}\nBind-mount stubs: {stubs}\nStale node dirs removed: {stale_nodes}\nComponents reinstalled: {components}\nDuration: {duration}\n{warnings_block}\nThe node is now fully ready to use.","label":"Host restore completed"},"system_problem":{"title":"{hostname}: System problem detected{entity_suffix}","body":"A system-level problem has been detected.\nReason: {reason}","label":"System problem detected"},"service_fail":{"title":"{hostname}: Service failed — {service_name}","body":"System service \"{service_name}\" has failed.\nReason: {reason}","label":"Service failed"},"oom_kill":{"title":"{hostname}: OOM Kill — {process}","body":"Process \"{process}\" was killed by the Out-of-Memory manager.\n{reason}","label":"Out of memory kill"},"service_fail_batch":{"title":"{hostname}: {service_count} services failed{entity_suffix}","body":"{reason}","label":"Service fail batch"},"cron_output":{"title":"{hostname}: {pve_title}","body":"{reason}","label":"Cron job output (per-cron stdout via mail)"},"system_mail":{"title":"{hostname}: {pve_title}","body":"{reason}","label":"Smartd / mail bounces (PVE system mail)"},"apt_listchanges":{"title":"{hostname}: {pve_title}","body":"Upstream package information forwarded by Proxmox VE through apt-listchanges. The following text comes from the package maintainer and is not a ProxMenux recommendation.\n\n{reason}","label":"apt-listchanges package notices"},"webhook_test":{"title":"{hostname}: Webhook test received","body":"PVE webhook connectivity test successful.\n{reason}","label":"Webhook test"},"update_available":{"title":"{hostname}: Updates available","body":"Total updates: {total_count}\nSecurity: {security_count}\nProxmox: {pve_count}\nKernel: {kernel_count}\nImportant packages:\n{important_list}","label":"Updates available (legacy)"},"unknown_persistent":{"title":"{hostname}: Check unavailable - {category}{entity_suffix}","body":"Health check for {category} has been unavailable for 3+ cycles.\n{reason}","label":"Check unavailable"},"health_persistent":{"title":"{hostname}: {count} active health issue(s){entity_suffix}","body":"The following health issues remain unresolved:\n{issue_list}\n\nThis digest is sent once every 24 hours while issues persist.","label":"Active health issues (daily)"},"health_issue_new":{"title":"{hostname}: New health issue — {category}{entity_suffix}","body":"New {severity} issue detected in: {category}\nDetails: {reason}","label":"New health issue"},"health_issue_resolved":{"title":"{hostname}: Resolved - {category}{entity_suffix}","body":"{category} issue has been resolved.\n{reason}\nDuration: {duration}","label":"Health issue resolved"},"update_summary":{"title":"{hostname}: Updates available","body":"Total updates: {total_count}\nSecurity updates: {security_count}\nProxmox-related updates: {pve_count}\nKernel updates: {kernel_count}\nImportant packages:\n{important_list}","label":"Host package updates"},"pve_update":{"title":"{hostname}: Proxmox VE {new_version} available","body":"A new Proxmox VE release is available.\nCurrent: {current_version}\nNew: {new_version}\n{details}","label":"Proxmox VE update available"},"update_complete":{"title":"{hostname}: System update completed","body":"System packages have been successfully updated.\n{details}","label":"Host update completed"},"ai_model_migrated":{"title":"{hostname}: AI model updated","body":"The AI model for notifications has been automatically updated.\nProvider: {provider}\nPrevious model: {old_model}\nNew model: {new_model}\n\n{message}","label":"AI model auto-updated"},"proxmenux_update":{"title":"{hostname}: ProxMenux {new_version} available","body":"A new version of ProxMenux is available.\nCurrent: {current_version}\nNew: {new_version}","label":"ProxMenux update available"},"mount_stale":{"title":"{hostname}: stale remote mount {mount_target}","body":"Remote mount {mount_target} ({fstype}) from {mount_source} is stale{lxc_scope}.\nStat timed out or returned an error: {error}\n\nApps writing to this path will silently land on the underlying filesystem and may fill the disk. Remount or fix connectivity ASAP.","label":"Remote mount stale"},"mount_readonly":{"title":"{hostname}: remote mount {mount_target} is read-only","body":"Remote mount {mount_target} ({fstype}) from {mount_source} is mounted read-only{lxc_scope}. Writes will fail. If this was unintentional, remount with rw.","label":"Remote mount read-only"},"lxc_disk_low":{"title":"{hostname}: CT {vmid} rootfs at {usage_percent}%","body":"CT {vmid} ({name}) rootfs is at {usage_percent}% ({disk_bytes_human} / {maxdisk_bytes_human}).\n\nA full LXC rootfs prevents the container from booting cleanly. Either expand the rootfs (pct resize {vmid} rootfs +1G) or free space inside the container.","label":"LXC rootfs near full"},"vm_disk_low":{"title":"{hostname}: VM {vmid} filesystems at {usage_percent}%","body":"VM {vmid} ({name}) guest filesystems are at {usage_percent}% ({disk_bytes_human} / {maxdisk_bytes_human}).\n\nReported by the QEMU guest agent. Includes every persistent filesystem the guest mounts on a block device — virtual disks and PCI-passthrough drives alike. Free up space inside the guest or expand the affected storage before writes start to fail.","label":"VM filesystems near full"},"lxc_mount_low":{"title":"{hostname}: CT {vmid} mount {mount} at {usage_percent}%","body":"Mount {mount} inside CT {vmid} ({name}) is at {usage_percent}% used.\nFilesystem type: {fstype}\n\nA full mount inside a container often blocks the application silently — writes either fail or, worse, land on the rootfs and trigger the rootfs alert next. Free up space on the mount or expand it.","label":"LXC mount near full"},"pve_storage_full":{"title":"{hostname}: PVE storage {storage_name} at {usage_percent}%","body":"Proxmox storage \"{storage_name}\" (type: {storage_type}) is at {usage_percent}% used.\n\nOnce full, no new VM/CT can be provisioned and existing guests may fail to write. Move/delete unused volumes or expand the underlying pool/LV/RBD image.","label":"PVE storage near full"},"zfs_pool_full":{"title":"{hostname}: ZFS pool {pool_name} at {usage_percent}%","body":"ZFS pool \"{pool_name}\" is at {usage_percent}% capacity.\n\nZFS performance and write reliability degrade sharply above ~80% capacity (CoW needs free space for new blocks). Free up snapshots, prune old datasets, or add more vdevs to the pool.","label":"ZFS pool near full"},"post_install_update":{"title":"{hostname}: {count} ProxMenux optimization update(s) available","body":"{count} ProxMenux optimization update(s) available on this host.\n\n🛠️ Available versions:\n{tool_list}\n\n💡 Apply from:\n • ProxMenux Monitor → Settings → ProxMenux Optimizations\n • Or run the post-install menu (option 2) → \"Apply available updates\"","label":"ProxMenux optimization updates available"},"secure_gateway_update_available":{"title":"{hostname}: {app_name} update available{update_title_suffix}","body":"{app_name} (managed by ProxMenux) has 📦 {package_count} package update(s) pending in its container.\n{version_line}\n\n💡 Open ProxMenux Monitor > Settings > Secure Gateway and click \"Update\" to apply.\n\n🗂️ Packages:\n{package_list}","label":"Secure Gateway update available"},"nvidia_driver_update_available":{"title":"{hostname}: NVIDIA driver update available — v{latest_version}","body":"A newer maintenance release is available for the installed NVIDIA driver branch.\n🔹 Currently installed: v{current_version}\n🟢 Latest available: v{latest_version}\n\n{upgrade_reason}\n\n💡 To reinstall:\n • From the ProxMenux post-install menu: {menu_label}\n\nReinstalling rebuilds the DKMS module against the running kernel and requires a reboot to load the new driver.","label":"NVIDIA driver update available"},"coral_driver_update_available":{"title":"{hostname}: Coral TPU driver update available — {latest_version}","body":"A newer {variant_label} is available.\n🔹 Currently installed: {current_version}\n🟢 Latest available: {latest_version}\n\n{upgrade_reason}\n\n💡 To reinstall:\n • From the ProxMenux post-install menu: {menu_label}\n\n{reboot_note}","label":"Coral TPU driver update available"},"burst_auth_fail":{"title":"{hostname}: +{count} more auth failures in {window}","body":"+{count} additional authentication failures detected in {window} ({total_count} total).\nSources: {entity_list}","label":"Auth failures burst"},"burst_ip_block":{"title":"{hostname}: Fail2Ban banned +{count} more IPs in {window}","body":"+{count} additional IPs banned by Fail2Ban in {window} ({total_count} total).\nIPs: {entity_list}","label":"IP block burst"},"burst_disk_io":{"title":"{hostname}: +{count} more disk I/O errors on {entity_list}","body":"+{count} additional I/O errors detected in {window} ({total_count} total).\nDevices: {entity_list}","label":"Disk I/O burst"},"burst_cluster":{"title":"{hostname}: Cluster flapping detected (+{count} more changes)","body":"Cluster state changed +{count} more times in {window} ({total_count} total).\nNodes: {entity_list}","label":"Cluster flapping burst"},"burst_service_fail":{"title":"{hostname}: +{count} more services failed in {window}","body":"+{count} additional service failures detected in {window} ({total_count} total).\nThis typically indicates a node reboot or PVE service restart.\n\nAdditional failures:\n{details}","label":"Service fail burst"},"burst_system":{"title":"{hostname}: +{count} more system problems in {window}","body":"+{count} additional system problems detected in {window} ({total_count} total).\n\nAdditional issues:\n{details}","label":"System problems burst"},"burst_generic":{"title":"{hostname}: +{count} more {event_type} events in {window}","body":"+{count} additional events of type {event_type} in {window} ({total_count} total).\n\nAdditional events:\n{details}","label":"Generic burst"},"kernel_warning":{"title":"{hostname}: Kernel diagnostic event detected","body":"The kernel recorded a diagnostic event: {kernel_details}","label":"Kernel warnings and diagnostic traces"}},"lxcUpdate":{"status":{"success":"succeeded","failure":"failed","partial":"completed partially","deferred":"deferred","skipped":"skipped"},"source":{"scheduled":"Scheduled","manual":"Manual"},"sourceLabel":"Source","targets":"Targets","osPending":"OS packages pending: {before} → {after}","osUnverified":"OS packages: update command executed; result not verified","applications":"Applications: {items}","applicationsUnverified":"Applications: updater executed; no tracked version change was observed","dockerEngineChange":"Docker Engine: {before} → {after}","dockerEngineVerified":"Docker Engine: verified at {version}","dockerEngineUnverified":"Docker Engine: update command executed; version not verified","dockerImagesPending":"Docker images pending: {before} → {after}","dockerImagesChanged":"Docker images changed: {images}","deferredTargets":"Deferred targets: {targets}","reason":"Reason: {reason}","restartRequired":"Restart required: {value}","yes":"yes","no":"no","verificationPending":"Verification pending until the container is running","verificationWarning":"Verification warning: {error}","duration":"Duration: {duration}"},"fallback":{"unknownTitle":"{hostname}: {event_type}","healthCheckDegraded":"Health check degraded","none":"none"},"fields":{"vmid":"VM/CT","name":"Name","device":"Device","sourceIp":"Source IP","node":"Node","category":"Category","service":"Service","jail":"Jail","user":"User","count":"Count","window":"Window","affected":"Affected"},"vzdump":{"size":"Size: {value}","duration":"Duration: {value}","file":"File","backups":"{count} backups","failed":"{count} failed","total":"Total: {value}","time":"Time: {value}"},"startup":{"issuesTitle":"{hostname}: System startup - {count} issue(s) detected","completeTitle":"{hostname}: System startup completed","operational":"All systems operational.","vmCountOne":"{count} VM","vmCountMany":"{count} VMs","ctCountOne":"{count} CT","ctCountMany":"{count} CTs","started":"✅ {counts} started","vmFailed":"❌ VM failed: {name} - {reason}","ctFailed":"❌ CT failed: {name} - {reason}","unknownError":"unknown error","storageUnavailable":"⚠️ Storage: {count} unavailable ({names})","servicesFailed":"⚠️ Services: {count} failed ({names})","duration":"⏱️ Startup completed in {minutes} min"},"appUpdates":{"singleTitle":"{hostname}: {app_name} update available on CT {vmid}","singleBody":"{app_name} on CT {vmid} ({ct_name}) has a new version:\n {installed} → {latest}","emptyTitle":"{hostname}: Application updates available","emptyBody":"Application updates are available.","batchTitle":"{hostname}: {count} application updates available","batchLeadOneContainer":"{count} applications in {container_count} LXC container have a newer version:","batchLeadManyContainers":"{count} applications in {container_count} LXC containers have a newer version:","additional":"… {count} additional application(s)","app":"app","unknown":"unknown"},"healthDegraded":{"categories":{"cpu":"CPU usage and temperature","memory":"Memory and swap","storage":"Storage mounts and space","disks":"Disk I/O and errors","network":"Network interfaces","vms":"VMs and containers","services":"PVE services","logs":"System logs","updates":"System updates","security":"Security"},"severity":{"critical":"Critical","warning":"Warning"},"singleTitle":"{hostname}: {severity} health status – {category}","multipleTitle":"{hostname}: {count} health checks degraded","multipleLine":"• {severity}: {category}\n {reason}","reasons":{"cpuSustained":"CPU is above {threshold}% for {duration}s."}},"digest":{"title":"{hostname}: 24h summary ({timestamp})","readFailed":"digest read failed: {error}","empty":"No INFO events buffered for this digest window.","lead":"{count} INFO events grouped by category:\n","more":" • … and {count} more","footer":"(Critical/Warning events arrived at the time they happened, not in this digest.)","quietTitle":"{hostname}: {count} events buffered during Quiet Hours","groups":{"vm_ct":"VM a CT","backup":"Backup","resources":"Resources","storage":"Storage","network":"Network","security":"Security","cluster":"Cluster","services":"Services","health":"Health","updates":"Updates","hardware":"Hardware","system":"System","other":"Other"}},"test":{"title":"ProxMenux Test","welcome":"Welcome to ProxMenux Monitor!","verify":"This is a test message to verify your notification channel is working correctly.","configuration":"Channel configuration:","iconsEnabled":"Icons: enabled","iconsDisabled":"Icons: disabled","aiEnabled":"AI: enabled ({info})","aiDisabled":"AI: disabled","alerts":"You will receive alerts about system events, health status changes, and security incidents.","photoCaption":"You can use this image as the profile photo for your notification bot."},"channels":{"discord":{"category":"Category","host":"Host","severity":"Severity"},"email":{"report":"{group} Report","details":"Details","host":"Host","footer":"ProxMenux Notification Service","severity":{"critical":"Critical","warning":"Warning","info":"Information","ok":"Resolved","default":"Notice"},"groups":{"vm_ct":"Virtual Machine / Container","backup":"Backup & Snapshot","resources":"System Resources","storage":"Storage","network":"Network","security":"Security","cluster":"Cluster","services":"System Services","health":"Health Monitor","updates":"System Updates","system":"System","hardware":"Hardware","other":"System Notification"},"fields":{"vmCtId":"VM/CT ID","name":"Name","action":"Action","targetNode":"Target Node","reason":"Reason","storage":"Storage","status":"Status","size":"Size","duration":"Duration","snapshot":"Snapshot","metric":"Metric","currentValue":"Current Value","threshold":"Threshold","cpuCores":"CPU Cores","memory":"Memory","temperature":"Temperature","mountPoint":"Mount Point","usage":"Usage","available":"Available","device":"Device","severity":"Severity","storageName":"Storage Name","type":"Type","interface":"Interface","latency":"Latency","event":"Event","sourceIp":"Source IP","username":"Username","service":"Service","jail":"Jail","failures":"Failures","change":"Change","node":"Node","quorum":"Quorum","nodesAffected":"Nodes Affected","process":"Process","category":"Category","previousSeverity":"Previous Severity","activeIssues":"Active Issues","totalUpdates":"Total Updates","securityUpdates":"Security Updates","proxmoxUpdates":"Proxmox Updates","kernelUpdates":"Kernel Updates","importantPackages":"Important Packages","currentVersion":"Current Version","newVersion":"New Version"},"status":{"failed":"Failed","completed":"Completed","started":"Started"}}}}}
}
diff --git a/AppImage/messages/it/common.json b/AppImage/messages/it/common.json
index 52127595..adc16858 100644
--- a/AppImage/messages/it/common.json
+++ b/AppImage/messages/it/common.json
@@ -1820,6 +1820,12 @@
"updates": {
"label": "Aggiornamenti"
},
+ "hardware": {
+ "label": "Hardware"
+ },
+ "system": {
+ "label": "System"
+ },
"other": {
"label": "Altro"
}
@@ -1831,6 +1837,8 @@
"error_escalated": "Il problema sanitario si è intensificato",
"health_degraded": "Controllo dello stato peggiorato",
"lxc_updates_available": "Aggiornamenti LXC disponibili (sperimentali)",
+ "lxc_update_applied": "LXC update applied",
+ "docker_stack_update_available": "Docker updates available",
"vm_start": "La VM è stata avviata",
"vm_start_warning": "VM avviata (avvisi)",
"vm_stop": "La macchina virtuale si è arrestata",
@@ -1982,7 +1990,10 @@
"systemHostname": "Nome host del sistema",
"displayNameHint": "Nome mostrato nelle notifiche. Modificalo o lascialo vuoto per utilizzare il nome host del sistema.",
"activeFooter": "Le notifiche sono attive. Ogni canale segue la propria categoria e selezione di eventi.",
- "inactiveFooter": "Abilita le notifiche per ricevere avvisi di sistema, integrità e sicurezza attraverso i canali configurati."
+ "inactiveFooter": "Abilita le notifiche per ricevere avvisi di sistema, integrità e sicurezza attraverso i canali configurati.",
+ "notificationLanguage": "Notification language",
+ "selectNotificationLanguage": "Select notification language",
+ "notificationLanguageHint": "Used for deterministic runtime notifications even when AI is disabled."
},
"ai": {
"title": "Miglioramento dell'intelligenza artificiale",
@@ -6018,5 +6029,6 @@
"withoutLynis": "Run without Lynis",
"cancel": "Cancel"
}
- }
+ },
+ "runtime": {"notifications":{"templates":{"state_change":{"title":"{hostname}: {category} changed to {current}{entity_suffix}","body":"{category} status changed from {previous} to {current}.\n{reason}","label":"Health state changed"},"new_error":{"title":"{hostname}: New {severity} - {category}{entity_suffix}","body":"{reason}","label":"New health issue"},"error_resolved":{"title":"{hostname}: Resolved - {category}{entity_suffix}","body":"The {category} issue has been resolved.\n{reason}\n🚦 Previous severity: {original_severity}\n⏱️ Duration: {duration}","label":"Recovery notification"},"error_escalated":{"title":"{hostname}: Escalated to {severity} - {category}{entity_suffix}","body":"{reason}","label":"Health issue escalated"},"health_degraded":{"title":"{title_or_default}","body":"{reason}","label":"Health check degraded"},"lxc_updates_available":{"title":"{hostname}: {count} LXC(s) with package updates available","body":"📊 {count} LXC(s) with pending package updates (📦 {total_packages} total, 🔒 {security_count} security):\n\n{ct_list}","label":"LXC updates available (experimental)"},"lxc_update_applied":{"title":"{hostname}: LXC {ct_name} ({vmid}) update {result}","body":"{details}","label":"LXC update applied"},"app_update_available":{"title":"{hostname}: {app_name} update available on CT {vmid}","body":"{app_name} on CT {vmid} ({ct_name}) has a new version:\n {installed} → {latest}","label":"App update available"},"docker_stack_update_available":{"title":"{hostname}: Docker updates available on CT {vmid}","body":"Container {ct_name} (CT {vmid}) has {count} Docker update(s):\n{details}","label":"Docker updates available"},"vm_start":{"title":"{hostname}: VM {vmname} ({vmid}) started","body":"Virtual machine {vmname} (ID: {vmid}) is now running.","label":"VM started"},"vm_start_warning":{"title":"{hostname}: VM {vmname} ({vmid}) started with warnings","body":"Virtual machine {vmname} (ID: {vmid}) started successfully but has warnings.\nWarnings: {reason}","label":"VM started (warnings)"},"vm_stop":{"title":"{hostname}: VM {vmname} ({vmid}) stopped","body":"Virtual machine {vmname} (ID: {vmid}) has been stopped.","label":"VM stopped"},"vm_shutdown":{"title":"{hostname}: VM {vmname} ({vmid}) shut down","body":"Virtual machine {vmname} (ID: {vmid}) has been cleanly shut down.","label":"VM shutdown"},"vm_fail":{"title":"{hostname}: VM {vmname} ({vmid}) FAILED","body":"Virtual machine {vmname} (ID: {vmid}) has crashed or failed to start.\nReason: {reason}","label":"VM FAILED"},"vm_restart":{"title":"{hostname}: VM {vmname} ({vmid}) restarted","body":"Virtual machine {vmname} (ID: {vmid}) has been restarted.","label":"VM restarted"},"ct_start":{"title":"{hostname}: CT {vmname} ({vmid}) started","body":"Container {vmname} (ID: {vmid}) is now running.","label":"CT started"},"ct_start_warning":{"title":"{hostname}: CT {vmname} ({vmid}) started with warnings","body":"Container {vmname} (ID: {vmid}) started successfully but has warnings.\nWarnings: {reason}","label":"CT started (warnings)"},"ct_stop":{"title":"{hostname}: CT {vmname} ({vmid}) stopped","body":"Container {vmname} (ID: {vmid}) has been stopped.","label":"CT stopped"},"ct_shutdown":{"title":"{hostname}: CT {vmname} ({vmid}) shut down","body":"Container {vmname} (ID: {vmid}) has been cleanly shut down.","label":"CT shutdown"},"ct_restart":{"title":"{hostname}: CT {vmname} ({vmid}) restarted","body":"Container {vmname} (ID: {vmid}) has been restarted.","label":"CT restarted"},"ct_fail":{"title":"{hostname}: CT {vmname} ({vmid}) FAILED","body":"Container {vmname} (ID: {vmid}) has crashed or failed to start.\nReason: {reason}","label":"CT FAILED"},"migration_start":{"title":"{hostname}: Migration started — {vmname} ({vmid})","body":"Live migration of {vmname} (ID: {vmid}) to node {target_node} has started.","label":"Migration started"},"migration_complete":{"title":"{hostname}: Migration complete — {vmname} ({vmid})","body":"{vmname} (ID: {vmid}) successfully migrated to node {target_node}.","label":"Migration complete"},"migration_warning":{"title":"{hostname}: Migration complete with warnings — {vmname} ({vmid})","body":"{vmname} (ID: {vmid}) migrated to node {target_node} but encountered warnings.\nWarnings: {reason}","label":"Migration (warnings)"},"migration_fail":{"title":"{hostname}: Migration FAILED — {vmname} ({vmid})","body":"Migration of {vmname} (ID: {vmid}) to node {target_node} failed.\nReason: {reason}","label":"Migration FAILED"},"replication_fail":{"title":"{hostname}: Replication FAILED — {vmname} ({vmid})","body":"Replication of {vmname} (ID: {vmid}) failed.\nReason: {reason}","label":"Replication FAILED"},"replication_complete":{"title":"{hostname}: Replication complete — {vmname} ({vmid})","body":"Replication of {vmname} (ID: {vmid}) completed successfully.","label":"Replication complete"},"backup_start":{"title":"{hostname}: Backup started on {storage}","body":"Backup job started on storage {storage}.\n{reason}","label":"Backup started"},"backup_complete":{"title":"{hostname} → {storage}: Backup complete — {vmname} ({vmid})","body":"Backup of {vmname} (ID: {vmid}) completed successfully on {storage}.\nSize: {size}","label":"Backup complete"},"backup_warning":{"title":"{hostname} → {storage}: Backup complete with warnings — {vmname} ({vmid})","body":"Backup of {vmname} (ID: {vmid}) on {storage} completed but encountered warnings.\nWarnings: {reason}","label":"Backup (warnings)"},"backup_fail":{"title":"{hostname} → {storage}: Backup FAILED — {vmname} ({vmid})","body":"Backup of {vmname} (ID: {vmid}) failed on {storage}.\nReason: {reason}","label":"Backup FAILED"},"host_backup_start":{"title":"{hostname}: Host backup started → {backend_label}","body":"Job: {job_id}\nBackend: {backend_label}\nDestination: {destination}\nProfile: {profile_mode}","label":"Host backup started"},"host_backup_complete":{"title":"{hostname}: Host backup complete → {backend_label}","body":"Job: {job_id}\nBackend: {backend_label}\nDestination: {destination}\nData size: {data_size}\nArchive size: {archive_size}\nDuration: {duration}","label":"Host backup complete"},"host_backup_fail":{"title":"{hostname}: Host backup FAILED → {backend_label}","body":"Job: {job_id}\nBackend: {backend_label}\nDestination: {destination}\nDuration before failure: {duration}\nReason: {reason}\nLog: {log_file}","label":"Host backup FAILED"},"snapshot_complete":{"title":"{hostname}: Snapshot created — {vmname} ({vmid})","body":"Snapshot \"{snapshot_name}\" created for {vmname} (ID: {vmid}).","label":"Snapshot created"},"snapshot_fail":{"title":"{hostname}: Snapshot FAILED — {vmname} ({vmid})","body":"Snapshot creation for {vmname} (ID: {vmid}) failed.\nReason: {reason}","label":"Snapshot FAILED"},"cpu_high":{"title":"{hostname}: High CPU usage — {value}%","body":"CPU usage has reached {value}% on {cores} cores.\n{details}","label":"High CPU usage"},"ram_high":{"title":"{hostname}: High memory usage — {value}%","body":"Memory usage: {used} / {total} ({value}%).\n{details}","label":"High memory usage"},"temp_high":{"title":"{hostname}: High CPU temperature — {value}°C","body":"CPU temperature has reached {value}°C (threshold: {threshold}°C).\n{details}","label":"High temperature"},"disk_space_low":{"title":"{hostname}: Low disk space on {mount}","body":"Filesystem {mount}: {used}% used ({available} available).\nFree up disk space to avoid service disruption.","label":"Low disk space"},"disk_io_error":{"title":"{hostname}: Disk failure detected — {device}","body":"I/O error or disk failure detected on device {device}.\n{reason}","label":"Disk failure / I/O error"},"storage_unavailable":{"title":"{hostname}: Storage unavailable — {storage_name}","body":"PVE storage \"{storage_name}\" (type: {storage_type}) is not accessible.\nReason: {reason}","label":"Storage unavailable"},"smart_test_complete":{"title":"{hostname}: SMART test completed — {device}","body":"SMART {test_type} test on /dev/{device} has completed.\nResult: {result}\nDuration: {duration}","label":"SMART test completed"},"smart_test_failed":{"title":"{hostname}: SMART test FAILED — {device}","body":"SMART {test_type} test on /dev/{device} has failed.\nResult: {result}\nReason: {reason}","label":"SMART test FAILED"},"gpu_mode_switch":{"title":"{hostname}: GPU mode changed to {new_mode}","body":"GPU passthrough mode has been switched.\nGPU: {gpu_name} ({gpu_pci})\nPrevious mode: {old_mode}\nNew mode: {new_mode}\n{details}","label":"GPU mode switched"},"gpu_passthrough_blocked":{"title":"{hostname}: {guest_type} {guest_id} blocked at startup","body":"PCIe passthrough guard prevented {guest_type} {guest_id} ({guest_name}) from starting.\nReason: {reason}\n{details}","label":"GPU passthrough blocked"},"pci_passthrough_conflict":{"title":"{hostname}: PCIe device conflict detected — {device_pci}","body":"A PCIe device is assigned to multiple guests.\nDevice: {device_pci}\nConflicting guests: {guest_list}\nAction required: Stop one of the guests or reassign the device.","label":"PCIe device conflict"},"load_high":{"title":"{hostname}: High system load — {value}","body":"System load average is {value} on {cores} cores.\n{details}","label":"High system load"},"network_down":{"title":"{hostname}: Network connectivity lost{entity_suffix}","body":"The node has lost network connectivity.\nReason: {reason}","label":"Network connectivity lost"},"network_latency":{"title":"{hostname}: High network latency — {value}ms","body":"Latency to gateway: {value}ms (threshold: {threshold}ms).\nThis may indicate network congestion or hardware issues.","label":"High network latency"},"auth_fail":{"title":"{hostname}: Authentication failure","body":"Failed login attempt detected.\nSource IP: {source_ip}\nUser: {username}\nService: {service}","label":"Authentication failure"},"ip_block":{"title":"{hostname}: IP blocked by Fail2Ban","body":"IP address {source_ip} has been banned.\nJail: {jail}\nFailed attempts: {failures}","label":"IP blocked by Fail2Ban"},"firewall_issue":{"title":"{hostname}: Firewall issue detected{entity_suffix}","body":"A firewall configuration issue has been detected.\nReason: {reason}","label":"Firewall issue detected"},"user_permission_change":{"title":"{hostname}: User permission changed","body":"User: {username}\nChange: {change_details}","label":"User permission changed"},"split_brain":{"title":"{hostname}: SPLIT-BRAIN detected","body":"A cluster split-brain condition has been detected. Quorum may be lost.\nQuorum status: {quorum}","label":"SPLIT-BRAIN detected"},"node_disconnect":{"title":"{hostname}: Node {node_name} disconnected","body":"Node {node_name} has disconnected from the cluster.","label":"Node disconnected"},"node_reconnect":{"title":"{hostname}: Node {node_name} reconnected","body":"Node {node_name} has rejoined the cluster successfully.","label":"Node reconnected"},"system_startup":{"title":"{hostname}: {reason}","body":"{summary}","label":"System startup report"},"system_shutdown":{"title":"{hostname}: System shutting down","body":"The node is shutting down.\n{reason}","label":"System shutting down"},"system_reboot":{"title":"{hostname}: System rebooting","body":"The node is rebooting.\n{reason}","label":"System rebooting"},"system_restore_completed":{"title":"{hostname}: Host restore finished","body":"Post-restore tasks completed in background.\n\nGuests applied: {guests}\nBind-mount stubs: {stubs}\nStale node dirs removed: {stale_nodes}\nComponents reinstalled: {components}\nDuration: {duration}\n{warnings_block}\nThe node is now fully ready to use.","label":"Host restore completed"},"system_problem":{"title":"{hostname}: System problem detected{entity_suffix}","body":"A system-level problem has been detected.\nReason: {reason}","label":"System problem detected"},"service_fail":{"title":"{hostname}: Service failed — {service_name}","body":"System service \"{service_name}\" has failed.\nReason: {reason}","label":"Service failed"},"oom_kill":{"title":"{hostname}: OOM Kill — {process}","body":"Process \"{process}\" was killed by the Out-of-Memory manager.\n{reason}","label":"Out of memory kill"},"service_fail_batch":{"title":"{hostname}: {service_count} services failed{entity_suffix}","body":"{reason}","label":"Service fail batch"},"cron_output":{"title":"{hostname}: {pve_title}","body":"{reason}","label":"Cron job output (per-cron stdout via mail)"},"system_mail":{"title":"{hostname}: {pve_title}","body":"{reason}","label":"Smartd / mail bounces (PVE system mail)"},"apt_listchanges":{"title":"{hostname}: {pve_title}","body":"Upstream package information forwarded by Proxmox VE through apt-listchanges. The following text comes from the package maintainer and is not a ProxMenux recommendation.\n\n{reason}","label":"apt-listchanges package notices"},"webhook_test":{"title":"{hostname}: Webhook test received","body":"PVE webhook connectivity test successful.\n{reason}","label":"Webhook test"},"update_available":{"title":"{hostname}: Updates available","body":"Total updates: {total_count}\nSecurity: {security_count}\nProxmox: {pve_count}\nKernel: {kernel_count}\nImportant packages:\n{important_list}","label":"Updates available (legacy)"},"unknown_persistent":{"title":"{hostname}: Check unavailable - {category}{entity_suffix}","body":"Health check for {category} has been unavailable for 3+ cycles.\n{reason}","label":"Check unavailable"},"health_persistent":{"title":"{hostname}: {count} active health issue(s){entity_suffix}","body":"The following health issues remain unresolved:\n{issue_list}\n\nThis digest is sent once every 24 hours while issues persist.","label":"Active health issues (daily)"},"health_issue_new":{"title":"{hostname}: New health issue — {category}{entity_suffix}","body":"New {severity} issue detected in: {category}\nDetails: {reason}","label":"New health issue"},"health_issue_resolved":{"title":"{hostname}: Resolved - {category}{entity_suffix}","body":"{category} issue has been resolved.\n{reason}\nDuration: {duration}","label":"Health issue resolved"},"update_summary":{"title":"{hostname}: Updates available","body":"Total updates: {total_count}\nSecurity updates: {security_count}\nProxmox-related updates: {pve_count}\nKernel updates: {kernel_count}\nImportant packages:\n{important_list}","label":"Host package updates"},"pve_update":{"title":"{hostname}: Proxmox VE {new_version} available","body":"A new Proxmox VE release is available.\nCurrent: {current_version}\nNew: {new_version}\n{details}","label":"Proxmox VE update available"},"update_complete":{"title":"{hostname}: System update completed","body":"System packages have been successfully updated.\n{details}","label":"Host update completed"},"ai_model_migrated":{"title":"{hostname}: AI model updated","body":"The AI model for notifications has been automatically updated.\nProvider: {provider}\nPrevious model: {old_model}\nNew model: {new_model}\n\n{message}","label":"AI model auto-updated"},"proxmenux_update":{"title":"{hostname}: ProxMenux {new_version} available","body":"A new version of ProxMenux is available.\nCurrent: {current_version}\nNew: {new_version}","label":"ProxMenux update available"},"mount_stale":{"title":"{hostname}: stale remote mount {mount_target}","body":"Remote mount {mount_target} ({fstype}) from {mount_source} is stale{lxc_scope}.\nStat timed out or returned an error: {error}\n\nApps writing to this path will silently land on the underlying filesystem and may fill the disk. Remount or fix connectivity ASAP.","label":"Remote mount stale"},"mount_readonly":{"title":"{hostname}: remote mount {mount_target} is read-only","body":"Remote mount {mount_target} ({fstype}) from {mount_source} is mounted read-only{lxc_scope}. Writes will fail. If this was unintentional, remount with rw.","label":"Remote mount read-only"},"lxc_disk_low":{"title":"{hostname}: CT {vmid} rootfs at {usage_percent}%","body":"CT {vmid} ({name}) rootfs is at {usage_percent}% ({disk_bytes_human} / {maxdisk_bytes_human}).\n\nA full LXC rootfs prevents the container from booting cleanly. Either expand the rootfs (pct resize {vmid} rootfs +1G) or free space inside the container.","label":"LXC rootfs near full"},"vm_disk_low":{"title":"{hostname}: VM {vmid} filesystems at {usage_percent}%","body":"VM {vmid} ({name}) guest filesystems are at {usage_percent}% ({disk_bytes_human} / {maxdisk_bytes_human}).\n\nReported by the QEMU guest agent. Includes every persistent filesystem the guest mounts on a block device — virtual disks and PCI-passthrough drives alike. Free up space inside the guest or expand the affected storage before writes start to fail.","label":"VM filesystems near full"},"lxc_mount_low":{"title":"{hostname}: CT {vmid} mount {mount} at {usage_percent}%","body":"Mount {mount} inside CT {vmid} ({name}) is at {usage_percent}% used.\nFilesystem type: {fstype}\n\nA full mount inside a container often blocks the application silently — writes either fail or, worse, land on the rootfs and trigger the rootfs alert next. Free up space on the mount or expand it.","label":"LXC mount near full"},"pve_storage_full":{"title":"{hostname}: PVE storage {storage_name} at {usage_percent}%","body":"Proxmox storage \"{storage_name}\" (type: {storage_type}) is at {usage_percent}% used.\n\nOnce full, no new VM/CT can be provisioned and existing guests may fail to write. Move/delete unused volumes or expand the underlying pool/LV/RBD image.","label":"PVE storage near full"},"zfs_pool_full":{"title":"{hostname}: ZFS pool {pool_name} at {usage_percent}%","body":"ZFS pool \"{pool_name}\" is at {usage_percent}% capacity.\n\nZFS performance and write reliability degrade sharply above ~80% capacity (CoW needs free space for new blocks). Free up snapshots, prune old datasets, or add more vdevs to the pool.","label":"ZFS pool near full"},"post_install_update":{"title":"{hostname}: {count} ProxMenux optimization update(s) available","body":"{count} ProxMenux optimization update(s) available on this host.\n\n🛠️ Available versions:\n{tool_list}\n\n💡 Apply from:\n • ProxMenux Monitor → Settings → ProxMenux Optimizations\n • Or run the post-install menu (option 2) → \"Apply available updates\"","label":"ProxMenux optimization updates available"},"secure_gateway_update_available":{"title":"{hostname}: {app_name} update available{update_title_suffix}","body":"{app_name} (managed by ProxMenux) has 📦 {package_count} package update(s) pending in its container.\n{version_line}\n\n💡 Open ProxMenux Monitor > Settings > Secure Gateway and click \"Update\" to apply.\n\n🗂️ Packages:\n{package_list}","label":"Secure Gateway update available"},"nvidia_driver_update_available":{"title":"{hostname}: NVIDIA driver update available — v{latest_version}","body":"A newer maintenance release is available for the installed NVIDIA driver branch.\n🔹 Currently installed: v{current_version}\n🟢 Latest available: v{latest_version}\n\n{upgrade_reason}\n\n💡 To reinstall:\n • From the ProxMenux post-install menu: {menu_label}\n\nReinstalling rebuilds the DKMS module against the running kernel and requires a reboot to load the new driver.","label":"NVIDIA driver update available"},"coral_driver_update_available":{"title":"{hostname}: Coral TPU driver update available — {latest_version}","body":"A newer {variant_label} is available.\n🔹 Currently installed: {current_version}\n🟢 Latest available: {latest_version}\n\n{upgrade_reason}\n\n💡 To reinstall:\n • From the ProxMenux post-install menu: {menu_label}\n\n{reboot_note}","label":"Coral TPU driver update available"},"burst_auth_fail":{"title":"{hostname}: +{count} more auth failures in {window}","body":"+{count} additional authentication failures detected in {window} ({total_count} total).\nSources: {entity_list}","label":"Auth failures burst"},"burst_ip_block":{"title":"{hostname}: Fail2Ban banned +{count} more IPs in {window}","body":"+{count} additional IPs banned by Fail2Ban in {window} ({total_count} total).\nIPs: {entity_list}","label":"IP block burst"},"burst_disk_io":{"title":"{hostname}: +{count} more disk I/O errors on {entity_list}","body":"+{count} additional I/O errors detected in {window} ({total_count} total).\nDevices: {entity_list}","label":"Disk I/O burst"},"burst_cluster":{"title":"{hostname}: Cluster flapping detected (+{count} more changes)","body":"Cluster state changed +{count} more times in {window} ({total_count} total).\nNodes: {entity_list}","label":"Cluster flapping burst"},"burst_service_fail":{"title":"{hostname}: +{count} more services failed in {window}","body":"+{count} additional service failures detected in {window} ({total_count} total).\nThis typically indicates a node reboot or PVE service restart.\n\nAdditional failures:\n{details}","label":"Service fail burst"},"burst_system":{"title":"{hostname}: +{count} more system problems in {window}","body":"+{count} additional system problems detected in {window} ({total_count} total).\n\nAdditional issues:\n{details}","label":"System problems burst"},"burst_generic":{"title":"{hostname}: +{count} more {event_type} events in {window}","body":"+{count} additional events of type {event_type} in {window} ({total_count} total).\n\nAdditional events:\n{details}","label":"Generic burst"},"kernel_warning":{"title":"{hostname}: Kernel diagnostic event detected","body":"The kernel recorded a diagnostic event: {kernel_details}","label":"Kernel warnings and diagnostic traces"}},"lxcUpdate":{"status":{"success":"succeeded","failure":"failed","partial":"completed partially","deferred":"deferred","skipped":"skipped"},"source":{"scheduled":"Scheduled","manual":"Manual"},"sourceLabel":"Source","targets":"Targets","osPending":"OS packages pending: {before} → {after}","osUnverified":"OS packages: update command executed; result not verified","applications":"Applications: {items}","applicationsUnverified":"Applications: updater executed; no tracked version change was observed","dockerEngineChange":"Docker Engine: {before} → {after}","dockerEngineVerified":"Docker Engine: verified at {version}","dockerEngineUnverified":"Docker Engine: update command executed; version not verified","dockerImagesPending":"Docker images pending: {before} → {after}","dockerImagesChanged":"Docker images changed: {images}","deferredTargets":"Deferred targets: {targets}","reason":"Reason: {reason}","restartRequired":"Restart required: {value}","yes":"yes","no":"no","verificationPending":"Verification pending until the container is running","verificationWarning":"Verification warning: {error}","duration":"Duration: {duration}"},"fallback":{"unknownTitle":"{hostname}: {event_type}","healthCheckDegraded":"Health check degraded","none":"none"},"fields":{"vmid":"VM/CT","name":"Name","device":"Device","sourceIp":"Source IP","node":"Node","category":"Category","service":"Service","jail":"Jail","user":"User","count":"Count","window":"Window","affected":"Affected"},"vzdump":{"size":"Size: {value}","duration":"Duration: {value}","file":"File","backups":"{count} backups","failed":"{count} failed","total":"Total: {value}","time":"Time: {value}"},"startup":{"issuesTitle":"{hostname}: System startup - {count} issue(s) detected","completeTitle":"{hostname}: System startup completed","operational":"All systems operational.","vmCountOne":"{count} VM","vmCountMany":"{count} VMs","ctCountOne":"{count} CT","ctCountMany":"{count} CTs","started":"✅ {counts} started","vmFailed":"❌ VM failed: {name} - {reason}","ctFailed":"❌ CT failed: {name} - {reason}","unknownError":"unknown error","storageUnavailable":"⚠️ Storage: {count} unavailable ({names})","servicesFailed":"⚠️ Services: {count} failed ({names})","duration":"⏱️ Startup completed in {minutes} min"},"appUpdates":{"singleTitle":"{hostname}: {app_name} update available on CT {vmid}","singleBody":"{app_name} on CT {vmid} ({ct_name}) has a new version:\n {installed} → {latest}","emptyTitle":"{hostname}: Application updates available","emptyBody":"Application updates are available.","batchTitle":"{hostname}: {count} application updates available","batchLeadOneContainer":"{count} applications in {container_count} LXC container have a newer version:","batchLeadManyContainers":"{count} applications in {container_count} LXC containers have a newer version:","additional":"… {count} additional application(s)","app":"app","unknown":"unknown"},"healthDegraded":{"categories":{"cpu":"CPU usage and temperature","memory":"Memory and swap","storage":"Storage mounts and space","disks":"Disk I/O and errors","network":"Network interfaces","vms":"VMs and containers","services":"PVE services","logs":"System logs","updates":"System updates","security":"Security"},"severity":{"critical":"Critical","warning":"Warning"},"singleTitle":"{hostname}: {severity} health status – {category}","multipleTitle":"{hostname}: {count} health checks degraded","multipleLine":"• {severity}: {category}\n {reason}","reasons":{"cpuSustained":"CPU is above {threshold}% for {duration}s."}},"digest":{"title":"{hostname}: 24h summary ({timestamp})","readFailed":"digest read failed: {error}","empty":"No INFO events buffered for this digest window.","lead":"{count} INFO events grouped by category:\n","more":" • … and {count} more","footer":"(Critical/Warning events arrived at the time they happened, not in this digest.)","quietTitle":"{hostname}: {count} events buffered during Quiet Hours","groups":{"vm_ct":"VM a CT","backup":"Backup","resources":"Resources","storage":"Storage","network":"Network","security":"Security","cluster":"Cluster","services":"Services","health":"Health","updates":"Updates","hardware":"Hardware","system":"System","other":"Other"}},"test":{"title":"ProxMenux Test","welcome":"Welcome to ProxMenux Monitor!","verify":"This is a test message to verify your notification channel is working correctly.","configuration":"Channel configuration:","iconsEnabled":"Icons: enabled","iconsDisabled":"Icons: disabled","aiEnabled":"AI: enabled ({info})","aiDisabled":"AI: disabled","alerts":"You will receive alerts about system events, health status changes, and security incidents.","photoCaption":"You can use this image as the profile photo for your notification bot."},"channels":{"discord":{"category":"Category","host":"Host","severity":"Severity"},"email":{"report":"{group} Report","details":"Details","host":"Host","footer":"ProxMenux Notification Service","severity":{"critical":"Critical","warning":"Warning","info":"Information","ok":"Resolved","default":"Notice"},"groups":{"vm_ct":"Virtual Machine / Container","backup":"Backup & Snapshot","resources":"System Resources","storage":"Storage","network":"Network","security":"Security","cluster":"Cluster","services":"System Services","health":"Health Monitor","updates":"System Updates","system":"System","hardware":"Hardware","other":"System Notification"},"fields":{"vmCtId":"VM/CT ID","name":"Name","action":"Action","targetNode":"Target Node","reason":"Reason","storage":"Storage","status":"Status","size":"Size","duration":"Duration","snapshot":"Snapshot","metric":"Metric","currentValue":"Current Value","threshold":"Threshold","cpuCores":"CPU Cores","memory":"Memory","temperature":"Temperature","mountPoint":"Mount Point","usage":"Usage","available":"Available","device":"Device","severity":"Severity","storageName":"Storage Name","type":"Type","interface":"Interface","latency":"Latency","event":"Event","sourceIp":"Source IP","username":"Username","service":"Service","jail":"Jail","failures":"Failures","change":"Change","node":"Node","quorum":"Quorum","nodesAffected":"Nodes Affected","process":"Process","category":"Category","previousSeverity":"Previous Severity","activeIssues":"Active Issues","totalUpdates":"Total Updates","securityUpdates":"Security Updates","proxmoxUpdates":"Proxmox Updates","kernelUpdates":"Kernel Updates","importantPackages":"Important Packages","currentVersion":"Current Version","newVersion":"New Version"},"status":{"failed":"Failed","completed":"Completed","started":"Started"}}}}}
}
diff --git a/AppImage/messages/pt/common.json b/AppImage/messages/pt/common.json
index ef5ac2dc..9a7f57e4 100644
--- a/AppImage/messages/pt/common.json
+++ b/AppImage/messages/pt/common.json
@@ -1820,6 +1820,12 @@
"updates": {
"label": "Atualizações"
},
+ "hardware": {
+ "label": "Hardware"
+ },
+ "system": {
+ "label": "System"
+ },
"other": {
"label": "Outro"
}
@@ -1831,6 +1837,8 @@
"error_escalated": "Problema de saúde escalado",
"health_degraded": "Verificação de integridade degradada",
"lxc_updates_available": "Atualizações LXC disponíveis (experimental)",
+ "lxc_update_applied": "LXC update applied",
+ "docker_stack_update_available": "Docker updates available",
"vm_start": "VM iniciada",
"vm_start_warning": "VM iniciada (avisos)",
"vm_stop": "VM parada",
@@ -1982,7 +1990,10 @@
"systemHostname": "Nome do host do sistema",
"displayNameHint": "Nome mostrado nas notificações. Edite-o ou deixe-o em branco para usar o nome do host do sistema.",
"activeFooter": "As notificações estão ativas. Cada canal segue sua própria categoria e seleção de eventos.",
- "inactiveFooter": "Habilite notificações para receber alertas de sistema, integridade e segurança por meio dos canais configurados."
+ "inactiveFooter": "Habilite notificações para receber alertas de sistema, integridade e segurança por meio dos canais configurados.",
+ "notificationLanguage": "Notification language",
+ "selectNotificationLanguage": "Select notification language",
+ "notificationLanguageHint": "Used for deterministic runtime notifications even when AI is disabled."
},
"ai": {
"title": "Aprimoramento de IA",
@@ -6019,5 +6030,6 @@
"withoutLynis": "Run without Lynis",
"cancel": "Cancel"
}
- }
+ },
+ "runtime": {"notifications":{"templates":{"state_change":{"title":"{hostname}: {category} changed to {current}{entity_suffix}","body":"{category} status changed from {previous} to {current}.\n{reason}","label":"Health state changed"},"new_error":{"title":"{hostname}: New {severity} - {category}{entity_suffix}","body":"{reason}","label":"New health issue"},"error_resolved":{"title":"{hostname}: Resolved - {category}{entity_suffix}","body":"The {category} issue has been resolved.\n{reason}\n🚦 Previous severity: {original_severity}\n⏱️ Duration: {duration}","label":"Recovery notification"},"error_escalated":{"title":"{hostname}: Escalated to {severity} - {category}{entity_suffix}","body":"{reason}","label":"Health issue escalated"},"health_degraded":{"title":"{title_or_default}","body":"{reason}","label":"Health check degraded"},"lxc_updates_available":{"title":"{hostname}: {count} LXC(s) with package updates available","body":"📊 {count} LXC(s) with pending package updates (📦 {total_packages} total, 🔒 {security_count} security):\n\n{ct_list}","label":"LXC updates available (experimental)"},"lxc_update_applied":{"title":"{hostname}: LXC {ct_name} ({vmid}) update {result}","body":"{details}","label":"LXC update applied"},"app_update_available":{"title":"{hostname}: {app_name} update available on CT {vmid}","body":"{app_name} on CT {vmid} ({ct_name}) has a new version:\n {installed} → {latest}","label":"App update available"},"docker_stack_update_available":{"title":"{hostname}: Docker updates available on CT {vmid}","body":"Container {ct_name} (CT {vmid}) has {count} Docker update(s):\n{details}","label":"Docker updates available"},"vm_start":{"title":"{hostname}: VM {vmname} ({vmid}) started","body":"Virtual machine {vmname} (ID: {vmid}) is now running.","label":"VM started"},"vm_start_warning":{"title":"{hostname}: VM {vmname} ({vmid}) started with warnings","body":"Virtual machine {vmname} (ID: {vmid}) started successfully but has warnings.\nWarnings: {reason}","label":"VM started (warnings)"},"vm_stop":{"title":"{hostname}: VM {vmname} ({vmid}) stopped","body":"Virtual machine {vmname} (ID: {vmid}) has been stopped.","label":"VM stopped"},"vm_shutdown":{"title":"{hostname}: VM {vmname} ({vmid}) shut down","body":"Virtual machine {vmname} (ID: {vmid}) has been cleanly shut down.","label":"VM shutdown"},"vm_fail":{"title":"{hostname}: VM {vmname} ({vmid}) FAILED","body":"Virtual machine {vmname} (ID: {vmid}) has crashed or failed to start.\nReason: {reason}","label":"VM FAILED"},"vm_restart":{"title":"{hostname}: VM {vmname} ({vmid}) restarted","body":"Virtual machine {vmname} (ID: {vmid}) has been restarted.","label":"VM restarted"},"ct_start":{"title":"{hostname}: CT {vmname} ({vmid}) started","body":"Container {vmname} (ID: {vmid}) is now running.","label":"CT started"},"ct_start_warning":{"title":"{hostname}: CT {vmname} ({vmid}) started with warnings","body":"Container {vmname} (ID: {vmid}) started successfully but has warnings.\nWarnings: {reason}","label":"CT started (warnings)"},"ct_stop":{"title":"{hostname}: CT {vmname} ({vmid}) stopped","body":"Container {vmname} (ID: {vmid}) has been stopped.","label":"CT stopped"},"ct_shutdown":{"title":"{hostname}: CT {vmname} ({vmid}) shut down","body":"Container {vmname} (ID: {vmid}) has been cleanly shut down.","label":"CT shutdown"},"ct_restart":{"title":"{hostname}: CT {vmname} ({vmid}) restarted","body":"Container {vmname} (ID: {vmid}) has been restarted.","label":"CT restarted"},"ct_fail":{"title":"{hostname}: CT {vmname} ({vmid}) FAILED","body":"Container {vmname} (ID: {vmid}) has crashed or failed to start.\nReason: {reason}","label":"CT FAILED"},"migration_start":{"title":"{hostname}: Migration started — {vmname} ({vmid})","body":"Live migration of {vmname} (ID: {vmid}) to node {target_node} has started.","label":"Migration started"},"migration_complete":{"title":"{hostname}: Migration complete — {vmname} ({vmid})","body":"{vmname} (ID: {vmid}) successfully migrated to node {target_node}.","label":"Migration complete"},"migration_warning":{"title":"{hostname}: Migration complete with warnings — {vmname} ({vmid})","body":"{vmname} (ID: {vmid}) migrated to node {target_node} but encountered warnings.\nWarnings: {reason}","label":"Migration (warnings)"},"migration_fail":{"title":"{hostname}: Migration FAILED — {vmname} ({vmid})","body":"Migration of {vmname} (ID: {vmid}) to node {target_node} failed.\nReason: {reason}","label":"Migration FAILED"},"replication_fail":{"title":"{hostname}: Replication FAILED — {vmname} ({vmid})","body":"Replication of {vmname} (ID: {vmid}) failed.\nReason: {reason}","label":"Replication FAILED"},"replication_complete":{"title":"{hostname}: Replication complete — {vmname} ({vmid})","body":"Replication of {vmname} (ID: {vmid}) completed successfully.","label":"Replication complete"},"backup_start":{"title":"{hostname}: Backup started on {storage}","body":"Backup job started on storage {storage}.\n{reason}","label":"Backup started"},"backup_complete":{"title":"{hostname} → {storage}: Backup complete — {vmname} ({vmid})","body":"Backup of {vmname} (ID: {vmid}) completed successfully on {storage}.\nSize: {size}","label":"Backup complete"},"backup_warning":{"title":"{hostname} → {storage}: Backup complete with warnings — {vmname} ({vmid})","body":"Backup of {vmname} (ID: {vmid}) on {storage} completed but encountered warnings.\nWarnings: {reason}","label":"Backup (warnings)"},"backup_fail":{"title":"{hostname} → {storage}: Backup FAILED — {vmname} ({vmid})","body":"Backup of {vmname} (ID: {vmid}) failed on {storage}.\nReason: {reason}","label":"Backup FAILED"},"host_backup_start":{"title":"{hostname}: Host backup started → {backend_label}","body":"Job: {job_id}\nBackend: {backend_label}\nDestination: {destination}\nProfile: {profile_mode}","label":"Host backup started"},"host_backup_complete":{"title":"{hostname}: Host backup complete → {backend_label}","body":"Job: {job_id}\nBackend: {backend_label}\nDestination: {destination}\nData size: {data_size}\nArchive size: {archive_size}\nDuration: {duration}","label":"Host backup complete"},"host_backup_fail":{"title":"{hostname}: Host backup FAILED → {backend_label}","body":"Job: {job_id}\nBackend: {backend_label}\nDestination: {destination}\nDuration before failure: {duration}\nReason: {reason}\nLog: {log_file}","label":"Host backup FAILED"},"snapshot_complete":{"title":"{hostname}: Snapshot created — {vmname} ({vmid})","body":"Snapshot \"{snapshot_name}\" created for {vmname} (ID: {vmid}).","label":"Snapshot created"},"snapshot_fail":{"title":"{hostname}: Snapshot FAILED — {vmname} ({vmid})","body":"Snapshot creation for {vmname} (ID: {vmid}) failed.\nReason: {reason}","label":"Snapshot FAILED"},"cpu_high":{"title":"{hostname}: High CPU usage — {value}%","body":"CPU usage has reached {value}% on {cores} cores.\n{details}","label":"High CPU usage"},"ram_high":{"title":"{hostname}: High memory usage — {value}%","body":"Memory usage: {used} / {total} ({value}%).\n{details}","label":"High memory usage"},"temp_high":{"title":"{hostname}: High CPU temperature — {value}°C","body":"CPU temperature has reached {value}°C (threshold: {threshold}°C).\n{details}","label":"High temperature"},"disk_space_low":{"title":"{hostname}: Low disk space on {mount}","body":"Filesystem {mount}: {used}% used ({available} available).\nFree up disk space to avoid service disruption.","label":"Low disk space"},"disk_io_error":{"title":"{hostname}: Disk failure detected — {device}","body":"I/O error or disk failure detected on device {device}.\n{reason}","label":"Disk failure / I/O error"},"storage_unavailable":{"title":"{hostname}: Storage unavailable — {storage_name}","body":"PVE storage \"{storage_name}\" (type: {storage_type}) is not accessible.\nReason: {reason}","label":"Storage unavailable"},"smart_test_complete":{"title":"{hostname}: SMART test completed — {device}","body":"SMART {test_type} test on /dev/{device} has completed.\nResult: {result}\nDuration: {duration}","label":"SMART test completed"},"smart_test_failed":{"title":"{hostname}: SMART test FAILED — {device}","body":"SMART {test_type} test on /dev/{device} has failed.\nResult: {result}\nReason: {reason}","label":"SMART test FAILED"},"gpu_mode_switch":{"title":"{hostname}: GPU mode changed to {new_mode}","body":"GPU passthrough mode has been switched.\nGPU: {gpu_name} ({gpu_pci})\nPrevious mode: {old_mode}\nNew mode: {new_mode}\n{details}","label":"GPU mode switched"},"gpu_passthrough_blocked":{"title":"{hostname}: {guest_type} {guest_id} blocked at startup","body":"PCIe passthrough guard prevented {guest_type} {guest_id} ({guest_name}) from starting.\nReason: {reason}\n{details}","label":"GPU passthrough blocked"},"pci_passthrough_conflict":{"title":"{hostname}: PCIe device conflict detected — {device_pci}","body":"A PCIe device is assigned to multiple guests.\nDevice: {device_pci}\nConflicting guests: {guest_list}\nAction required: Stop one of the guests or reassign the device.","label":"PCIe device conflict"},"load_high":{"title":"{hostname}: High system load — {value}","body":"System load average is {value} on {cores} cores.\n{details}","label":"High system load"},"network_down":{"title":"{hostname}: Network connectivity lost{entity_suffix}","body":"The node has lost network connectivity.\nReason: {reason}","label":"Network connectivity lost"},"network_latency":{"title":"{hostname}: High network latency — {value}ms","body":"Latency to gateway: {value}ms (threshold: {threshold}ms).\nThis may indicate network congestion or hardware issues.","label":"High network latency"},"auth_fail":{"title":"{hostname}: Authentication failure","body":"Failed login attempt detected.\nSource IP: {source_ip}\nUser: {username}\nService: {service}","label":"Authentication failure"},"ip_block":{"title":"{hostname}: IP blocked by Fail2Ban","body":"IP address {source_ip} has been banned.\nJail: {jail}\nFailed attempts: {failures}","label":"IP blocked by Fail2Ban"},"firewall_issue":{"title":"{hostname}: Firewall issue detected{entity_suffix}","body":"A firewall configuration issue has been detected.\nReason: {reason}","label":"Firewall issue detected"},"user_permission_change":{"title":"{hostname}: User permission changed","body":"User: {username}\nChange: {change_details}","label":"User permission changed"},"split_brain":{"title":"{hostname}: SPLIT-BRAIN detected","body":"A cluster split-brain condition has been detected. Quorum may be lost.\nQuorum status: {quorum}","label":"SPLIT-BRAIN detected"},"node_disconnect":{"title":"{hostname}: Node {node_name} disconnected","body":"Node {node_name} has disconnected from the cluster.","label":"Node disconnected"},"node_reconnect":{"title":"{hostname}: Node {node_name} reconnected","body":"Node {node_name} has rejoined the cluster successfully.","label":"Node reconnected"},"system_startup":{"title":"{hostname}: {reason}","body":"{summary}","label":"System startup report"},"system_shutdown":{"title":"{hostname}: System shutting down","body":"The node is shutting down.\n{reason}","label":"System shutting down"},"system_reboot":{"title":"{hostname}: System rebooting","body":"The node is rebooting.\n{reason}","label":"System rebooting"},"system_restore_completed":{"title":"{hostname}: Host restore finished","body":"Post-restore tasks completed in background.\n\nGuests applied: {guests}\nBind-mount stubs: {stubs}\nStale node dirs removed: {stale_nodes}\nComponents reinstalled: {components}\nDuration: {duration}\n{warnings_block}\nThe node is now fully ready to use.","label":"Host restore completed"},"system_problem":{"title":"{hostname}: System problem detected{entity_suffix}","body":"A system-level problem has been detected.\nReason: {reason}","label":"System problem detected"},"service_fail":{"title":"{hostname}: Service failed — {service_name}","body":"System service \"{service_name}\" has failed.\nReason: {reason}","label":"Service failed"},"oom_kill":{"title":"{hostname}: OOM Kill — {process}","body":"Process \"{process}\" was killed by the Out-of-Memory manager.\n{reason}","label":"Out of memory kill"},"service_fail_batch":{"title":"{hostname}: {service_count} services failed{entity_suffix}","body":"{reason}","label":"Service fail batch"},"cron_output":{"title":"{hostname}: {pve_title}","body":"{reason}","label":"Cron job output (per-cron stdout via mail)"},"system_mail":{"title":"{hostname}: {pve_title}","body":"{reason}","label":"Smartd / mail bounces (PVE system mail)"},"apt_listchanges":{"title":"{hostname}: {pve_title}","body":"Upstream package information forwarded by Proxmox VE through apt-listchanges. The following text comes from the package maintainer and is not a ProxMenux recommendation.\n\n{reason}","label":"apt-listchanges package notices"},"webhook_test":{"title":"{hostname}: Webhook test received","body":"PVE webhook connectivity test successful.\n{reason}","label":"Webhook test"},"update_available":{"title":"{hostname}: Updates available","body":"Total updates: {total_count}\nSecurity: {security_count}\nProxmox: {pve_count}\nKernel: {kernel_count}\nImportant packages:\n{important_list}","label":"Updates available (legacy)"},"unknown_persistent":{"title":"{hostname}: Check unavailable - {category}{entity_suffix}","body":"Health check for {category} has been unavailable for 3+ cycles.\n{reason}","label":"Check unavailable"},"health_persistent":{"title":"{hostname}: {count} active health issue(s){entity_suffix}","body":"The following health issues remain unresolved:\n{issue_list}\n\nThis digest is sent once every 24 hours while issues persist.","label":"Active health issues (daily)"},"health_issue_new":{"title":"{hostname}: New health issue — {category}{entity_suffix}","body":"New {severity} issue detected in: {category}\nDetails: {reason}","label":"New health issue"},"health_issue_resolved":{"title":"{hostname}: Resolved - {category}{entity_suffix}","body":"{category} issue has been resolved.\n{reason}\nDuration: {duration}","label":"Health issue resolved"},"update_summary":{"title":"{hostname}: Updates available","body":"Total updates: {total_count}\nSecurity updates: {security_count}\nProxmox-related updates: {pve_count}\nKernel updates: {kernel_count}\nImportant packages:\n{important_list}","label":"Host package updates"},"pve_update":{"title":"{hostname}: Proxmox VE {new_version} available","body":"A new Proxmox VE release is available.\nCurrent: {current_version}\nNew: {new_version}\n{details}","label":"Proxmox VE update available"},"update_complete":{"title":"{hostname}: System update completed","body":"System packages have been successfully updated.\n{details}","label":"Host update completed"},"ai_model_migrated":{"title":"{hostname}: AI model updated","body":"The AI model for notifications has been automatically updated.\nProvider: {provider}\nPrevious model: {old_model}\nNew model: {new_model}\n\n{message}","label":"AI model auto-updated"},"proxmenux_update":{"title":"{hostname}: ProxMenux {new_version} available","body":"A new version of ProxMenux is available.\nCurrent: {current_version}\nNew: {new_version}","label":"ProxMenux update available"},"mount_stale":{"title":"{hostname}: stale remote mount {mount_target}","body":"Remote mount {mount_target} ({fstype}) from {mount_source} is stale{lxc_scope}.\nStat timed out or returned an error: {error}\n\nApps writing to this path will silently land on the underlying filesystem and may fill the disk. Remount or fix connectivity ASAP.","label":"Remote mount stale"},"mount_readonly":{"title":"{hostname}: remote mount {mount_target} is read-only","body":"Remote mount {mount_target} ({fstype}) from {mount_source} is mounted read-only{lxc_scope}. Writes will fail. If this was unintentional, remount with rw.","label":"Remote mount read-only"},"lxc_disk_low":{"title":"{hostname}: CT {vmid} rootfs at {usage_percent}%","body":"CT {vmid} ({name}) rootfs is at {usage_percent}% ({disk_bytes_human} / {maxdisk_bytes_human}).\n\nA full LXC rootfs prevents the container from booting cleanly. Either expand the rootfs (pct resize {vmid} rootfs +1G) or free space inside the container.","label":"LXC rootfs near full"},"vm_disk_low":{"title":"{hostname}: VM {vmid} filesystems at {usage_percent}%","body":"VM {vmid} ({name}) guest filesystems are at {usage_percent}% ({disk_bytes_human} / {maxdisk_bytes_human}).\n\nReported by the QEMU guest agent. Includes every persistent filesystem the guest mounts on a block device — virtual disks and PCI-passthrough drives alike. Free up space inside the guest or expand the affected storage before writes start to fail.","label":"VM filesystems near full"},"lxc_mount_low":{"title":"{hostname}: CT {vmid} mount {mount} at {usage_percent}%","body":"Mount {mount} inside CT {vmid} ({name}) is at {usage_percent}% used.\nFilesystem type: {fstype}\n\nA full mount inside a container often blocks the application silently — writes either fail or, worse, land on the rootfs and trigger the rootfs alert next. Free up space on the mount or expand it.","label":"LXC mount near full"},"pve_storage_full":{"title":"{hostname}: PVE storage {storage_name} at {usage_percent}%","body":"Proxmox storage \"{storage_name}\" (type: {storage_type}) is at {usage_percent}% used.\n\nOnce full, no new VM/CT can be provisioned and existing guests may fail to write. Move/delete unused volumes or expand the underlying pool/LV/RBD image.","label":"PVE storage near full"},"zfs_pool_full":{"title":"{hostname}: ZFS pool {pool_name} at {usage_percent}%","body":"ZFS pool \"{pool_name}\" is at {usage_percent}% capacity.\n\nZFS performance and write reliability degrade sharply above ~80% capacity (CoW needs free space for new blocks). Free up snapshots, prune old datasets, or add more vdevs to the pool.","label":"ZFS pool near full"},"post_install_update":{"title":"{hostname}: {count} ProxMenux optimization update(s) available","body":"{count} ProxMenux optimization update(s) available on this host.\n\n🛠️ Available versions:\n{tool_list}\n\n💡 Apply from:\n • ProxMenux Monitor → Settings → ProxMenux Optimizations\n • Or run the post-install menu (option 2) → \"Apply available updates\"","label":"ProxMenux optimization updates available"},"secure_gateway_update_available":{"title":"{hostname}: {app_name} update available{update_title_suffix}","body":"{app_name} (managed by ProxMenux) has 📦 {package_count} package update(s) pending in its container.\n{version_line}\n\n💡 Open ProxMenux Monitor > Settings > Secure Gateway and click \"Update\" to apply.\n\n🗂️ Packages:\n{package_list}","label":"Secure Gateway update available"},"nvidia_driver_update_available":{"title":"{hostname}: NVIDIA driver update available — v{latest_version}","body":"A newer maintenance release is available for the installed NVIDIA driver branch.\n🔹 Currently installed: v{current_version}\n🟢 Latest available: v{latest_version}\n\n{upgrade_reason}\n\n💡 To reinstall:\n • From the ProxMenux post-install menu: {menu_label}\n\nReinstalling rebuilds the DKMS module against the running kernel and requires a reboot to load the new driver.","label":"NVIDIA driver update available"},"coral_driver_update_available":{"title":"{hostname}: Coral TPU driver update available — {latest_version}","body":"A newer {variant_label} is available.\n🔹 Currently installed: {current_version}\n🟢 Latest available: {latest_version}\n\n{upgrade_reason}\n\n💡 To reinstall:\n • From the ProxMenux post-install menu: {menu_label}\n\n{reboot_note}","label":"Coral TPU driver update available"},"burst_auth_fail":{"title":"{hostname}: +{count} more auth failures in {window}","body":"+{count} additional authentication failures detected in {window} ({total_count} total).\nSources: {entity_list}","label":"Auth failures burst"},"burst_ip_block":{"title":"{hostname}: Fail2Ban banned +{count} more IPs in {window}","body":"+{count} additional IPs banned by Fail2Ban in {window} ({total_count} total).\nIPs: {entity_list}","label":"IP block burst"},"burst_disk_io":{"title":"{hostname}: +{count} more disk I/O errors on {entity_list}","body":"+{count} additional I/O errors detected in {window} ({total_count} total).\nDevices: {entity_list}","label":"Disk I/O burst"},"burst_cluster":{"title":"{hostname}: Cluster flapping detected (+{count} more changes)","body":"Cluster state changed +{count} more times in {window} ({total_count} total).\nNodes: {entity_list}","label":"Cluster flapping burst"},"burst_service_fail":{"title":"{hostname}: +{count} more services failed in {window}","body":"+{count} additional service failures detected in {window} ({total_count} total).\nThis typically indicates a node reboot or PVE service restart.\n\nAdditional failures:\n{details}","label":"Service fail burst"},"burst_system":{"title":"{hostname}: +{count} more system problems in {window}","body":"+{count} additional system problems detected in {window} ({total_count} total).\n\nAdditional issues:\n{details}","label":"System problems burst"},"burst_generic":{"title":"{hostname}: +{count} more {event_type} events in {window}","body":"+{count} additional events of type {event_type} in {window} ({total_count} total).\n\nAdditional events:\n{details}","label":"Generic burst"},"kernel_warning":{"title":"{hostname}: Kernel diagnostic event detected","body":"The kernel recorded a diagnostic event: {kernel_details}","label":"Kernel warnings and diagnostic traces"}},"lxcUpdate":{"status":{"success":"succeeded","failure":"failed","partial":"completed partially","deferred":"deferred","skipped":"skipped"},"source":{"scheduled":"Scheduled","manual":"Manual"},"sourceLabel":"Source","targets":"Targets","osPending":"OS packages pending: {before} → {after}","osUnverified":"OS packages: update command executed; result not verified","applications":"Applications: {items}","applicationsUnverified":"Applications: updater executed; no tracked version change was observed","dockerEngineChange":"Docker Engine: {before} → {after}","dockerEngineVerified":"Docker Engine: verified at {version}","dockerEngineUnverified":"Docker Engine: update command executed; version not verified","dockerImagesPending":"Docker images pending: {before} → {after}","dockerImagesChanged":"Docker images changed: {images}","deferredTargets":"Deferred targets: {targets}","reason":"Reason: {reason}","restartRequired":"Restart required: {value}","yes":"yes","no":"no","verificationPending":"Verification pending until the container is running","verificationWarning":"Verification warning: {error}","duration":"Duration: {duration}"},"fallback":{"unknownTitle":"{hostname}: {event_type}","healthCheckDegraded":"Health check degraded","none":"none"},"fields":{"vmid":"VM/CT","name":"Name","device":"Device","sourceIp":"Source IP","node":"Node","category":"Category","service":"Service","jail":"Jail","user":"User","count":"Count","window":"Window","affected":"Affected"},"vzdump":{"size":"Size: {value}","duration":"Duration: {value}","file":"File","backups":"{count} backups","failed":"{count} failed","total":"Total: {value}","time":"Time: {value}"},"startup":{"issuesTitle":"{hostname}: System startup - {count} issue(s) detected","completeTitle":"{hostname}: System startup completed","operational":"All systems operational.","vmCountOne":"{count} VM","vmCountMany":"{count} VMs","ctCountOne":"{count} CT","ctCountMany":"{count} CTs","started":"✅ {counts} started","vmFailed":"❌ VM failed: {name} - {reason}","ctFailed":"❌ CT failed: {name} - {reason}","unknownError":"unknown error","storageUnavailable":"⚠️ Storage: {count} unavailable ({names})","servicesFailed":"⚠️ Services: {count} failed ({names})","duration":"⏱️ Startup completed in {minutes} min"},"appUpdates":{"singleTitle":"{hostname}: {app_name} update available on CT {vmid}","singleBody":"{app_name} on CT {vmid} ({ct_name}) has a new version:\n {installed} → {latest}","emptyTitle":"{hostname}: Application updates available","emptyBody":"Application updates are available.","batchTitle":"{hostname}: {count} application updates available","batchLeadOneContainer":"{count} applications in {container_count} LXC container have a newer version:","batchLeadManyContainers":"{count} applications in {container_count} LXC containers have a newer version:","additional":"… {count} additional application(s)","app":"app","unknown":"unknown"},"healthDegraded":{"categories":{"cpu":"CPU usage and temperature","memory":"Memory and swap","storage":"Storage mounts and space","disks":"Disk I/O and errors","network":"Network interfaces","vms":"VMs and containers","services":"PVE services","logs":"System logs","updates":"System updates","security":"Security"},"severity":{"critical":"Critical","warning":"Warning"},"singleTitle":"{hostname}: {severity} health status – {category}","multipleTitle":"{hostname}: {count} health checks degraded","multipleLine":"• {severity}: {category}\n {reason}","reasons":{"cpuSustained":"CPU is above {threshold}% for {duration}s."}},"digest":{"title":"{hostname}: 24h summary ({timestamp})","readFailed":"digest read failed: {error}","empty":"No INFO events buffered for this digest window.","lead":"{count} INFO events grouped by category:\n","more":" • … and {count} more","footer":"(Critical/Warning events arrived at the time they happened, not in this digest.)","quietTitle":"{hostname}: {count} events buffered during Quiet Hours","groups":{"vm_ct":"VM a CT","backup":"Backup","resources":"Resources","storage":"Storage","network":"Network","security":"Security","cluster":"Cluster","services":"Services","health":"Health","updates":"Updates","hardware":"Hardware","system":"System","other":"Other"}},"test":{"title":"ProxMenux Test","welcome":"Welcome to ProxMenux Monitor!","verify":"This is a test message to verify your notification channel is working correctly.","configuration":"Channel configuration:","iconsEnabled":"Icons: enabled","iconsDisabled":"Icons: disabled","aiEnabled":"AI: enabled ({info})","aiDisabled":"AI: disabled","alerts":"You will receive alerts about system events, health status changes, and security incidents.","photoCaption":"You can use this image as the profile photo for your notification bot."},"channels":{"discord":{"category":"Category","host":"Host","severity":"Severity"},"email":{"report":"{group} Report","details":"Details","host":"Host","footer":"ProxMenux Notification Service","severity":{"critical":"Critical","warning":"Warning","info":"Information","ok":"Resolved","default":"Notice"},"groups":{"vm_ct":"Virtual Machine / Container","backup":"Backup & Snapshot","resources":"System Resources","storage":"Storage","network":"Network","security":"Security","cluster":"Cluster","services":"System Services","health":"Health Monitor","updates":"System Updates","system":"System","hardware":"Hardware","other":"System Notification"},"fields":{"vmCtId":"VM/CT ID","name":"Name","action":"Action","targetNode":"Target Node","reason":"Reason","storage":"Storage","status":"Status","size":"Size","duration":"Duration","snapshot":"Snapshot","metric":"Metric","currentValue":"Current Value","threshold":"Threshold","cpuCores":"CPU Cores","memory":"Memory","temperature":"Temperature","mountPoint":"Mount Point","usage":"Usage","available":"Available","device":"Device","severity":"Severity","storageName":"Storage Name","type":"Type","interface":"Interface","latency":"Latency","event":"Event","sourceIp":"Source IP","username":"Username","service":"Service","jail":"Jail","failures":"Failures","change":"Change","node":"Node","quorum":"Quorum","nodesAffected":"Nodes Affected","process":"Process","category":"Category","previousSeverity":"Previous Severity","activeIssues":"Active Issues","totalUpdates":"Total Updates","securityUpdates":"Security Updates","proxmoxUpdates":"Proxmox Updates","kernelUpdates":"Kernel Updates","importantPackages":"Important Packages","currentVersion":"Current Version","newVersion":"New Version"},"status":{"failed":"Failed","completed":"Completed","started":"Started"}}}}}
}
diff --git a/AppImage/messages/sk/common.json b/AppImage/messages/sk/common.json
index 99c0c97f..5f7ce11d 100644
--- a/AppImage/messages/sk/common.json
+++ b/AppImage/messages/sk/common.json
@@ -1819,6 +1819,12 @@
"updates": {
"label": "Aktualizácie"
},
+ "hardware": {
+ "label": "Hardvér"
+ },
+ "system": {
+ "label": "Systém"
+ },
"other": {
"label": "Ostatné"
}
@@ -1830,6 +1836,8 @@
"error_escalated": "Problém sa zhoršil",
"health_degraded": "Zhoršený stav kontroly",
"lxc_updates_available": "Dostupné aktualizácie LXC (experimentálne)",
+ "lxc_update_applied": "Aktualizácia LXC bola vykonaná",
+ "docker_stack_update_available": "Dostupné aktualizácie Dockeru",
"vm_start": "VM bola spustená",
"vm_start_warning": "VM bola spustená s upozorneniami",
"vm_stop": "VM bola zastavená",
@@ -1981,7 +1989,10 @@
"systemHostname": "Názov systému",
"displayNameHint": "Meno zobrazené v notifikáciách. Upravte ho alebo nechajte prázdne a použije sa názov systému.",
"activeFooter": "Notifikácie sú aktívne. Každý kanál posiela udalosti podľa vlastného výberu kategórií.",
- "inactiveFooter": "Zapnite notifikácie a dostávajte upozornenia o systéme, stave a bezpečnosti cez nastavené kanály."
+ "inactiveFooter": "Zapnite notifikácie a dostávajte upozornenia o systéme, stave a bezpečnosti cez nastavené kanály.",
+ "notificationLanguage": "Jazyk notifikácií",
+ "selectNotificationLanguage": "Vyberte jazyk notifikácií",
+ "notificationLanguageHint": "Používa sa pre spoľahlivé runtime notifikácie aj vtedy, keď je AI vypnutá."
},
"ai": {
"title": "AI vylepšenie",
@@ -6083,5 +6094,6 @@
"withoutLynis": "Spustiť bez Lynisu",
"cancel": "Zrušiť"
}
- }
+ },
+ "runtime": {"notifications":{"templates":{"state_change":{"title":"{hostname}: {category} sa zmenilo na {current}{entity_suffix}","body":"Stav kategórie {category} sa zmenil z {previous} na {current}.\n{reason}","label":"Zmena stavu kontroly"},"new_error":{"title":"{hostname}: Nový {severity} - {category}{entity_suffix}","body":"{reason}","label":"Nový problém so stavom"},"error_resolved":{"title":"{hostname}: Vyriešené - {category}{entity_suffix}","body":"Problém v kategórii {category} bol vyriešený.\n{reason}\n🚦 Predchádzajúca závažnosť: {original_severity}\n⏱️ Trvanie: {duration}","label":"Problém bol vyriešený"},"error_escalated":{"title":"{hostname}: Zvýšené na {severity} - {category}{entity_suffix}","body":"{reason}","label":"Problém sa zhoršil"},"health_degraded":{"title":"{title_or_default}","body":"{reason}","label":"Zhoršený stav kontroly"},"lxc_updates_available":{"title":"{hostname}: Aktualizácie balíkov sú dostupné pre {count} LXC","body":"📊 Čakajúce aktualizácie balíkov má {count} LXC (📦 spolu {total_packages}, 🔒 bezpečnostné {security_count}):\n\n{ct_list}","label":"Dostupné aktualizácie LXC (experimentálne)"},"lxc_update_applied":{"title":"{hostname}: LXC {ct_name} ({vmid}) aktualizácia {result}","body":"{details}","label":"Aktualizácia LXC použitá"},"app_update_available":{"title":"{hostname}: Pre {app_name} je na CT {vmid} dostupná aktualizácia","body":"Aplikácia {app_name} na CT {vmid} ({ct_name}) má novú verziu:\n {installed} → {latest}","label":"K dispozícii je aktualizácia aplikácie"},"docker_stack_update_available":{"title":"{hostname}: Na CT {vmid} sú dostupné aktualizácie Docker","body":"Kontajner {ct_name} (CT {vmid}) má {count} aktualizácií Docker:\n{details}","label":"Dostupné aktualizácie Docker"},"vm_start":{"title":"{hostname}: VM {vmname} ({vmid}) spustený","body":"Virtuálny stroj {vmname} (ID: {vmid}) je spustený.","label":"VM bola spustená"},"vm_start_warning":{"title":"{hostname}: VM {vmname} ({vmid}) spustený s upozorneniami","body":"Virtuálny stroj {vmname} (ID: {vmid}) sa úspešne spustil, ale obsahuje upozornenia.\nUpozornenia: {reason}","label":"VM bola spustená s upozorneniami"},"vm_stop":{"title":"{hostname}: VM {vmname} ({vmid}) zastavený","body":"Virtuálny stroj {vmname} (ID: {vmid}) bol zastavený.","label":"VM bola zastavená"},"vm_shutdown":{"title":"{hostname}: VM {vmname} ({vmid}) vypnutý","body":"Virtuálny stroj {vmname} (ID: {vmid}) bol korektne vypnutý.","label":"VM bola vypnutá"},"vm_fail":{"title":"{hostname}: VM {vmname} ({vmid}) ZLYHAL","body":"Virtuálny stroj {vmname} (ID: {vmid}) zlyhal alebo sa ho nepodarilo spustiť.\nDôvod: {reason}","label":"Spustenie VM zlyhalo"},"vm_restart":{"title":"{hostname}: VM {vmname} ({vmid}) reštartovaný","body":"Virtuálny stroj {vmname} (ID: {vmid}) bol reštartovaný.","label":"VM bola reštartovaná"},"ct_start":{"title":"{hostname}: CT {vmname} ({vmid}) spustený","body":"Kontajner {vmname} (ID: {vmid}) je spustený.","label":"Kontajner bol spustený"},"ct_start_warning":{"title":"{hostname}: CT {vmname} ({vmid}) spustený s upozorneniami","body":"Kontajner {vmname} (ID: {vmid}) sa úspešne spustil, ale obsahuje upozornenia.\nUpozornenia: {reason}","label":"Kontajner bol spustený s upozorneniami"},"ct_stop":{"title":"{hostname}: CT {vmname} ({vmid}) zastavený","body":"Kontajner {vmname} (ID: {vmid}) bol zastavený.","label":"Kontajner bol zastavený"},"ct_shutdown":{"title":"{hostname}: CT {vmname} ({vmid}) vypnutý","body":"Kontajner {vmname} (ID: {vmid}) bol korektne vypnutý.","label":"Kontajner bol vypnutý"},"ct_restart":{"title":"{hostname}: CT {vmname} ({vmid}) reštartovaný","body":"Kontajner {vmname} (ID: {vmid}) bol reštartovaný.","label":"Kontajner bol reštartovaný"},"ct_fail":{"title":"{hostname}: CT {vmname} ({vmid}) ZLYHAL","body":"Kontajner {vmname} (ID: {vmid}) zlyhal alebo sa ho nepodarilo spustiť.\nDôvod: {reason}","label":"Spustenie kontajnera zlyhalo"},"migration_start":{"title":"{hostname}: Migrácia spustená — {vmname} ({vmid})","body":"Živá migrácia VM alebo LXC {vmname} (ID: {vmid}) na uzol {target_node} sa začala.","label":"Migrácia sa začala"},"migration_complete":{"title":"{hostname}: Migrácia dokončená — {vmname} ({vmid})","body":"{vmname} (ID: {vmid}) bol úspešne presunutý na uzol {target_node}.","label":"Migrácia bola dokončená"},"migration_warning":{"title":"{hostname}: Migrácia dokončená s upozorneniami — {vmname} ({vmid})","body":"VM alebo LXC {vmname} (ID: {vmid}) bol presunutý na uzol {target_node}, ale vyskytli sa upozornenia.\nUpozornenia: {reason}","label":"Migrácia skončila s upozorneniami"},"migration_fail":{"title":"{hostname}: Migrácia ZLYHALA — {vmname} ({vmid})","body":"Migrácia VM alebo LXC {vmname} (ID: {vmid}) na uzol {target_node} zlyhala.\nDôvod: {reason}","label":"Migrácia zlyhala"},"replication_fail":{"title":"{hostname}: Replikácia ZLYHALA — {vmname} ({vmid})","body":"Replikácia VM alebo LXC {vmname} (ID: {vmid}) zlyhala.\nDôvod: {reason}","label":"Replikácia zlyhala"},"replication_complete":{"title":"{hostname}: Replikácia dokončená — {vmname} ({vmid})","body":"Replikácia VM alebo LXC {vmname} (ID: {vmid}) bola úspešne dokončená.","label":"Replikácia bola dokončená"},"backup_start":{"title":"{hostname}: Záloha na úložisko {storage} spustená","body":"Úloha zálohovania na úložisko {storage} sa začala.\n{reason}","label":"Zálohovanie sa začalo"},"backup_complete":{"title":"{hostname} → {storage}: Záloha dokončená — {vmname} ({vmid})","body":"Záloha {vmname} (ID: {vmid}) na úložisku {storage} bola úspešne dokončená.\nVeľkosť: {size}","label":"Záloha bola dokončená"},"backup_warning":{"title":"{hostname} → {storage}: Záloha dokončená s upozorneniami — {vmname} ({vmid})","body":"Záloha {vmname} (ID: {vmid}) na úložisku {storage} bola dokončená, ale vyskytli sa upozornenia.\nUpozornenia: {reason}","label":"Záloha skončila s upozorneniami"},"backup_fail":{"title":"{hostname} → {storage}: Záloha ZLYHALA — {vmname} ({vmid})","body":"Záloha {vmname} (ID: {vmid}) na úložisku {storage} zlyhala.\nDôvod: {reason}","label":"Zálohovanie zlyhalo"},"host_backup_start":{"title":"{hostname}: Záloha servera spustená → {backend_label}","body":"Úloha: {job_id}\nBackend: {backend_label}\nCieľ: {destination}\nProfil: {profile_mode}","label":"Zálohovanie servera sa začalo"},"host_backup_complete":{"title":"{hostname}: Záloha servera dokončená → {backend_label}","body":"Úloha: {job_id}\nBackend: {backend_label}\nCieľ: {destination}\nVeľkosť dát: {data_size}\nVeľkosť archívu: {archive_size}\nTrvanie: {duration}","label":"Záloha servera bola dokončená"},"host_backup_fail":{"title":"{hostname}: Záloha servera ZLYHALA → {backend_label}","body":"Úloha: {job_id}\nBackend: {backend_label}\nCieľ: {destination}\nTrvanie pred zlyhaním: {duration}\nDôvod: {reason}\nZáznam: {log_file}","label":"Zálohovanie servera zlyhalo"},"snapshot_complete":{"title":"{hostname}: Snímka vytvorená — {vmname} ({vmid})","body":"Snímka „{snapshot_name}“ bola vytvorená pre {vmname} (ID: {vmid}).","label":"Snapshot bol vytvorený"},"snapshot_fail":{"title":"{hostname}: Vytvorenie snímky ZLYHALO — {vmname} ({vmid})","body":"Vytvorenie snímky pre {vmname} (ID: {vmid}) zlyhalo.\nDôvod: {reason}","label":"Vytvorenie snapshotu zlyhalo"},"cpu_high":{"title":"{hostname}: Vysoké využitie CPU — {value}%","body":"Využitie CPU dosiahlo {value}% na {cores} jadrách.\n{details}","label":"Vysoké využitie CPU"},"ram_high":{"title":"{hostname}: Vysoké využitie pamäte — {value}%","body":"Využitie pamäte: {used} / {total} ({value}%).\n{details}","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}","label":"Vysoká teplota"},"disk_space_low":{"title":"{hostname}: Málo miesta na disku {mount}","body":"Súborový systém {mount}: {used}% využité ({available} voľných).\nUvoľnite miesto na disku, aby ste predišli výpadku služieb.","label":"Málo miesta na disku"},"disk_io_error":{"title":"{hostname}: Zistené zlyhanie disku — {device}","body":"Na zariadení bola zistená chyba I/O alebo zlyhanie disku {device}.\n{reason}","label":"Chyba disku alebo I/O"},"storage_unavailable":{"title":"{hostname}: Úložisko nedostupné — {storage_name}","body":"Úložisko PVE „{storage_name}“ (typ: {storage_type}) nie je dostupné.\nDôvod: {reason}","label":"Úložisko nie je dostupné"},"smart_test_complete":{"title":"{hostname}: Test SMART dokončený — {device}","body":"Test SMART typu {test_type} na /dev/{device} bol dokončený.\nVýsledok: {result}\nTrvanie: {duration}","label":"SMART test bol dokončený"},"smart_test_failed":{"title":"{hostname}: Test SMART ZLYHAL — {device}","body":"Test SMART typu {test_type} na /dev/{device} zlyhal.\nVýsledok: {result}\nDôvod: {reason}","label":"SMART test zlyhal"},"gpu_mode_switch":{"title":"{hostname}: Režim GPU zmenený na {new_mode}","body":"Režim priameho priradenia GPU bol zmenený.\nGPU: {gpu_name} ({gpu_pci})\nPredchádzajúci režim: {old_mode}\nNový režim: {new_mode}\n{details}","label":"Režim GPU bol zmenený"},"gpu_passthrough_blocked":{"title":"{hostname}: {guest_type} {guest_id} zablokovaný pri spustení","body":"Ochrana priameho priradenia PCIe zabránila spusteniu {guest_type} {guest_id} ({guest_name}).\nDôvod: {reason}\n{details}","label":"GPU passthrough bol zablokovaný"},"pci_passthrough_conflict":{"title":"{hostname}: Zistený konflikt zariadenia PCIe — {device_pci}","body":"Zariadenie PCIe je priradené viacerým VM alebo LXC.\nZariadenie: {device_pci}\nKonfliktné VM a LXC: {guest_list}\nVyžaduje sa zásah: zastavte jednu z VM alebo LXC alebo zmeňte priradenie zariadenia.","label":"Konflikt PCIe zariadenia"},"load_high":{"title":"{hostname}: Vysoká záťaž systému — {value}","body":"Priemerná záťaž systému je {value} na {cores} jadrách.\n{details}","label":"Vysoká záťaž systému"},"network_down":{"title":"{hostname}: Sieťové pripojenie prerušené{entity_suffix}","body":"Uzol stratil sieťové pripojenie.\nDôvod: {reason}","label":"Sieťové pripojenie bolo prerušené"},"network_latency":{"title":"{hostname}: Vysoká latencia siete — {value}ms","body":"Latencia k bráne: {value}ms (prahová hodnota: {threshold}ms).\nMôže to znamenať preťaženie siete alebo problém s hardvérom.","label":"Vysoká odozva siete"},"auth_fail":{"title":"{hostname}: Zlyhanie overenia","body":"Bol zistený neúspešný pokus o prihlásenie.\nZdrojová IP: {source_ip}\nPoužívateľ: {username}\nSlužba: {service}","label":"Neúspešné prihlásenie"},"ip_block":{"title":"{hostname}: IP zablokovaná cez Fail2Ban","body":"IP adresa {source_ip} bola zablokovaná.\nJail: {jail}\nNeúspešné pokusy: {failures}","label":"Fail2Ban zablokoval IP adresu"},"firewall_issue":{"title":"{hostname}: Zistený problém firewallu{entity_suffix}","body":"Bol zistený problém v konfigurácii firewallu.\nDôvod: {reason}","label":"Zistený problém s firewallom"},"user_permission_change":{"title":"{hostname}: Zmenené oprávnenie používateľa","body":"Používateľ: {username}\nZmena: {change_details}","label":"Zmena oprávnení používateľa"},"split_brain":{"title":"{hostname}: Zistený SPLIT-BRAIN","body":"V klastri bol zistený stav split-brain. Kvórum môže byť stratené.\nStav kvóra: {quorum}","label":"Zistený split-brain"},"node_disconnect":{"title":"{hostname}: Uzol {node_name} odpojený","body":"Uzol {node_name} sa odpojil od klastra.","label":"Server sa odpojil"},"node_reconnect":{"title":"{hostname}: Uzol {node_name} znovu pripojený","body":"Uzol {node_name} sa úspešne znovu pripojil ku klastru.","label":"Server sa znova pripojil"},"system_startup":{"title":"{hostname}: {reason}","body":"{summary}","label":"Súhrn po spustení systému"},"system_shutdown":{"title":"{hostname}: Systém sa vypína","body":"Uzol sa vypína.\n{reason}","label":"Systém sa vypína"},"system_reboot":{"title":"{hostname}: Systém sa reštartuje","body":"Uzol sa reštartuje.\n{reason}","label":"Systém sa reštartuje"},"system_restore_completed":{"title":"{hostname}: Obnova servera dokončená","body":"Úlohy po obnove boli dokončené na pozadí.\n\nPoužité VM a LXC: {guests}\nZástupné priečinky bind mountov: {stubs}\nOdstránené zastarané priečinky uzlov: {stale_nodes}\nPreinštalované súčasti: {components}\nTrvanie: {duration}\n{warnings_block}\nUzol je teraz úplne pripravený na použitie.","label":"Obnova servera bola dokončená"},"system_problem":{"title":"{hostname}: Zistený systémový problém{entity_suffix}","body":"Bol zistený problém na úrovni systému.\nDôvod: {reason}","label":"Zistený problém so systémom"},"service_fail":{"title":"{hostname}: Služba zlyhala — {service_name}","body":"Systémová služba \"{service_name}\" zlyhala.\nDôvod: {reason}","label":"Služba zlyhala"},"oom_kill":{"title":"{hostname}: Ukončenie pri nedostatku pamäte — {process}","body":"Proces „{process}“ bol ukončený správcom pri nedostatku pamäte.\n{reason}","label":"Proces bol ukončený pre nedostatok pamäte"},"service_fail_batch":{"title":"{hostname}: {service_count} zlyhaných služieb{entity_suffix}","body":"{reason}","label":"Zlyhanie viacerých služieb"},"cron_output":{"title":"{hostname}: {pve_title}","body":"{reason}","label":"Výstup úlohy cron"},"system_mail":{"title":"{hostname}: {pve_title}","body":"{reason}","label":"Správy Smartd a systému"},"apt_listchanges":{"title":"{hostname}: {pve_title}","body":"Informácie o balíkoch od ich tvorcov preposlané systémom Proxmox VE cez apt-listchanges. Nasledujúci text pochádza od správcu balíka a nie je odporúčaním ProxMenux.\n\n{reason}","label":"Oznámenia o balíkoch apt-listchanges"},"webhook_test":{"title":"{hostname}: Prijatý test webhooku","body":"Test pripojenia webhooku PVE bol úspešný.\n{reason}","label":"Test webhooku"},"update_available":{"title":"{hostname}: Dostupné aktualizácie","body":"Aktualizácie spolu: {total_count}\nBezpečnostné: {security_count}\nProxmox: {pve_count}\nJadro: {kernel_count}\nDôležité balíky:\n{important_list}","label":"Dostupné aktualizácie (starší typ)"},"unknown_persistent":{"title":"{hostname}: Kontrola nedostupná - {category}{entity_suffix}","body":"Kontrola stavu pre kategóriu {category} je nedostupná aspoň 3 cykly.\n{reason}","label":"Kontrola nie je dostupná"},"health_persistent":{"title":"{hostname}: {count} aktívnych problémov so stavom{entity_suffix}","body":"Nasledujúce problémy so stavom systému zostávajú nevyriešené:\n{issue_list}\n\nTento súhrn sa odosiela raz za 24 hodín, kým problémy pretrvávajú.","label":"Aktívne problémy so stavom (denne)"},"health_issue_new":{"title":"{hostname}: Nový problém so stavom systému — {category}{entity_suffix}","body":"Nový {severity} problém zistený v kategórii: {category}\nPodrobnosti: {reason}","label":"Nový problém so stavom"},"health_issue_resolved":{"title":"{hostname}: Vyriešené - {category}{entity_suffix}","body":"Problém v kategórii {category} bol vyriešený.\n{reason}\nTrvanie: {duration}","label":"Problém bol vyriešený"},"update_summary":{"title":"{hostname}: Dostupné aktualizácie","body":"Aktualizácie spolu: {total_count}\nBezpečnostné aktualizácie: {security_count}\nAktualizácie súvisiace s Proxmoxom: {pve_count}\nAktualizácie jadra: {kernel_count}\nDôležité balíky:\n{important_list}","label":"Dostupné aktualizácie"},"pve_update":{"title":"{hostname}: Dostupný Proxmox VE {new_version}","body":"Je dostupná nová verzia Proxmox VE.\nAktuálna: {current_version}\nNová: {new_version}\n{details}","label":"Dostupná aktualizácia Proxmox VE"},"update_complete":{"title":"{hostname}: Aktualizácia systému dokončená","body":"Systémové balíky boli úspešne aktualizované.\n{details}","label":"Aktualizácia bola dokončená"},"ai_model_migrated":{"title":"{hostname}: Model AI aktualizovaný","body":"Model AI pre notifikácie bol automaticky aktualizovaný.\nPoskytovateľ: {provider}\nPredchádzajúci model: {old_model}\nNový model: {new_model}\n\n{message}","label":"AI model bol automaticky aktualizovaný"},"proxmenux_update":{"title":"{hostname}: Dostupný ProxMenux {new_version}","body":"Je dostupná nová verzia ProxMenux.\nAktuálna: {current_version}\nNová: {new_version}","label":"Dostupná aktualizácia ProxMenux"},"mount_stale":{"title":"{hostname}: nereagujúce vzdialené pripojenie {mount_target}","body":"Vzdialené pripojenie {mount_target} ({fstype}) zo zdroja {mount_source} nereaguje{lxc_scope}.\nKontrola stat vypršala alebo vrátila chybu: {error}\n\nAplikácie zapisujúce do tejto cesty môžu nepozorovane zapisovať na podkladový súborový systém a zaplniť disk. Pripojenie obnovte alebo čo najskôr opravte konektivitu.","label":"Vzdialené pripojenie neodpovedá"},"mount_readonly":{"title":"{hostname}: vzdialené pripojenie {mount_target} je iba na čítanie","body":"Vzdialené pripojenie {mount_target} ({fstype}) zo zdroja {mount_source} je pripojené iba na čítanie{lxc_scope}. Zápisy zlyhajú. Ak to nebolo zámerné, pripojte ho znova s voľbou rw.","label":"Vzdialené pripojenie je iba na čítanie"},"lxc_disk_low":{"title":"{hostname}: CT {vmid} koreňový systém na {usage_percent}%","body":"Koreňový súborový systém CT {vmid} ({name}) je zaplnený na {usage_percent}% ({disk_bytes_human} / {maxdisk_bytes_human}).\n\nPlný rootfs LXC bráni korektnému spusteniu kontajnera. Zväčšite rootfs (pct resize {vmid} rootfs +1G) alebo uvoľnite miesto v kontajneri.","label":"Rootfs LXC je takmer plný"},"vm_disk_low":{"title":"{hostname}: VM {vmid} súborové systémy na {usage_percent}%","body":"Súborové systémy VM {vmid} ({name}) sú zaplnené na {usage_percent}% ({disk_bytes_human} / {maxdisk_bytes_human}).\n\nÚdaje poskytol QEMU Guest Agent. Zahŕňajú každý trvalý súborový systém pripojený z blokového zariadenia vrátane virtuálnych diskov a diskov priamo priradených cez PCI. Uvoľnite miesto vo VM alebo zväčšite dotknuté úložisko skôr, než začnú zlyhávať zápisy.","label":"Súborové systémy VM sú takmer plné"},"lxc_mount_low":{"title":"{hostname}: Pripojenie {mount} v CT {vmid} je zaplnené na {usage_percent}%","body":"Pripojenie {mount} v CT {vmid} ({name}) je využité na {usage_percent}%.\nTyp súborového systému: {fstype}\n\nPlné pripojenie v kontajneri často nepozorovane zablokuje aplikáciu — zápisy zlyhajú alebo skončia na rootfs a vyvolajú ďalšie upozornenie. Uvoľnite miesto alebo pripojenie zväčšite.","label":"Pripojenie LXC je takmer plné"},"pve_storage_full":{"title":"{hostname}: Úložisko PVE {storage_name} je zaplnené na {usage_percent}%","body":"Úložisko Proxmox „{storage_name}“ (typ: {storage_type}) je využité na {usage_percent}%.\n\nPo zaplnení nebude možné vytvárať nové VM/CT a existujúcim VM a LXC môžu zlyhať zápisy. Presuňte alebo odstráňte nepoužívané zväzky, prípadne zväčšite podkladový pool/LV/RBD obraz.","label":"Úložisko PVE je takmer plné"},"zfs_pool_full":{"title":"{hostname}: Fond ZFS {pool_name} je zaplnený na {usage_percent}%","body":"Fond ZFS „{pool_name}“ je zaplnený na {usage_percent}%.\n\nVýkon a spoľahlivosť zápisu ZFS sa nad približne 80 % výrazne zhoršujú, pretože CoW potrebuje voľné miesto pre nové bloky. Uvoľnite snímky, odstráňte staré datasety alebo pridajte do fondu ďalšie vdevy.","label":"ZFS pool je takmer plný"},"post_install_update":{"title":"{hostname}: {count} ProxMenux dostupných aktualizácií optimalizácií","body":"Na tomto serveri je dostupných {count} aktualizácií optimalizácií ProxMenux.\n\n🛠️ Dostupné verzie:\n{tool_list}\n\n💡 Použiť cez:\n • ProxMenux Monitor → Nastavenia → Optimalizácie ProxMenux\n • Alebo spustite ponuku po inštalácii (možnosť 2) → „Použiť dostupné aktualizácie“","label":"Dostupné aktualizácie optimalizácií ProxMenux"},"secure_gateway_update_available":{"title":"{hostname}: {app_name} dostupná aktualizácia{update_title_suffix}","body":"Aplikácia {app_name} (spravovaná cez ProxMenux) má vo svojom kontajneri 📦 {package_count} čakajúcich aktualizácií balíkov.\n{version_line}\n\n💡 Otvorte ProxMenux Monitor > Nastavenia > Secure Gateway a kliknite na „Aktualizovať“.\n\n🗂️ Balíky:\n{package_list}","label":"K dispozícii je aktualizácia Secure Gateway"},"nvidia_driver_update_available":{"title":"{hostname}: NVIDIA dostupná aktualizácia ovládača — v{latest_version}","body":"Pre nainštalovaný rad ovládača NVIDIA je dostupná novšia údržbová verzia.\n🔹 Aktuálne nainštalovaná: v{current_version}\n🟢 Najnovšia dostupná: v{latest_version}\n\n{upgrade_reason}\n\n💡 Preinštalovanie:\n • V ponuke ProxMenux po inštalácii: {menu_label}\n\nPreinštalovanie zostaví modul DKMS pre aktuálne jadro a načítanie nového ovládača vyžaduje reštart.","label":"Dostupná aktualizácia ovládača NVIDIA"},"coral_driver_update_available":{"title":"{hostname}: Coral TPU dostupná aktualizácia ovládača — {latest_version}","body":"Je dostupná novšia verzia {variant_label}.\n🔹 Aktuálne nainštalovaná: {current_version}\n🟢 Najnovšia dostupná: {latest_version}\n\n{upgrade_reason}\n\n💡 Preinštalovanie:\n • V ponuke ProxMenux po inštalácii: {menu_label}\n\n{reboot_note}","label":"Dostupná aktualizácia ovládača Coral TPU"},"burst_auth_fail":{"title":"{hostname}: Ďalšie zlyhania overenia za {window}: +{count}","body":"Za {window} bolo zistených ďalších {count} zlyhaní overenia (spolu {total_count}).\nZdroje: {entity_list}","label":"Veľa neúspešných prihlásení"},"burst_ip_block":{"title":"{hostname}: Fail2Ban zablokoval za {window} ďalšie IP: +{count}","body":"Fail2Ban zablokoval za {window} ďalších {count} IP (spolu {total_count}).\nIP adresy: {entity_list}","label":"Veľa zablokovaných IP adries"},"burst_disk_io":{"title":"{hostname}: +{count} ďalších chýb I/O disku na {entity_list}","body":"Za {window} bolo zistených ďalších {count} chýb I/O (spolu {total_count}).\nZariadenia: {entity_list}","label":"Viacero diskových I/O chýb"},"burst_cluster":{"title":"{hostname}: Zistené kolísanie stavu klastra (+{count} ďalších zmien)","body":"Stav klastra sa za {window} zmenil ešte {count}-krát (spolu {total_count}).\nUzly: {entity_list}","label":"Nestabilné spojenie clustra"},"burst_service_fail":{"title":"{hostname}: Za {window} zlyhali ďalšie služby: +{count}","body":"Za {window} bolo zistených ďalších {count} zlyhaní služieb (spolu {total_count}).\nZvyčajne to znamená reštart uzla alebo služby PVE.\n\nĎalšie zlyhania:\n{details}","label":"Zlyhanie viacerých služieb"},"burst_system":{"title":"{hostname}: Ďalšie systémové problémy za {window}: +{count}","body":"Za {window} bolo zistených ďalších {count} systémových problémov (spolu {total_count}).\n\nĎalšie problémy:\n{details}","label":"Viacero systémových problémov"},"burst_generic":{"title":"{hostname}: Ďalšie udalosti {event_type} za {window}: +{count}","body":"Za {window} bolo zaznamenaných ďalších {count} udalostí typu {event_type} (spolu {total_count}).\n\nĎalšie udalosti:\n{details}","label":"Viacero súvisiacich udalostí"},"kernel_warning":{"title":"{hostname}: Zaznamenaná diagnostická udalosť jadra","body":"Jadro zaznamenalo diagnostickú udalosť: {kernel_details}","label":"Upozornenia jadra a diagnostické záznamy"}},"lxcUpdate":{"status":{"success":"úspešne dokončená","failure":"zlyhala","partial":"dokončená čiastočne","deferred":"odložená","skipped":"preskočená"},"source":{"scheduled":"Plánovane","manual":"Manuálne"},"sourceLabel":"Zdroj","targets":"Ciele","osPending":"Čakajúce aktualizácie OS: {before} → {after}","osUnverified":"Balíky OS: príkaz na aktualizáciu bol spustený, výsledok sa nepodarilo overiť","applications":"Aplikácie: {items}","applicationsUnverified":"Aplikácie: aktualizátor bol spustený, nezistila sa však zmena sledovanej verzie","dockerEngineChange":"Docker Engine: {before} → {after}","dockerEngineVerified":"Docker Engine: overený vo verzii {version}","dockerEngineUnverified":"Docker Engine: príkaz na aktualizáciu bol spustený, verziu sa nepodarilo overiť","dockerImagesPending":"Čakajúce aktualizácie Docker obrazov: {before} → {after}","dockerImagesChanged":"Zmenené Docker obrazy: {images}","deferredTargets":"Odložené ciele: {targets}","reason":"Dôvod: {reason}","restartRequired":"Vyžaduje sa reštart: {value}","yes":"áno","no":"nie","verificationPending":"Overenie bude pokračovať po spustení kontajnera","verificationWarning":"Upozornenie pri overení: {error}","duration":"Trvanie: {duration}"},"fallback":{"unknownTitle":"{hostname}: {event_type}","healthCheckDegraded":"Zhoršený stav kontroly","none":"žiadne"},"fields":{"vmid":"VM/CT","name":"Názov","device":"Zariadenie","sourceIp":"Zdrojová IP","node":"Uzol","category":"Kategória","service":"Služba","jail":"Jail","user":"Používateľ","count":"Počet","window":"Obdobie","affected":"Ovplyvnené"},"vzdump":{"size":"Veľkosť: {value}","duration":"Trvanie: {value}","file":"Súbor","backups":"Počet záloh: {count}","failed":"Zlyhalo: {count}","total":"Spolu: {value}","time":"Čas: {value}"},"startup":{"issuesTitle":"{hostname}: Spustenie systému – zistených problémov: {count}","completeTitle":"{hostname}: Spustenie systému dokončené","operational":"Všetky systémy sú funkčné.","vmCountOne":"{count} VM","vmCountMany":"Počet VM: {count}","ctCountOne":"{count} CT","ctCountMany":"Počet CT: {count}","started":"✅ Spustené: {counts}","vmFailed":"❌ VM {name} zlyhal – {reason}","ctFailed":"❌ CT {name} zlyhal – {reason}","unknownError":"neznáma chyba","storageUnavailable":"⚠️ Nedostupné úložiská: {count} ({names})","servicesFailed":"⚠️ Zlyhané služby: {count} ({names})","duration":"⏱️ Spustenie dokončené za {minutes} min"},"appUpdates":{"singleTitle":"{hostname}: Pre {app_name} je na CT {vmid} dostupná aktualizácia","singleBody":"Aplikácia {app_name} na CT {vmid} ({ct_name}) má novú verziu:\n {installed} → {latest}","emptyTitle":"{hostname}: Dostupné aktualizácie aplikácií","emptyBody":"Sú dostupné aktualizácie aplikácií.","batchTitle":"{hostname}: Dostupné aktualizácie aplikácií: {count}","batchLeadOneContainer":"Aplikácie s dostupnou novšou verziou: {count} v {container_count} kontajneri LXC:","batchLeadManyContainers":"Aplikácie s dostupnou novšou verziou: {count} v {container_count} kontajneroch LXC:","additional":"… ďalšie aplikácie: {count}","app":"aplikácia","unknown":"neznáme"},"healthDegraded":{"categories":{"cpu":"Využitie CPU a teplota","memory":"Pamäť a swap","storage":"Pripojené úložiská a miesto","disks":"I/O disku a chyby","network":"Sieťové rozhrania","vms":"VM a kontajnery","services":"Služby PVE","logs":"Systémové záznamy","updates":"Systémové aktualizácie","security":"Bezpečnosť"},"severity":{"critical":"Kritický","warning":"Upozornenie"},"singleTitle":"{hostname}: {severity} stav – {category}","multipleTitle":"{hostname}: Zhoršené kontroly stavu: {count}","multipleLine":"• {severity}: {category}\n {reason}","reasons":{"cpuSustained":"CPU je nad {threshold} % už {duration} s."}},"digest":{"title":"{hostname}: Súhrn za 24 hodín ({timestamp})","readFailed":"načítanie súhrnu zlyhalo: {error}","empty":"Pre toto obdobie súhrnu nie sú uložené žiadne udalosti INFO.","lead":"{count} udalostí INFO zoskupených podľa kategórie:\n","more":" • … a ďalšie: {count}","footer":"(Kritické udalosti a upozornenia boli doručené ihneď, nie v tomto súhrne.)","quietTitle":"{hostname}: Udalosti uložené počas tichých hodín: {count}","groups":{"vm_ct":"VM a CT","backup":"Zálohy","resources":"Zdroje","storage":"Úložisko","network":"Sieť","security":"Bezpečnosť","cluster":"Klaster","services":"Služby","health":"Stav systému","updates":"Aktualizácie","hardware":"Hardvér","system":"Systém","other":"Ostatné"}},"test":{"title":"Test ProxMenux","welcome":"Vitajte v ProxMenux Monitor!","verify":"Toto je testovacia správa na overenie správneho fungovania notifikačného kanála.","configuration":"Nastavenie kanála:","iconsEnabled":"Ikony: zapnuté","iconsDisabled":"Ikony: vypnuté","aiEnabled":"AI: zapnutá ({info})","aiDisabled":"AI: vypnutá","alerts":"Budete dostávať upozornenia na systémové udalosti, zmeny stavu a bezpečnostné incidenty.","photoCaption":"Tento obrázok môžete použiť ako profilovú fotografiu svojho notifikačného bota."},"channels":{"discord":{"category":"Kategória","host":"Server","severity":"Závažnosť"},"email":{"report":"Prehľad: {group}","details":"Podrobnosti","host":"Server","footer":"Notifikačná služba ProxMenux","severity":{"critical":"Kritické","warning":"Upozornenie","info":"Informácia","ok":"Vyriešené","default":"Oznámenie"},"groups":{"vm_ct":"Virtuálny stroj / kontajner","backup":"Záloha a snímka","resources":"Systémové zdroje","storage":"Úložisko","network":"Sieť","security":"Bezpečnosť","cluster":"Klaster","services":"Systémové služby","health":"Stav systému","updates":"Systémové aktualizácie","system":"Systém","hardware":"Hardvér","other":"Systémová notifikácia"},"fields":{"vmCtId":"ID VM/CT","name":"Názov","action":"Akcia","targetNode":"Cieľový uzol","reason":"Dôvod","storage":"Úložisko","status":"Stav","size":"Veľkosť","duration":"Trvanie","snapshot":"Snímka","metric":"Metrika","currentValue":"Aktuálna hodnota","threshold":"Prahová hodnota","cpuCores":"Jadrá CPU","memory":"Pamäť","temperature":"Teplota","mountPoint":"Bod pripojenia","usage":"Využitie","available":"Voľné","device":"Zariadenie","severity":"Závažnosť","storageName":"Názov úložiska","type":"Typ","interface":"Rozhranie","latency":"Latencia","event":"Udalosť","sourceIp":"Zdrojová IP","username":"Používateľ","service":"Služba","jail":"Jail","failures":"Zlyhania","change":"Zmena","node":"Uzol","quorum":"Kvórum","nodesAffected":"Ovplyvnené uzly","process":"Proces","category":"Kategória","previousSeverity":"Predchádzajúca závažnosť","activeIssues":"Aktívne problémy","totalUpdates":"Aktualizácie spolu","securityUpdates":"Bezpečnostné aktualizácie","proxmoxUpdates":"Aktualizácie Proxmox","kernelUpdates":"Aktualizácie jadra","importantPackages":"Dôležité balíky","currentVersion":"Aktuálna verzia","newVersion":"Nová verzia"},"status":{"failed":"Zlyhalo","completed":"Dokončené","started":"Spustené"}}}}}
}
diff --git a/AppImage/messages/sv/common.json b/AppImage/messages/sv/common.json
index 438f410a..1dd77c5c 100644
--- a/AppImage/messages/sv/common.json
+++ b/AppImage/messages/sv/common.json
@@ -1820,6 +1820,12 @@
"updates": {
"label": "Uppdateringar"
},
+ "hardware": {
+ "label": "Hardware"
+ },
+ "system": {
+ "label": "System"
+ },
"other": {
"label": "Andra"
}
@@ -1831,6 +1837,8 @@
"error_escalated": "Hälsoproblemet eskalerade",
"health_degraded": "Hälsokontrollen försämrad",
"lxc_updates_available": "LXC-uppdateringar tillgängliga (experimentell)",
+ "lxc_update_applied": "LXC update applied",
+ "docker_stack_update_available": "Docker updates available",
"vm_start": "VM startade",
"vm_start_warning": "VM startade (varningar)",
"vm_stop": "VM stannade",
@@ -1982,7 +1990,10 @@
"systemHostname": "Systemets värdnamn",
"displayNameHint": "Namn visas i aviseringar. Redigera den eller lämna den tom för att använda systemets värdnamn.",
"activeFooter": "Aviseringar är aktiva. Varje kanal följer sin egen kategori och evenemangsval.",
- "inactiveFooter": "Aktivera aviseringar för att ta emot system-, hälso- och säkerhetsvarningar via de konfigurerade kanalerna."
+ "inactiveFooter": "Aktivera aviseringar för att ta emot system-, hälso- och säkerhetsvarningar via de konfigurerade kanalerna.",
+ "notificationLanguage": "Notification language",
+ "selectNotificationLanguage": "Select notification language",
+ "notificationLanguageHint": "Used for deterministic runtime notifications even when AI is disabled."
},
"ai": {
"title": "AI-förbättring",
@@ -6018,5 +6029,6 @@
"withoutLynis": "Run without Lynis",
"cancel": "Cancel"
}
- }
+ },
+ "runtime": {"notifications":{"templates":{"state_change":{"title":"{hostname}: {category} changed to {current}{entity_suffix}","body":"{category} status changed from {previous} to {current}.\n{reason}","label":"Health state changed"},"new_error":{"title":"{hostname}: New {severity} - {category}{entity_suffix}","body":"{reason}","label":"New health issue"},"error_resolved":{"title":"{hostname}: Resolved - {category}{entity_suffix}","body":"The {category} issue has been resolved.\n{reason}\n🚦 Previous severity: {original_severity}\n⏱️ Duration: {duration}","label":"Recovery notification"},"error_escalated":{"title":"{hostname}: Escalated to {severity} - {category}{entity_suffix}","body":"{reason}","label":"Health issue escalated"},"health_degraded":{"title":"{title_or_default}","body":"{reason}","label":"Health check degraded"},"lxc_updates_available":{"title":"{hostname}: {count} LXC(s) with package updates available","body":"📊 {count} LXC(s) with pending package updates (📦 {total_packages} total, 🔒 {security_count} security):\n\n{ct_list}","label":"LXC updates available (experimental)"},"lxc_update_applied":{"title":"{hostname}: LXC {ct_name} ({vmid}) update {result}","body":"{details}","label":"LXC update applied"},"app_update_available":{"title":"{hostname}: {app_name} update available on CT {vmid}","body":"{app_name} on CT {vmid} ({ct_name}) has a new version:\n {installed} → {latest}","label":"App update available"},"docker_stack_update_available":{"title":"{hostname}: Docker updates available on CT {vmid}","body":"Container {ct_name} (CT {vmid}) has {count} Docker update(s):\n{details}","label":"Docker updates available"},"vm_start":{"title":"{hostname}: VM {vmname} ({vmid}) started","body":"Virtual machine {vmname} (ID: {vmid}) is now running.","label":"VM started"},"vm_start_warning":{"title":"{hostname}: VM {vmname} ({vmid}) started with warnings","body":"Virtual machine {vmname} (ID: {vmid}) started successfully but has warnings.\nWarnings: {reason}","label":"VM started (warnings)"},"vm_stop":{"title":"{hostname}: VM {vmname} ({vmid}) stopped","body":"Virtual machine {vmname} (ID: {vmid}) has been stopped.","label":"VM stopped"},"vm_shutdown":{"title":"{hostname}: VM {vmname} ({vmid}) shut down","body":"Virtual machine {vmname} (ID: {vmid}) has been cleanly shut down.","label":"VM shutdown"},"vm_fail":{"title":"{hostname}: VM {vmname} ({vmid}) FAILED","body":"Virtual machine {vmname} (ID: {vmid}) has crashed or failed to start.\nReason: {reason}","label":"VM FAILED"},"vm_restart":{"title":"{hostname}: VM {vmname} ({vmid}) restarted","body":"Virtual machine {vmname} (ID: {vmid}) has been restarted.","label":"VM restarted"},"ct_start":{"title":"{hostname}: CT {vmname} ({vmid}) started","body":"Container {vmname} (ID: {vmid}) is now running.","label":"CT started"},"ct_start_warning":{"title":"{hostname}: CT {vmname} ({vmid}) started with warnings","body":"Container {vmname} (ID: {vmid}) started successfully but has warnings.\nWarnings: {reason}","label":"CT started (warnings)"},"ct_stop":{"title":"{hostname}: CT {vmname} ({vmid}) stopped","body":"Container {vmname} (ID: {vmid}) has been stopped.","label":"CT stopped"},"ct_shutdown":{"title":"{hostname}: CT {vmname} ({vmid}) shut down","body":"Container {vmname} (ID: {vmid}) has been cleanly shut down.","label":"CT shutdown"},"ct_restart":{"title":"{hostname}: CT {vmname} ({vmid}) restarted","body":"Container {vmname} (ID: {vmid}) has been restarted.","label":"CT restarted"},"ct_fail":{"title":"{hostname}: CT {vmname} ({vmid}) FAILED","body":"Container {vmname} (ID: {vmid}) has crashed or failed to start.\nReason: {reason}","label":"CT FAILED"},"migration_start":{"title":"{hostname}: Migration started — {vmname} ({vmid})","body":"Live migration of {vmname} (ID: {vmid}) to node {target_node} has started.","label":"Migration started"},"migration_complete":{"title":"{hostname}: Migration complete — {vmname} ({vmid})","body":"{vmname} (ID: {vmid}) successfully migrated to node {target_node}.","label":"Migration complete"},"migration_warning":{"title":"{hostname}: Migration complete with warnings — {vmname} ({vmid})","body":"{vmname} (ID: {vmid}) migrated to node {target_node} but encountered warnings.\nWarnings: {reason}","label":"Migration (warnings)"},"migration_fail":{"title":"{hostname}: Migration FAILED — {vmname} ({vmid})","body":"Migration of {vmname} (ID: {vmid}) to node {target_node} failed.\nReason: {reason}","label":"Migration FAILED"},"replication_fail":{"title":"{hostname}: Replication FAILED — {vmname} ({vmid})","body":"Replication of {vmname} (ID: {vmid}) failed.\nReason: {reason}","label":"Replication FAILED"},"replication_complete":{"title":"{hostname}: Replication complete — {vmname} ({vmid})","body":"Replication of {vmname} (ID: {vmid}) completed successfully.","label":"Replication complete"},"backup_start":{"title":"{hostname}: Backup started on {storage}","body":"Backup job started on storage {storage}.\n{reason}","label":"Backup started"},"backup_complete":{"title":"{hostname} → {storage}: Backup complete — {vmname} ({vmid})","body":"Backup of {vmname} (ID: {vmid}) completed successfully on {storage}.\nSize: {size}","label":"Backup complete"},"backup_warning":{"title":"{hostname} → {storage}: Backup complete with warnings — {vmname} ({vmid})","body":"Backup of {vmname} (ID: {vmid}) on {storage} completed but encountered warnings.\nWarnings: {reason}","label":"Backup (warnings)"},"backup_fail":{"title":"{hostname} → {storage}: Backup FAILED — {vmname} ({vmid})","body":"Backup of {vmname} (ID: {vmid}) failed on {storage}.\nReason: {reason}","label":"Backup FAILED"},"host_backup_start":{"title":"{hostname}: Host backup started → {backend_label}","body":"Job: {job_id}\nBackend: {backend_label}\nDestination: {destination}\nProfile: {profile_mode}","label":"Host backup started"},"host_backup_complete":{"title":"{hostname}: Host backup complete → {backend_label}","body":"Job: {job_id}\nBackend: {backend_label}\nDestination: {destination}\nData size: {data_size}\nArchive size: {archive_size}\nDuration: {duration}","label":"Host backup complete"},"host_backup_fail":{"title":"{hostname}: Host backup FAILED → {backend_label}","body":"Job: {job_id}\nBackend: {backend_label}\nDestination: {destination}\nDuration before failure: {duration}\nReason: {reason}\nLog: {log_file}","label":"Host backup FAILED"},"snapshot_complete":{"title":"{hostname}: Snapshot created — {vmname} ({vmid})","body":"Snapshot \"{snapshot_name}\" created for {vmname} (ID: {vmid}).","label":"Snapshot created"},"snapshot_fail":{"title":"{hostname}: Snapshot FAILED — {vmname} ({vmid})","body":"Snapshot creation for {vmname} (ID: {vmid}) failed.\nReason: {reason}","label":"Snapshot FAILED"},"cpu_high":{"title":"{hostname}: High CPU usage — {value}%","body":"CPU usage has reached {value}% on {cores} cores.\n{details}","label":"High CPU usage"},"ram_high":{"title":"{hostname}: High memory usage — {value}%","body":"Memory usage: {used} / {total} ({value}%).\n{details}","label":"High memory usage"},"temp_high":{"title":"{hostname}: High CPU temperature — {value}°C","body":"CPU temperature has reached {value}°C (threshold: {threshold}°C).\n{details}","label":"High temperature"},"disk_space_low":{"title":"{hostname}: Low disk space on {mount}","body":"Filesystem {mount}: {used}% used ({available} available).\nFree up disk space to avoid service disruption.","label":"Low disk space"},"disk_io_error":{"title":"{hostname}: Disk failure detected — {device}","body":"I/O error or disk failure detected on device {device}.\n{reason}","label":"Disk failure / I/O error"},"storage_unavailable":{"title":"{hostname}: Storage unavailable — {storage_name}","body":"PVE storage \"{storage_name}\" (type: {storage_type}) is not accessible.\nReason: {reason}","label":"Storage unavailable"},"smart_test_complete":{"title":"{hostname}: SMART test completed — {device}","body":"SMART {test_type} test on /dev/{device} has completed.\nResult: {result}\nDuration: {duration}","label":"SMART test completed"},"smart_test_failed":{"title":"{hostname}: SMART test FAILED — {device}","body":"SMART {test_type} test on /dev/{device} has failed.\nResult: {result}\nReason: {reason}","label":"SMART test FAILED"},"gpu_mode_switch":{"title":"{hostname}: GPU mode changed to {new_mode}","body":"GPU passthrough mode has been switched.\nGPU: {gpu_name} ({gpu_pci})\nPrevious mode: {old_mode}\nNew mode: {new_mode}\n{details}","label":"GPU mode switched"},"gpu_passthrough_blocked":{"title":"{hostname}: {guest_type} {guest_id} blocked at startup","body":"PCIe passthrough guard prevented {guest_type} {guest_id} ({guest_name}) from starting.\nReason: {reason}\n{details}","label":"GPU passthrough blocked"},"pci_passthrough_conflict":{"title":"{hostname}: PCIe device conflict detected — {device_pci}","body":"A PCIe device is assigned to multiple guests.\nDevice: {device_pci}\nConflicting guests: {guest_list}\nAction required: Stop one of the guests or reassign the device.","label":"PCIe device conflict"},"load_high":{"title":"{hostname}: High system load — {value}","body":"System load average is {value} on {cores} cores.\n{details}","label":"High system load"},"network_down":{"title":"{hostname}: Network connectivity lost{entity_suffix}","body":"The node has lost network connectivity.\nReason: {reason}","label":"Network connectivity lost"},"network_latency":{"title":"{hostname}: High network latency — {value}ms","body":"Latency to gateway: {value}ms (threshold: {threshold}ms).\nThis may indicate network congestion or hardware issues.","label":"High network latency"},"auth_fail":{"title":"{hostname}: Authentication failure","body":"Failed login attempt detected.\nSource IP: {source_ip}\nUser: {username}\nService: {service}","label":"Authentication failure"},"ip_block":{"title":"{hostname}: IP blocked by Fail2Ban","body":"IP address {source_ip} has been banned.\nJail: {jail}\nFailed attempts: {failures}","label":"IP blocked by Fail2Ban"},"firewall_issue":{"title":"{hostname}: Firewall issue detected{entity_suffix}","body":"A firewall configuration issue has been detected.\nReason: {reason}","label":"Firewall issue detected"},"user_permission_change":{"title":"{hostname}: User permission changed","body":"User: {username}\nChange: {change_details}","label":"User permission changed"},"split_brain":{"title":"{hostname}: SPLIT-BRAIN detected","body":"A cluster split-brain condition has been detected. Quorum may be lost.\nQuorum status: {quorum}","label":"SPLIT-BRAIN detected"},"node_disconnect":{"title":"{hostname}: Node {node_name} disconnected","body":"Node {node_name} has disconnected from the cluster.","label":"Node disconnected"},"node_reconnect":{"title":"{hostname}: Node {node_name} reconnected","body":"Node {node_name} has rejoined the cluster successfully.","label":"Node reconnected"},"system_startup":{"title":"{hostname}: {reason}","body":"{summary}","label":"System startup report"},"system_shutdown":{"title":"{hostname}: System shutting down","body":"The node is shutting down.\n{reason}","label":"System shutting down"},"system_reboot":{"title":"{hostname}: System rebooting","body":"The node is rebooting.\n{reason}","label":"System rebooting"},"system_restore_completed":{"title":"{hostname}: Host restore finished","body":"Post-restore tasks completed in background.\n\nGuests applied: {guests}\nBind-mount stubs: {stubs}\nStale node dirs removed: {stale_nodes}\nComponents reinstalled: {components}\nDuration: {duration}\n{warnings_block}\nThe node is now fully ready to use.","label":"Host restore completed"},"system_problem":{"title":"{hostname}: System problem detected{entity_suffix}","body":"A system-level problem has been detected.\nReason: {reason}","label":"System problem detected"},"service_fail":{"title":"{hostname}: Service failed — {service_name}","body":"System service \"{service_name}\" has failed.\nReason: {reason}","label":"Service failed"},"oom_kill":{"title":"{hostname}: OOM Kill — {process}","body":"Process \"{process}\" was killed by the Out-of-Memory manager.\n{reason}","label":"Out of memory kill"},"service_fail_batch":{"title":"{hostname}: {service_count} services failed{entity_suffix}","body":"{reason}","label":"Service fail batch"},"cron_output":{"title":"{hostname}: {pve_title}","body":"{reason}","label":"Cron job output (per-cron stdout via mail)"},"system_mail":{"title":"{hostname}: {pve_title}","body":"{reason}","label":"Smartd / mail bounces (PVE system mail)"},"apt_listchanges":{"title":"{hostname}: {pve_title}","body":"Upstream package information forwarded by Proxmox VE through apt-listchanges. The following text comes from the package maintainer and is not a ProxMenux recommendation.\n\n{reason}","label":"apt-listchanges package notices"},"webhook_test":{"title":"{hostname}: Webhook test received","body":"PVE webhook connectivity test successful.\n{reason}","label":"Webhook test"},"update_available":{"title":"{hostname}: Updates available","body":"Total updates: {total_count}\nSecurity: {security_count}\nProxmox: {pve_count}\nKernel: {kernel_count}\nImportant packages:\n{important_list}","label":"Updates available (legacy)"},"unknown_persistent":{"title":"{hostname}: Check unavailable - {category}{entity_suffix}","body":"Health check for {category} has been unavailable for 3+ cycles.\n{reason}","label":"Check unavailable"},"health_persistent":{"title":"{hostname}: {count} active health issue(s){entity_suffix}","body":"The following health issues remain unresolved:\n{issue_list}\n\nThis digest is sent once every 24 hours while issues persist.","label":"Active health issues (daily)"},"health_issue_new":{"title":"{hostname}: New health issue — {category}{entity_suffix}","body":"New {severity} issue detected in: {category}\nDetails: {reason}","label":"New health issue"},"health_issue_resolved":{"title":"{hostname}: Resolved - {category}{entity_suffix}","body":"{category} issue has been resolved.\n{reason}\nDuration: {duration}","label":"Health issue resolved"},"update_summary":{"title":"{hostname}: Updates available","body":"Total updates: {total_count}\nSecurity updates: {security_count}\nProxmox-related updates: {pve_count}\nKernel updates: {kernel_count}\nImportant packages:\n{important_list}","label":"Host package updates"},"pve_update":{"title":"{hostname}: Proxmox VE {new_version} available","body":"A new Proxmox VE release is available.\nCurrent: {current_version}\nNew: {new_version}\n{details}","label":"Proxmox VE update available"},"update_complete":{"title":"{hostname}: System update completed","body":"System packages have been successfully updated.\n{details}","label":"Host update completed"},"ai_model_migrated":{"title":"{hostname}: AI model updated","body":"The AI model for notifications has been automatically updated.\nProvider: {provider}\nPrevious model: {old_model}\nNew model: {new_model}\n\n{message}","label":"AI model auto-updated"},"proxmenux_update":{"title":"{hostname}: ProxMenux {new_version} available","body":"A new version of ProxMenux is available.\nCurrent: {current_version}\nNew: {new_version}","label":"ProxMenux update available"},"mount_stale":{"title":"{hostname}: stale remote mount {mount_target}","body":"Remote mount {mount_target} ({fstype}) from {mount_source} is stale{lxc_scope}.\nStat timed out or returned an error: {error}\n\nApps writing to this path will silently land on the underlying filesystem and may fill the disk. Remount or fix connectivity ASAP.","label":"Remote mount stale"},"mount_readonly":{"title":"{hostname}: remote mount {mount_target} is read-only","body":"Remote mount {mount_target} ({fstype}) from {mount_source} is mounted read-only{lxc_scope}. Writes will fail. If this was unintentional, remount with rw.","label":"Remote mount read-only"},"lxc_disk_low":{"title":"{hostname}: CT {vmid} rootfs at {usage_percent}%","body":"CT {vmid} ({name}) rootfs is at {usage_percent}% ({disk_bytes_human} / {maxdisk_bytes_human}).\n\nA full LXC rootfs prevents the container from booting cleanly. Either expand the rootfs (pct resize {vmid} rootfs +1G) or free space inside the container.","label":"LXC rootfs near full"},"vm_disk_low":{"title":"{hostname}: VM {vmid} filesystems at {usage_percent}%","body":"VM {vmid} ({name}) guest filesystems are at {usage_percent}% ({disk_bytes_human} / {maxdisk_bytes_human}).\n\nReported by the QEMU guest agent. Includes every persistent filesystem the guest mounts on a block device — virtual disks and PCI-passthrough drives alike. Free up space inside the guest or expand the affected storage before writes start to fail.","label":"VM filesystems near full"},"lxc_mount_low":{"title":"{hostname}: CT {vmid} mount {mount} at {usage_percent}%","body":"Mount {mount} inside CT {vmid} ({name}) is at {usage_percent}% used.\nFilesystem type: {fstype}\n\nA full mount inside a container often blocks the application silently — writes either fail or, worse, land on the rootfs and trigger the rootfs alert next. Free up space on the mount or expand it.","label":"LXC mount near full"},"pve_storage_full":{"title":"{hostname}: PVE storage {storage_name} at {usage_percent}%","body":"Proxmox storage \"{storage_name}\" (type: {storage_type}) is at {usage_percent}% used.\n\nOnce full, no new VM/CT can be provisioned and existing guests may fail to write. Move/delete unused volumes or expand the underlying pool/LV/RBD image.","label":"PVE storage near full"},"zfs_pool_full":{"title":"{hostname}: ZFS pool {pool_name} at {usage_percent}%","body":"ZFS pool \"{pool_name}\" is at {usage_percent}% capacity.\n\nZFS performance and write reliability degrade sharply above ~80% capacity (CoW needs free space for new blocks). Free up snapshots, prune old datasets, or add more vdevs to the pool.","label":"ZFS pool near full"},"post_install_update":{"title":"{hostname}: {count} ProxMenux optimization update(s) available","body":"{count} ProxMenux optimization update(s) available on this host.\n\n🛠️ Available versions:\n{tool_list}\n\n💡 Apply from:\n • ProxMenux Monitor → Settings → ProxMenux Optimizations\n • Or run the post-install menu (option 2) → \"Apply available updates\"","label":"ProxMenux optimization updates available"},"secure_gateway_update_available":{"title":"{hostname}: {app_name} update available{update_title_suffix}","body":"{app_name} (managed by ProxMenux) has 📦 {package_count} package update(s) pending in its container.\n{version_line}\n\n💡 Open ProxMenux Monitor > Settings > Secure Gateway and click \"Update\" to apply.\n\n🗂️ Packages:\n{package_list}","label":"Secure Gateway update available"},"nvidia_driver_update_available":{"title":"{hostname}: NVIDIA driver update available — v{latest_version}","body":"A newer maintenance release is available for the installed NVIDIA driver branch.\n🔹 Currently installed: v{current_version}\n🟢 Latest available: v{latest_version}\n\n{upgrade_reason}\n\n💡 To reinstall:\n • From the ProxMenux post-install menu: {menu_label}\n\nReinstalling rebuilds the DKMS module against the running kernel and requires a reboot to load the new driver.","label":"NVIDIA driver update available"},"coral_driver_update_available":{"title":"{hostname}: Coral TPU driver update available — {latest_version}","body":"A newer {variant_label} is available.\n🔹 Currently installed: {current_version}\n🟢 Latest available: {latest_version}\n\n{upgrade_reason}\n\n💡 To reinstall:\n • From the ProxMenux post-install menu: {menu_label}\n\n{reboot_note}","label":"Coral TPU driver update available"},"burst_auth_fail":{"title":"{hostname}: +{count} more auth failures in {window}","body":"+{count} additional authentication failures detected in {window} ({total_count} total).\nSources: {entity_list}","label":"Auth failures burst"},"burst_ip_block":{"title":"{hostname}: Fail2Ban banned +{count} more IPs in {window}","body":"+{count} additional IPs banned by Fail2Ban in {window} ({total_count} total).\nIPs: {entity_list}","label":"IP block burst"},"burst_disk_io":{"title":"{hostname}: +{count} more disk I/O errors on {entity_list}","body":"+{count} additional I/O errors detected in {window} ({total_count} total).\nDevices: {entity_list}","label":"Disk I/O burst"},"burst_cluster":{"title":"{hostname}: Cluster flapping detected (+{count} more changes)","body":"Cluster state changed +{count} more times in {window} ({total_count} total).\nNodes: {entity_list}","label":"Cluster flapping burst"},"burst_service_fail":{"title":"{hostname}: +{count} more services failed in {window}","body":"+{count} additional service failures detected in {window} ({total_count} total).\nThis typically indicates a node reboot or PVE service restart.\n\nAdditional failures:\n{details}","label":"Service fail burst"},"burst_system":{"title":"{hostname}: +{count} more system problems in {window}","body":"+{count} additional system problems detected in {window} ({total_count} total).\n\nAdditional issues:\n{details}","label":"System problems burst"},"burst_generic":{"title":"{hostname}: +{count} more {event_type} events in {window}","body":"+{count} additional events of type {event_type} in {window} ({total_count} total).\n\nAdditional events:\n{details}","label":"Generic burst"},"kernel_warning":{"title":"{hostname}: Kernel diagnostic event detected","body":"The kernel recorded a diagnostic event: {kernel_details}","label":"Kernel warnings and diagnostic traces"}},"lxcUpdate":{"status":{"success":"succeeded","failure":"failed","partial":"completed partially","deferred":"deferred","skipped":"skipped"},"source":{"scheduled":"Scheduled","manual":"Manual"},"sourceLabel":"Source","targets":"Targets","osPending":"OS packages pending: {before} → {after}","osUnverified":"OS packages: update command executed; result not verified","applications":"Applications: {items}","applicationsUnverified":"Applications: updater executed; no tracked version change was observed","dockerEngineChange":"Docker Engine: {before} → {after}","dockerEngineVerified":"Docker Engine: verified at {version}","dockerEngineUnverified":"Docker Engine: update command executed; version not verified","dockerImagesPending":"Docker images pending: {before} → {after}","dockerImagesChanged":"Docker images changed: {images}","deferredTargets":"Deferred targets: {targets}","reason":"Reason: {reason}","restartRequired":"Restart required: {value}","yes":"yes","no":"no","verificationPending":"Verification pending until the container is running","verificationWarning":"Verification warning: {error}","duration":"Duration: {duration}"},"fallback":{"unknownTitle":"{hostname}: {event_type}","healthCheckDegraded":"Health check degraded","none":"none"},"fields":{"vmid":"VM/CT","name":"Name","device":"Device","sourceIp":"Source IP","node":"Node","category":"Category","service":"Service","jail":"Jail","user":"User","count":"Count","window":"Window","affected":"Affected"},"vzdump":{"size":"Size: {value}","duration":"Duration: {value}","file":"File","backups":"{count} backups","failed":"{count} failed","total":"Total: {value}","time":"Time: {value}"},"startup":{"issuesTitle":"{hostname}: System startup - {count} issue(s) detected","completeTitle":"{hostname}: System startup completed","operational":"All systems operational.","vmCountOne":"{count} VM","vmCountMany":"{count} VMs","ctCountOne":"{count} CT","ctCountMany":"{count} CTs","started":"✅ {counts} started","vmFailed":"❌ VM failed: {name} - {reason}","ctFailed":"❌ CT failed: {name} - {reason}","unknownError":"unknown error","storageUnavailable":"⚠️ Storage: {count} unavailable ({names})","servicesFailed":"⚠️ Services: {count} failed ({names})","duration":"⏱️ Startup completed in {minutes} min"},"appUpdates":{"singleTitle":"{hostname}: {app_name} update available on CT {vmid}","singleBody":"{app_name} on CT {vmid} ({ct_name}) has a new version:\n {installed} → {latest}","emptyTitle":"{hostname}: Application updates available","emptyBody":"Application updates are available.","batchTitle":"{hostname}: {count} application updates available","batchLeadOneContainer":"{count} applications in {container_count} LXC container have a newer version:","batchLeadManyContainers":"{count} applications in {container_count} LXC containers have a newer version:","additional":"… {count} additional application(s)","app":"app","unknown":"unknown"},"healthDegraded":{"categories":{"cpu":"CPU usage and temperature","memory":"Memory and swap","storage":"Storage mounts and space","disks":"Disk I/O and errors","network":"Network interfaces","vms":"VMs and containers","services":"PVE services","logs":"System logs","updates":"System updates","security":"Security"},"severity":{"critical":"Critical","warning":"Warning"},"singleTitle":"{hostname}: {severity} health status – {category}","multipleTitle":"{hostname}: {count} health checks degraded","multipleLine":"• {severity}: {category}\n {reason}","reasons":{"cpuSustained":"CPU is above {threshold}% for {duration}s."}},"digest":{"title":"{hostname}: 24h summary ({timestamp})","readFailed":"digest read failed: {error}","empty":"No INFO events buffered for this digest window.","lead":"{count} INFO events grouped by category:\n","more":" • … and {count} more","footer":"(Critical/Warning events arrived at the time they happened, not in this digest.)","quietTitle":"{hostname}: {count} events buffered during Quiet Hours","groups":{"vm_ct":"VM a CT","backup":"Backup","resources":"Resources","storage":"Storage","network":"Network","security":"Security","cluster":"Cluster","services":"Services","health":"Health","updates":"Updates","hardware":"Hardware","system":"System","other":"Other"}},"test":{"title":"ProxMenux Test","welcome":"Welcome to ProxMenux Monitor!","verify":"This is a test message to verify your notification channel is working correctly.","configuration":"Channel configuration:","iconsEnabled":"Icons: enabled","iconsDisabled":"Icons: disabled","aiEnabled":"AI: enabled ({info})","aiDisabled":"AI: disabled","alerts":"You will receive alerts about system events, health status changes, and security incidents.","photoCaption":"You can use this image as the profile photo for your notification bot."},"channels":{"discord":{"category":"Category","host":"Host","severity":"Severity"},"email":{"report":"{group} Report","details":"Details","host":"Host","footer":"ProxMenux Notification Service","severity":{"critical":"Critical","warning":"Warning","info":"Information","ok":"Resolved","default":"Notice"},"groups":{"vm_ct":"Virtual Machine / Container","backup":"Backup & Snapshot","resources":"System Resources","storage":"Storage","network":"Network","security":"Security","cluster":"Cluster","services":"System Services","health":"Health Monitor","updates":"System Updates","system":"System","hardware":"Hardware","other":"System Notification"},"fields":{"vmCtId":"VM/CT ID","name":"Name","action":"Action","targetNode":"Target Node","reason":"Reason","storage":"Storage","status":"Status","size":"Size","duration":"Duration","snapshot":"Snapshot","metric":"Metric","currentValue":"Current Value","threshold":"Threshold","cpuCores":"CPU Cores","memory":"Memory","temperature":"Temperature","mountPoint":"Mount Point","usage":"Usage","available":"Available","device":"Device","severity":"Severity","storageName":"Storage Name","type":"Type","interface":"Interface","latency":"Latency","event":"Event","sourceIp":"Source IP","username":"Username","service":"Service","jail":"Jail","failures":"Failures","change":"Change","node":"Node","quorum":"Quorum","nodesAffected":"Nodes Affected","process":"Process","category":"Category","previousSeverity":"Previous Severity","activeIssues":"Active Issues","totalUpdates":"Total Updates","securityUpdates":"Security Updates","proxmoxUpdates":"Proxmox Updates","kernelUpdates":"Kernel Updates","importantPackages":"Important Packages","currentVersion":"Current Version","newVersion":"New Version"},"status":{"failed":"Failed","completed":"Completed","started":"Started"}}}}}
}
diff --git a/AppImage/scripts/build_appimage.sh b/AppImage/scripts/build_appimage.sh
index 85b031f4..5d3e799e 100755
--- a/AppImage/scripts/build_appimage.sh
+++ b/AppImage/scripts/build_appimage.sh
@@ -163,6 +163,13 @@ cp "$SCRIPT_DIR/proxmox_known_errors.py" "$APP_DIR/usr/bin/" 2>/dev/null || echo
cp "$SCRIPT_DIR/ai_context_enrichment.py" "$APP_DIR/usr/bin/" 2>/dev/null || echo "⚠️ ai_context_enrichment.py not found"
cp "$SCRIPT_DIR/startup_grace.py" "$APP_DIR/usr/bin/" 2>/dev/null || echo "⚠️ startup_grace.py not found"
cp "$SCRIPT_DIR/flask_notification_routes.py" "$APP_DIR/usr/bin/" 2>/dev/null || echo "⚠️ flask_notification_routes.py not found"
+# Backend notification rendering reuses the canonical Monitor catalogs.
+# Keep every supported locale in the bundle so the runtime selector and its
+# fallback behavior exactly match the Monitor UI.
+for locale in en de es fr it pt sk sv; do
+ mkdir -p "$APP_DIR/usr/share/proxmenux/messages/$locale"
+ cp "$APPIMAGE_ROOT/messages/$locale/common.json" "$APP_DIR/usr/share/proxmenux/messages/$locale/common.json"
+done
cp "$SCRIPT_DIR/oci_manager.py" "$APP_DIR/usr/bin/" 2>/dev/null || echo "⚠️ oci_manager.py not found"
cp "$SCRIPT_DIR/flask_oci_routes.py" "$APP_DIR/usr/bin/" 2>/dev/null || echo "⚠️ flask_oci_routes.py not found"
cp "$SCRIPT_DIR/flask_audit_routes.py" "$APP_DIR/usr/bin/" 2>/dev/null || echo "⚠️ flask_audit_routes.py not found"
diff --git a/AppImage/scripts/flask_server.py b/AppImage/scripts/flask_server.py
index 9316341e..b1e6cffb 100644
--- a/AppImage/scripts/flask_server.py
+++ b/AppImage/scripts/flask_server.py
@@ -93,7 +93,7 @@ from flask_security_routes import security_bp # noqa: E402
from flask_notification_routes import notification_bp # noqa: E402
from flask_oci_routes import oci_bp # noqa: E402
from flask_audit_routes import audit_bp # noqa: E402
-from notification_manager import notification_manager # noqa: E402
+from notification_manager import notification_manager, resolve_notification_hostname # noqa: E402
import post_install_versions # noqa: E402 — Sprint 12A: detect post-install function updates
from jwt_middleware import require_auth, require_auth_or_ticket, require_admin_scope # noqa: E402
import auth_manager # noqa: E402
@@ -1464,6 +1464,11 @@ def _health_collector_loop():
if not hostname:
import socket as _sock
hostname = _sock.gethostname()
+ # The health collector builds its title before the event
+ # reaches NotificationManager, so normalize here as well.
+ hostname = resolve_notification_hostname(
+ hostname, notification_manager._config,
+ )
# Capture journal context for AI enrichment
# Extract category keys and reasons for keyword matching
@@ -1526,6 +1531,7 @@ def _health_collector_loop():
'count': str(len(degraded)),
'title': title,
'reason': body,
+ 'health_degraded': {'categories': degraded},
'_journal_context': journal_context,
},
source='health_monitor',
@@ -14297,6 +14303,20 @@ def _finalize_lxc_update(
'result': result_words[status],
'duration': duration,
'details': details,
+ 'lxc_update': {
+ 'status': status,
+ 'source': source,
+ 'targets': executed,
+ 'labels': labels,
+ 'deferred_targets': deferred,
+ 'duration': duration,
+ 'reason': str(reason)[:500] if reason else None,
+ 'before': before,
+ 'after': after,
+ 'verification_pending': verification_pending,
+ 'verification_errors': verification_errors,
+ 'reboot_required': reboot_required,
+ },
},
source=source,
entity='ct',
diff --git a/AppImage/scripts/notification_channels.py b/AppImage/scripts/notification_channels.py
index 932879fd..0f6b43c8 100644
--- a/AppImage/scripts/notification_channels.py
+++ b/AppImage/scripts/notification_channels.py
@@ -36,6 +36,18 @@ _KNOWN_SSRF_TARGETS = {
_BLOCKED_LOOPBACK_PORTS = {'8006', '8007'} # PVE API HTTPS / HTTPS-alt
+def _runtime_notification_text(key: str, data: Optional[Dict] = None,
+ **values: Any) -> str:
+ """Resolve runtime text lazily to avoid the manager/channel import cycle."""
+ from notification_templates import runtime_message
+ language = str((data or {}).get('_notification_language', 'en'))
+ return runtime_message(key, language, **values)
+
+
+def _runtime_text(key: str, data: Optional[Dict] = None, **values: Any) -> str:
+ return _runtime_notification_text(f'channels.{key}', data, **values)
+
+
def _validate_user_webhook_url(url: str) -> Tuple[bool, str]:
"""Lightweight SSRF guard for Gotify-style channels.
@@ -308,18 +320,102 @@ 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]
- chunks = []
- while text:
- if len(text) <= self.MAX_LENGTH:
- chunks.append(text)
+
+ token_re = re.compile(
+ r'&(?:#[0-9]+|#x[0-9A-Fa-f]+|[A-Za-z][A-Za-z0-9]+);|<[^<>]+>|.',
+ re.DOTALL,
+ )
+ entity_re = re.compile(r'&(?:#[0-9]+|#x[0-9A-Fa-f]+|[A-Za-z][A-Za-z0-9]+);')
+ 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))
+
+ def _openers(stack):
+ return ''.join(opener for _, opener in stack)
+
+ def _plain_chunks(tokens):
+ """Drop unsafe formatting while preserving safe visible HTML text."""
+ safe_tokens = []
+ for token in tokens:
+ if tag_re.fullmatch(token):
+ continue
+ if entity_re.fullmatch(token) and len(token) <= self.MAX_LENGTH:
+ safe_tokens.append(token)
+ continue
+ if entity_re.fullmatch(token) or token in {'&', '<', '>'}:
+ safe_tokens.extend(token_re.findall(self._escape_html(token)))
+ else:
+ safe_tokens.append(token)
+
+ plain_chunks = []
+ current = ''
+ for token in safe_tokens:
+ if current and len(current) + len(token) > self.MAX_LENGTH:
+ plain_chunks.append(current)
+ current = ''
+ current += token
+ if current:
+ plain_chunks.append(current)
+ return plain_chunks
+
+ tokens = token_re.findall(text)
+ probe_tags = []
+ unsafe_html = False
+ for token in tokens:
+ match = tag_re.fullmatch(token)
+ if match and match.group(1):
+ name = match.group(2).lower()
+ if not probe_tags or probe_tags[-1][0] != name:
+ unsafe_html = True
+ break
+ next_tags = _advance(probe_tags, token)
+ minimum_chunk = len(_openers(probe_tags)) + len(token) + len(_closers(next_tags))
+ if len(token) > self.MAX_LENGTH or minimum_chunk > self.MAX_LENGTH:
+ unsafe_html = True
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')
+ probe_tags = next_tags
+
+ if unsafe_html:
+ return _plain_chunks(tokens)
+
+ chunks = []
+ current = ''
+ open_tags = []
+ for token in tokens:
+ 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 = _openers(open_tags)
+ current += token
+ open_tags = _advance(open_tags, token)
+
+ if current:
+ chunks.append(current + _closers(open_tags))
+ if any(len(chunk) > self.MAX_LENGTH for chunk in chunks):
+ return _plain_chunks(tokens)
return chunks
@staticmethod
@@ -612,11 +708,11 @@ class DiscordChannel(NotificationChannel):
]
elif data:
if data.get('category'):
- fields.append({'name': 'Category', 'value': data['category'], 'inline': True})
+ fields.append({'name': _runtime_text('discord.category', data), 'value': data['category'], 'inline': True})
if data.get('hostname'):
- fields.append({'name': 'Host', 'value': data['hostname'], 'inline': True})
+ fields.append({'name': _runtime_text('discord.host', data), 'value': data['hostname'], 'inline': True})
if data.get('severity'):
- fields.append({'name': 'Severity', 'value': data['severity'], 'inline': True})
+ fields.append({'name': _runtime_text('discord.severity', data), 'value': data['severity'], 'inline': True})
embeds: List[Dict[str, Any]] = []
for idx, chunk in enumerate(chunks):
@@ -936,12 +1032,17 @@ class EmailChannel(NotificationChannel):
import time as _time
data = data or {}
- sev = self._SEV_STYLE.get(severity, self._SEV_DEFAULT)
+ sev = dict(self._SEV_STYLE.get(severity, self._SEV_DEFAULT))
+ severity_key = severity.lower() if severity in self._SEV_STYLE else 'default'
+ sev['label'] = _runtime_text(f'email.severity.{severity_key}', data)
# Determine group for section header
event_type = data.get('_event_type', '')
group = data.get('_group', 'other')
- section_label = self._GROUP_LABELS.get(group, 'System Notification')
+ section_label = _runtime_text(f'email.groups.{group}', data)
+ report_label = _runtime_text('email.report', data, group=section_label)
+ host_label = _runtime_text('email.host', data)
+ footer_label = _runtime_text('email.footer', data)
# Timestamp
ts = data.get('timestamp', '') or _time.strftime('%Y-%m-%d %H:%M:%S UTC', _time.gmtime())
@@ -949,6 +1050,19 @@ 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. Keep their
+ # lines exactly once, but retain the localized structured status row;
+ # the remaining structured backup metadata only duplicates the report.
+ if event_type in {'backup_complete', 'backup_fail'}:
+ status_label = html_mod.escape(
+ _runtime_text('email.fields.status', data)
+ )
+ detail_rows = [row for row in detail_rows if row[0] == status_label]
+ detail_rows.extend(
+ ('', 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'):
@@ -983,7 +1097,7 @@ class EmailChannel(NotificationChannel):
if reason and len(reason) > 80:
reason_html = f'''
-
Details
+
{_runtime_text('email.details', data)}
{html_mod.escape(reason)}
'''
@@ -993,7 +1107,7 @@ class EmailChannel(NotificationChannel):
display_title = display_title.replace(prefix, '').strip()
return f'''
-
+
@@ -1004,7 +1118,7 @@ class EmailChannel(NotificationChannel):
ProxMenux Monitor
- {html_mod.escape(section_label)} Report
+ {html_mod.escape(report_label)}
|
{sev['label'].upper()}
@@ -1024,7 +1138,7 @@ class EmailChannel(NotificationChannel):
|
- Host: {html_mod.escape(data.get('hostname', ''))}
+ {html_mod.escape(host_label)}: {html_mod.escape(data.get('hostname', ''))}
|
{html_mod.escape(ts)}
@@ -1044,7 +1158,7 @@ class EmailChannel(NotificationChannel):
- | ProxMenux Notification Service |
+ {html_mod.escape(footer_label)} |
proxmenux.com |
@@ -1064,11 +1178,37 @@ class EmailChannel(NotificationChannel):
"""
esc = html_mod.escape
rows = []
+ field_keys = {
+ 'VM/CT ID': 'vmCtId', 'Name': 'name', 'Action': 'action',
+ 'Target Node': 'targetNode', 'Reason': 'reason', 'Storage': 'storage',
+ 'Status': 'status', 'Size': 'size', 'Duration': 'duration',
+ 'Snapshot': 'snapshot', 'Metric': 'metric', 'Current Value': 'currentValue',
+ 'Threshold': 'threshold', 'CPU Cores': 'cpuCores', 'Memory': 'memory',
+ 'Temperature': 'temperature', 'Mount Point': 'mountPoint', 'Usage': 'usage',
+ 'Available': 'available', 'Device': 'device', 'Severity': 'severity',
+ 'Storage Name': 'storageName', 'Type': 'type', 'Interface': 'interface',
+ 'Latency': 'latency', 'Event': 'event', 'Source IP': 'sourceIp',
+ 'Username': 'username', 'Service': 'service', 'Jail': 'jail',
+ 'Failures': 'failures', 'Change': 'change', 'Node': 'node',
+ 'Quorum': 'quorum', 'Nodes Affected': 'nodesAffected', 'Process': 'process',
+ 'Details': 'reason', 'Category': 'category',
+ 'Previous Severity': 'previousSeverity', 'Active Issues': 'activeIssues',
+ 'Total Updates': 'totalUpdates', 'Security Updates': 'securityUpdates',
+ 'Proxmox Updates': 'proxmoxUpdates', 'Kernel Updates': 'kernelUpdates',
+ 'Important Packages': 'importantPackages', 'Current Version': 'currentVersion',
+ 'New Version': 'newVersion',
+ }
+ language_data = data
+
+ def _event_label() -> str:
+ return _runtime_notification_text(f'templates.{event_type}.label', language_data)
def _add(label: str, value, fmt: str = ''):
- """Add a row if value is truthy."""
+ """Add a localized row if value is truthy."""
+ original_label = label
+ label = _runtime_text(f"email.fields.{field_keys[label]}", language_data)
v = str(value).strip() if value else ''
- if not v or v == '0' and label not in ('Failures',):
+ if not v or v == '0' and original_label not in ('Failures',):
return
if fmt == 'severity':
sev_colors = {
@@ -1090,7 +1230,8 @@ class EmailChannel(NotificationChannel):
if group == 'vm_ct':
_add('VM/CT ID', data.get('vmid'), 'code')
_add('Name', data.get('vmname'), 'bold')
- _add('Action', event_type.replace('_', ' ').replace('vm ', 'VM ').replace('ct ', 'CT ').title())
+ action = _runtime_notification_text(f'templates.{event_type}.label', data)
+ _add('Action', action)
_add('Target Node', data.get('target_node'))
_add('Reason', data.get('reason'))
@@ -1102,7 +1243,8 @@ class EmailChannel(NotificationChannel):
# tell which target the backup ran against. Reported gap: emails
# showed no way to distinguish which PBS failed with 2+ configured.
_add('Storage', data.get('storage') or data.get('storage_name'), 'code')
- _add('Status', 'Failed' if 'fail' in event_type else 'Completed' if 'complete' in event_type else 'Started',
+ status_key = 'failed' if 'fail' in event_type else 'completed' if 'complete' in event_type else 'started'
+ _add('Status', _runtime_text(f'email.status.{status_key}', language_data),
'severity' if 'fail' in event_type else '')
_add('Size', data.get('size'))
_add('Duration', data.get('duration'))
@@ -1114,7 +1256,7 @@ class EmailChannel(NotificationChannel):
# ── Resources ──
elif group == 'resources':
- _add('Metric', event_type.replace('_', ' ').title())
+ _add('Metric', _event_label())
_add('Current Value', data.get('value'), 'bold')
_add('Threshold', data.get('threshold'))
_add('CPU Cores', data.get('cores'))
@@ -1148,7 +1290,7 @@ class EmailChannel(NotificationChannel):
# ── Security ──
elif group == 'security':
- _add('Event', event_type.replace('_', ' ').title())
+ _add('Event', _event_label())
_add('Source IP', data.get('source_ip'), 'code')
_add('Username', data.get('username'), 'code')
_add('Service', data.get('service'))
@@ -1158,7 +1300,7 @@ class EmailChannel(NotificationChannel):
# ── Cluster ──
elif group == 'cluster':
- _add('Event', event_type.replace('_', ' ').title())
+ _add('Event', _event_label())
_add('Node', data.get('node_name'), 'bold')
_add('Quorum', data.get('quorum'))
_add('Nodes Affected', data.get('entity_list'))
@@ -1167,7 +1309,7 @@ class EmailChannel(NotificationChannel):
elif group == 'services':
_add('Service', data.get('service_name'), 'code')
_add('Process', data.get('process'), 'code')
- _add('Event', event_type.replace('_', ' ').title())
+ _add('Event', _event_label())
reason = data.get('reason', '')
if reason and len(reason) <= 80:
_add('Details', reason)
@@ -1199,7 +1341,7 @@ class EmailChannel(NotificationChannel):
f' {esc(p)}'
for p in pkg_lines
)
- rows.append((esc('Important Packages'), pkg_html))
+ rows.append((esc(_runtime_text('email.fields.importantPackages', language_data)), pkg_html))
_add('Current Version', data.get('current_version'), 'code')
# `new_version` is the field used by generic package-update events;
# driver-update templates (nvidia, coral) populate `latest_version`.
@@ -1207,6 +1349,13 @@ class EmailChannel(NotificationChannel):
# title/body already printed the new version.
_add('New Version', data.get('new_version') or data.get('latest_version'), 'code')
+ # ── Generic system events ──
+ elif group == 'system':
+ _add('Event', _event_label())
+ reason = data.get('reason', '')
+ if reason and len(reason) <= 80:
+ _add('Details', reason)
+
# ── Other / unknown ──
else:
reason = data.get('reason', '')
diff --git a/AppImage/scripts/notification_events.py b/AppImage/scripts/notification_events.py
index 82e8429e..86a75ae5 100644
--- a/AppImage/scripts/notification_events.py
+++ b/AppImage/scripts/notification_events.py
@@ -3517,7 +3517,9 @@ class PollingCollector:
'security_count': str(len(security_pkgs)),
'pve_count': str(len(pve_pkgs)),
'kernel_count': str(len(kernel_pkgs)),
- 'important_list': '\n'.join(f' \u2022 {l}' for l in important_lines) if important_lines else 'none',
+ # An empty value is intentionally rendered later by the
+ # notification template as the selected locale's “none”.
+ 'important_list': '\n'.join(f' \u2022 {l}' for l in important_lines),
'package_list': ', '.join(important_lines[:6]) if important_lines else '',
}
diff --git a/AppImage/scripts/notification_manager.py b/AppImage/scripts/notification_manager.py
index 0a14ea05..8dcb4ca1 100644
--- a/AppImage/scripts/notification_manager.py
+++ b/AppImage/scripts/notification_manager.py
@@ -41,7 +41,8 @@ if BASE_DIR not in sys.path:
from notification_channels import create_channel, CHANNEL_TYPES
from notification_templates import (
render_template, format_with_ai, format_with_ai_full, enrich_with_emojis, TEMPLATES,
- EVENT_GROUPS, get_event_types_by_group, get_default_enabled_events
+ EVENT_GROUPS, CATEGORY_EMOJI, EVENT_EMOJI, get_event_types_by_group,
+ get_default_enabled_events, runtime_message,
)
from notification_events import (
JournalWatcher, TaskWatcher, PollingCollector, NotificationEvent,
@@ -61,6 +62,17 @@ except ImportError:
DB_PATH = Path('/usr/local/share/proxmenux/health_monitor.db')
SETTINGS_PREFIX = 'notification.'
ENCRYPTION_KEY_FILE = Path('/usr/local/share/proxmenux/.notification_key')
+RUNTIME_NOTIFICATION_LANGUAGES = ('en', 'de', 'es', 'fr', 'it', 'pt', 'sk', 'sv')
+ALLOWED_AI_LANGUAGES = (
+ 'en', 'sk', 'es', 'fr', 'de', 'it', 'pt', 'ru', 'sv', 'no',
+ 'ja', 'zh', 'ko', 'pl', 'nl', 'tr', 'ar',
+)
+_AI_BYPASS_EVENTS = frozenset({'backup_complete', 'backup_fail'})
+
+
+def _should_bypass_ai(event_type: str) -> bool:
+ """Keep complete backup inventories deterministic on every send path."""
+ return event_type in _AI_BYPASS_EVENTS
# Keys that contain sensitive data and should be encrypted
SENSITIVE_KEYS = {
@@ -200,6 +212,40 @@ def _resolve_display_hostname(config: Optional[Dict[str, str]] = None) -> str:
return socket.gethostname()
+def resolve_notification_hostname(value: Any = None,
+ config: Optional[Dict[str, str]] = None) -> str:
+ """Return the configured display name for a hostname of this local node.
+
+ Some event producers pass ``socket.gethostname()`` explicitly while
+ others let the notification layer resolve it. A configured display name
+ must have the same result in both cases. Keep a hostname that is not an
+ alias of this machine intact: it can identify a remote node forwarded to
+ this monitor.
+ """
+ configured_name = (config or {}).get('hostname', '')
+ configured_name = str(configured_name or '').strip()
+ candidate = str(value or '').strip()
+
+ if not configured_name:
+ return candidate or _resolve_display_hostname(config)
+ if not candidate:
+ return configured_name
+
+ local_aliases = set()
+ for resolver in (socket.gethostname, socket.getfqdn):
+ try:
+ hostname = str(resolver() or '').strip()
+ except Exception:
+ hostname = ''
+ if hostname:
+ local_aliases.add(hostname.casefold())
+ local_aliases.add(hostname.split('.', 1)[0].casefold())
+
+ if candidate.casefold() in local_aliases:
+ return configured_name
+ return candidate
+
+
# ─── Encryption for Sensitive Data ───────────────────────────────
#
# Audit Tier 4 #24 flagged the previous implementation as trivially reversible:
@@ -934,6 +980,21 @@ class NotificationManager:
or self._config.get('ai_model', '')
)
+ def _notification_language(self) -> str:
+ """Return a bundled runtime locale, with a safe English fallback.
+
+ Older installations used the AI language for every notification. Keep
+ that value as a migration fallback only; notification text is now
+ independent of whether AI enhancement is enabled.
+ """
+ selected = str(self._config.get('notification_language', '')).strip().lower()
+ if selected in RUNTIME_NOTIFICATION_LANGUAGES:
+ return selected
+ legacy = str(self._config.get('ai_language', '')).strip().lower()
+ if legacy in RUNTIME_NOTIFICATION_LANGUAGES:
+ return legacy
+ return 'en'
+
def _build_ai_config(self) -> Dict[str, Any]:
"""Build the shared AI config passed to notification rewriters."""
ai_provider = self._config.get('ai_provider', 'groq')
@@ -1197,6 +1258,13 @@ class NotificationManager:
def _dispatch_event(self, event: NotificationEvent):
"""Shared dispatch pipeline: cooldown -> rate limit -> render -> send."""
+ # Event sources may supply the local kernel hostname themselves.
+ # Normalize it here so every delivery path honours the configured
+ # notification display name, including newly added event producers.
+ event.data['hostname'] = resolve_notification_hostname(
+ event.data.get('hostname'), self._config,
+ )
+
# Suppress VM/CT start/stop during active backups (second layer of defense).
# The primary filter is in TaskWatcher, but timing gaps can let events
# slip through. This catch-all filter checks at dispatch time.
@@ -1232,9 +1300,13 @@ class NotificationManager:
severity = event.severity
event.data['severity'] = severity
- rendered = render_template(event.event_type, event.data)
+ notification_language = self._notification_language()
+ rendered = render_template(
+ event.event_type, event.data, language=notification_language,
+ )
enriched_data = dict(event.data)
+ enriched_data['_notification_language'] = notification_language
enriched_data['_rendered_fields'] = rendered.get('fields', [])
enriched_data['_body_html'] = rendered.get('body_html', '')
enriched_data['_event_type'] = event.event_type
@@ -1376,27 +1448,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 not _should_bypass_ai(event_type):
+ 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)
@@ -1525,6 +1602,10 @@ class NotificationManager:
'vm_fail', 'ct_fail',
'system_shutdown', 'system_reboot',
})
+ # A task completion can be observed twice through adjacent collectors.
+ # Keep the daily digest useful by coalescing only byte-for-byte identical
+ # buffered INFO entries that arrive close together.
+ _DIGEST_DUPLICATE_WINDOW = 300 # seconds
def _should_buffer_for_digest(self, ch_name: str, severity: str,
event_type: str) -> bool:
@@ -1555,12 +1636,42 @@ class NotificationManager:
conn = sqlite3.connect(str(DB_PATH), timeout=10)
conn.execute('PRAGMA journal_mode=WAL')
conn.execute('PRAGMA busy_timeout=5000')
+ # Adjacent collectors can observe one completed LXC update with
+ # different transport details (for example source or duration).
+ # The rendered title already identifies its LXC and result, so
+ # coalesce that narrowly. Other event types retain the stricter
+ # title-and-body comparison so distinct updates stay visible.
+ now = int(time.time())
+ if event_type == 'lxc_update_applied':
+ duplicate = conn.execute(
+ 'SELECT 1 FROM digest_pending '
+ 'WHERE channel = ? AND event_type = ? AND event_group = ? '
+ 'AND severity = ? AND title = ? AND ts >= ? LIMIT 1',
+ (
+ ch_name, event_type, event_group, severity, title,
+ now - self._DIGEST_DUPLICATE_WINDOW,
+ ),
+ ).fetchone()
+ else:
+ duplicate = conn.execute(
+ 'SELECT 1 FROM digest_pending '
+ 'WHERE channel = ? AND event_type = ? AND event_group = ? '
+ 'AND severity = ? AND title = ? AND body = ? AND ts >= ? '
+ 'LIMIT 1',
+ (
+ ch_name, event_type, event_group, severity, title, body,
+ now - self._DIGEST_DUPLICATE_WINDOW,
+ ),
+ ).fetchone()
+ if duplicate:
+ conn.close()
+ return
conn.execute(
'INSERT INTO digest_pending '
'(channel, event_type, event_group, severity, ts, title, body) '
'VALUES (?, ?, ?, ?, ?, ?, ?)',
(ch_name, event_type, event_group, severity,
- int(time.time()), title, body),
+ now, title, body),
)
conn.commit()
conn.close()
@@ -1618,9 +1729,14 @@ class NotificationManager:
(issue #233).
"""
host = _resolve_display_hostname(self._config)
- summary_title = (
- f"{host}: 24h summary ({now.strftime('%Y-%m-%d %H:%M')})"
+ language = self._notification_language()
+ summary_title = runtime_message(
+ 'digest.title', language, hostname=host,
+ timestamp=now.strftime('%Y-%m-%d %H:%M'),
)
+ rich_format = self._config.get(f'{ch_name}.rich_format', 'false') == 'true'
+ if rich_format:
+ summary_title = f'📋 {summary_title}'
try:
conn = sqlite3.connect(str(DB_PATH), timeout=10)
@@ -1637,7 +1753,7 @@ class NotificationManager:
print(f"[NotificationManager] digest read failed for {ch_name}: {e}")
self._record_history(
'digest', ch_name, summary_title,
- f'digest read failed: {e}', 'INFO',
+ runtime_message('digest.readFailed', language, error=e), 'INFO',
False, str(e), 'digest_scheduler',
)
self._stats['total_errors'] += 1
@@ -1655,17 +1771,18 @@ class NotificationManager:
# just nothing INFO non-exempt to summarize.
self._record_history(
'digest', ch_name, summary_title,
- 'No INFO events buffered for this digest window.',
+ runtime_message('digest.empty', language),
'INFO', True, '', 'digest_scheduler',
)
return
- summary_body = self._compose_digest_body(rows)
+ summary_body = self._compose_digest_body(rows, use_icons=rich_format)
result: dict = {'success': False, 'error': ''}
try:
result = channel.send(summary_title, summary_body, severity='INFO',
- data={'_digest': True, '_count': len(rows)}) or result
+ data={'_digest': True, '_count': len(rows),
+ '_notification_language': language}) or result
except Exception as e:
print(f"[NotificationManager] digest send failed for "
f"{ch_name}: {e}")
@@ -1703,7 +1820,7 @@ class NotificationManager:
print(f"[NotificationManager] digest cleanup failed for "
f"{ch_name}: {e}")
- def _compose_digest_body(self, rows: list) -> str:
+ def _compose_digest_body(self, rows: list, use_icons: bool = False) -> str:
"""Render a grouped summary body. rows is a list of
(id, event_type, event_group, ts, title, body) tuples ordered
by timestamp ASC.
@@ -1714,20 +1831,25 @@ class NotificationManager:
label = group or 'other'
groups.setdefault(label, []).append((ts, ev_type, title))
- lines = [f"{len(rows)} INFO events grouped by category:\n"]
+ language = self._notification_language()
+ lines = [runtime_message('digest.lead', language, count=len(rows))]
for group, items in groups.items():
- lines.append(f"{group.title()}: {len(items)}")
+ group_label = runtime_message(f'digest.groups.{group}', language) or group.title()
+ group_icon = CATEGORY_EMOJI.get(group, '') if use_icons else ''
+ group_prefix = f'{group_icon} ' if group_icon else ''
+ lines.append(f"{group_prefix}{group_label}: {len(items)}")
for ts, ev_type, title in items[:8]:
hhmm = datetime.fromtimestamp(ts).strftime('%H:%M')
short_title = title.split(': ', 1)[-1] if ': ' in title else title
- lines.append(f" • {hhmm} {short_title}")
+ event_icon = (
+ EVENT_EMOJI.get(ev_type) or CATEGORY_EMOJI.get(group, '')
+ ) if use_icons else ''
+ event_prefix = f'{event_icon} ' if event_icon else ''
+ lines.append(f" • {event_prefix}{hhmm} {short_title}")
if len(items) > 8:
- lines.append(f" • … and {len(items) - 8} more")
+ lines.append(runtime_message('digest.more', language, count=len(items) - 8))
lines.append('')
- lines.append(
- '(Critical/Warning events arrived at the time they happened, '
- 'not in this digest.)'
- )
+ lines.append(runtime_message('digest.footer', language))
return '\n'.join(lines).rstrip() + '\n'
# ─── Quiet Hours buffer + flush ────────────────────────────
@@ -1857,16 +1979,19 @@ class NotificationManager:
return
host = _resolve_display_hostname(self._config)
- summary_title = (
- f"{host}: {len(rows)} events buffered during Quiet Hours"
+ language = self._notification_language()
+ summary_title = runtime_message(
+ 'digest.quietTitle', language, hostname=host, count=len(rows),
)
- summary_body = self._compose_digest_body(rows)
+ use_icons = self._config.get(f'{ch_name}.rich_format', 'false') == 'true'
+ summary_body = self._compose_digest_body(rows, use_icons=use_icons)
result: dict = {'success': False, 'error': ''}
try:
result = channel.send(
summary_title, summary_body, severity='INFO',
- data={'_quiet_hours_summary': True, '_count': len(rows)},
+ data={'_quiet_hours_summary': True, '_count': len(rows),
+ '_notification_language': language},
) or result
except Exception as e:
print(f"[NotificationManager] quiet send failed for "
@@ -2336,12 +2461,23 @@ class NotificationManager:
'skipped': True,
}
+ runtime_data = dict(data or {})
+ runtime_data['hostname'] = resolve_notification_hostname(
+ runtime_data.get('hostname'), self._config,
+ )
+ runtime_data.setdefault('_notification_language', self._notification_language())
+
# Render template if available
if event_type in TEMPLATES and not message:
- rendered = render_template(event_type, data or {})
+ rendered = render_template(
+ event_type, runtime_data,
+ language=runtime_data['_notification_language'],
+ )
title = title or rendered['title']
message = rendered['body']
severity = severity or rendered['severity']
+
+ data = runtime_data
# AI config for enhancement
ai_config = self._build_ai_config()
@@ -2364,13 +2500,16 @@ class NotificationManager:
# Pass channel_type so AI knows whether to append original (email only)
channel_ai_config = {**ai_config, 'channel_type': ch_name}
- ai_result = format_with_ai_full(
- title, message, severity, channel_ai_config,
- detail_level=detail_level,
- use_emojis=use_rich_format
- )
- ch_title = ai_result.get('title', title)
- ch_message = ai_result.get('body', message)
+ if _should_bypass_ai(event_type):
+ ch_title, ch_message = title, message
+ else:
+ ai_result = format_with_ai_full(
+ title, message, severity, channel_ai_config,
+ detail_level=detail_level,
+ use_emojis=use_rich_format
+ )
+ ch_title = ai_result.get('title', title)
+ ch_message = ai_result.get('body', message)
result = channel.send(ch_title, ch_message, severity, data)
results[ch_name] = result
@@ -2403,6 +2542,32 @@ class NotificationManager:
return self.send_notification(
'custom', severity, title, message, source=source
)
+
+ def _build_test_message(self, use_rich_format: bool, ai_enabled: bool,
+ ai_info: str) -> tuple:
+ """Build the channel test payload in the selected notification language."""
+ language = self._notification_language()
+ icon_key = 'test.iconsEnabled' if use_rich_format else 'test.iconsDisabled'
+ ai_key = 'test.aiEnabled' if ai_enabled else 'test.aiDisabled'
+ icon_status = runtime_message(icon_key, language)
+ ai_status = runtime_message(ai_key, language, info=ai_info)
+ if use_rich_format:
+ icon_status = f'✅ {icon_status}'
+ ai_status = f'✅ {ai_status}' if ai_enabled else f'❌ {ai_status}'
+ body = '\n\n'.join([
+ runtime_message('test.welcome', language),
+ runtime_message('test.verify', language),
+ '\n'.join([
+ runtime_message('test.configuration', language),
+ icon_status,
+ ai_status,
+ ]),
+ runtime_message('test.alerts', language),
+ ])
+ return (
+ runtime_message('test.title', language), body,
+ runtime_message('test.photoCaption', language),
+ )
def test_channel(self, channel_name: str = 'all') -> Dict[str, Any]:
"""Test one or all configured channels with AI enhancement."""
@@ -2448,7 +2613,6 @@ class NotificationManager:
# ProxMenux logo for welcome message
logo_url = 'https://proxmenux.com/telegram.png'
- logo_caption = 'You can use this image as the profile photo for your notification bot.'
for ch_name, channel in targets.items():
try:
@@ -2459,25 +2623,8 @@ class NotificationManager:
rich_key = f'{ch_name}.rich_format'
use_rich_format = self._config.get(rich_key, 'false') == 'true'
- # Build status indicators for icons and AI, adapted to channel format
- if use_rich_format:
- icon_status = '✅ Icons: enabled'
- ai_status = f'✅ AI: enabled ({ai_info})' if ai_enabled else '❌ AI: disabled'
- else:
- icon_status = 'Icons: disabled'
- ai_status = f'AI: enabled ({ai_info})' if ai_enabled else 'AI: disabled'
-
- # Base test message — shows current channel config
- # NOTE: narrative lines are intentionally unlabeled so the AI
- # does not prepend "Message:" or other spurious field labels.
- base_title = 'ProxMenux Test'
- base_message = (
- 'Welcome to ProxMenux Monitor!\n\n'
- 'This is a test message to verify your notification channel is working correctly.\n\n'
- 'Channel configuration:\n'
- f'{icon_status}\n'
- f'{ai_status}\n\n'
- 'You will receive alerts about system events, health status changes, and security incidents.'
+ base_title, base_message, logo_caption = self._build_test_message(
+ use_rich_format, ai_enabled, ai_info,
)
# Apply AI enhancement (translates to configured language)
@@ -2492,7 +2639,11 @@ class NotificationManager:
enhanced_message = ai_result.get('body', base_message)
# Send message
- send_result = channel.send(enhanced_title, enhanced_message, 'INFO')
+ send_result = channel.send(
+ enhanced_title, enhanced_message, 'INFO',
+ data={'_notification_language': self._notification_language(),
+ '_event_type': 'test', '_group': 'other'},
+ )
success = send_result.get('success', False)
error = send_result.get('error', '')
@@ -2865,6 +3016,7 @@ class NotificationManager:
'ai_api_keys': ai_api_keys,
'ai_models': ai_models,
'ai_model': self._active_ai_model(current_provider),
+ 'notification_language': self._notification_language(),
'ai_language': self._config.get('ai_language', 'en'),
'ai_ollama_url': self._config.get('ai_ollama_url', 'http://localhost:11434'),
'ai_openai_base_url': self._config.get('ai_openai_base_url', ''),
@@ -2888,6 +3040,7 @@ class NotificationManager:
def save_settings(self, settings: Dict[str, str]) -> Dict[str, Any]:
"""Save multiple notification settings at once."""
try:
+ previous_config = dict(self._config)
conn = sqlite3.connect(str(DB_PATH), timeout=10)
conn.execute('PRAGMA journal_mode=WAL')
conn.execute('PRAGMA busy_timeout=5000')
@@ -2944,6 +3097,11 @@ class NotificationManager:
if short_key == 'ai_language':
if str(value) not in _ALLOWED_AI_LANGUAGES:
raise ValueError(f"Invalid ai_language: must be one of {_ALLOWED_AI_LANGUAGES}")
+ if short_key == 'notification_language':
+ if str(value) not in RUNTIME_NOTIFICATION_LANGUAGES:
+ raise ValueError(
+ f"Invalid notification_language: must be one of {RUNTIME_NOTIFICATION_LANGUAGES}"
+ )
# Encrypt sensitive values before storing. Skip if the value is
# already in either encrypted form — `encrypt_sensitive_value`
@@ -2975,6 +3133,38 @@ class NotificationManager:
VALUES (?, ?, ?)
''', (marker_key, 'true', now))
self._config[f'event_explicit.{event_type}'] = 'true'
+
+ # A digest time can be changed after today's digest has already
+ # been sent. Retaining digest_last_at in that case silently
+ # makes the newly selected, still-future time wait until tomorrow.
+ # Reset the guard only for a genuine enable/time change to a later
+ # time today; ordinary saves and past times remain rate-limited.
+ now = datetime.now()
+ current_minute = now.hour * 60 + now.minute
+ for ch_type in CHANNEL_TYPES:
+ enabled_key = f'{ch_type}.digest_enabled'
+ time_key = f'{ch_type}.digest_time'
+ last_key = f'{ch_type}.digest_last_at'
+ if self._config.get(enabled_key, 'false') != 'true':
+ continue
+ changed = (
+ previous_config.get(enabled_key, 'false') != 'true'
+ or previous_config.get(time_key, '09:00') != self._config.get(time_key, '09:00')
+ )
+ if not changed:
+ continue
+ try:
+ hour, minute = (int(part) for part in self._config.get(time_key, '09:00').split(':', 1))
+ target_minute = hour * 60 + minute
+ except (ValueError, AttributeError):
+ continue
+ if not (current_minute < target_minute < 24 * 60):
+ continue
+ cursor.execute('''
+ INSERT OR REPLACE INTO user_settings (setting_key, setting_value, updated_at)
+ VALUES (?, ?, ?)
+ ''', (f'{SETTINGS_PREFIX}{last_key}', '', now.isoformat()))
+ self._config[last_key] = ''
conn.commit()
conn.close()
diff --git a/AppImage/scripts/notification_templates.py b/AppImage/scripts/notification_templates.py
index 2befce56..66a9065a 100644
--- a/AppImage/scripts/notification_templates.py
+++ b/AppImage/scripts/notification_templates.py
@@ -17,9 +17,193 @@ import socket
import time
import urllib.request
import urllib.error
+from functools import lru_cache
+from pathlib import Path
from typing import Dict, Any, Optional, List, Tuple
+_SCRIPT_ROOT = Path(__file__).resolve().parents[1]
+_BUNDLED_CATALOG_DIR = _SCRIPT_ROOT / 'share' / 'proxmenux' / 'messages'
+_SOURCE_CATALOG_DIR = _SCRIPT_ROOT / 'messages'
+RUNTIME_CATALOG_DIR = (
+ _BUNDLED_CATALOG_DIR
+ if _BUNDLED_CATALOG_DIR.is_dir()
+ else _SOURCE_CATALOG_DIR
+ if _SOURCE_CATALOG_DIR.is_dir()
+ else Path('/usr/share/proxmenux/messages')
+)
+
+
+class _SafeFormatDict(dict):
+ def __missing__(self, key):
+ return ''
+
+
+@lru_cache(maxsize=8)
+def _load_runtime_catalog(language: str) -> Dict[str, Any]:
+ """Load one existing Monitor catalog's runtime notification namespace."""
+ path = RUNTIME_CATALOG_DIR / language / 'common.json'
+ try:
+ with path.open(encoding='utf-8') as handle:
+ return json.load(handle).get('runtime', {}).get('notifications', {})
+ except (OSError, ValueError, TypeError):
+ return {}
+
+
+def _catalog_value(catalog: Dict[str, Any], dotted_key: str) -> Optional[str]:
+ value: Any = catalog
+ for part in dotted_key.split('.'):
+ if not isinstance(value, dict) or part not in value:
+ return None
+ value = value[part]
+ return value if isinstance(value, str) and value else None
+
+
+def runtime_message(key: str, language: str = 'en', **values: Any) -> str:
+ """Resolve runtime text with per-key English fallback and safe placeholders."""
+ requested = (language or 'en').split('-', 1)[0].lower()
+ value = _catalog_value(_load_runtime_catalog(requested), key)
+ if value is None:
+ value = _catalog_value(_load_runtime_catalog('en'), key)
+ if value is None:
+ return ''
+ try:
+ return value.format_map(_SafeFormatDict(values))
+ except (ValueError, IndexError):
+ return value
+
+
+def _format_lxc_update_details(data: Dict[str, Any], language: str) -> str:
+ """Render an LXC update outcome from structured data in the selected locale."""
+ update = data.get('lxc_update')
+ if not isinstance(update, dict):
+ return str(data.get('details') or '')
+
+ def _mapping(value: Any) -> Dict[str, Any]:
+ return value if isinstance(value, dict) else {}
+
+ def _items(value: Any) -> List[str]:
+ return [str(item) for item in value if str(item)] if isinstance(value, list) else []
+
+ source = 'scheduled' if update.get('source') == 'scheduled' else 'manual'
+ labels = _items(update.get('labels')) or _items(update.get('targets'))
+ lines = [
+ runtime_message('lxcUpdate.sourceLabel', language)
+ + ': ' + runtime_message(f'lxcUpdate.source.{source}', language),
+ runtime_message('lxcUpdate.targets', language) + ': ' + ', '.join(labels),
+ ]
+
+ targets = _items(update.get('targets'))
+ before = _mapping(update.get('before'))
+ after = _mapping(update.get('after'))
+ if 'os' in targets:
+ before_count = before.get('os_pending')
+ after_count = after.get('os_pending')
+ if isinstance(before_count, int) and isinstance(after_count, int):
+ lines.append(runtime_message(
+ 'lxcUpdate.osPending', language, before=before_count, after=after_count,
+ ))
+ else:
+ lines.append(runtime_message('lxcUpdate.osUnverified', language))
+
+ before_apps = _mapping(before.get('apps'))
+ after_apps = _mapping(after.get('apps'))
+ app_ids = {
+ target.split(':', 1)[1]
+ for target in targets if target.startswith('app:')
+ }
+ if 'apps' in targets:
+ app_ids.update(str(app_id) for app_id in before_apps)
+ app_ids.update(str(app_id) for app_id in after_apps)
+ app_lines = []
+ for app_id in sorted(app_ids):
+ old = _mapping(before_apps.get(app_id))
+ new = _mapping(after_apps.get(app_id))
+ old_version = old.get('installed_version')
+ new_version = new.get('installed_version')
+ if old_version and new_version and old_version != new_version:
+ app_lines.append(
+ f"{new.get('name') or old.get('name') or app_id}: "
+ f'{old_version} → {new_version}'
+ )
+ if app_lines:
+ lines.append(runtime_message(
+ 'lxcUpdate.applications', language, items='; '.join(app_lines[:8]),
+ ))
+ elif app_ids:
+ lines.append(runtime_message('lxcUpdate.applicationsUnverified', language))
+
+ docker_requested = any(target.startswith('docker-') for target in targets)
+ before_docker = _mapping(before.get('docker_inventory'))
+ after_docker = _mapping(after.get('docker_inventory'))
+ if 'docker-engine' in targets:
+ old_engine = before_docker.get('engine_version')
+ new_engine = after_docker.get('engine_version')
+ if old_engine and new_engine and old_engine != new_engine:
+ lines.append(runtime_message(
+ 'lxcUpdate.dockerEngineChange', language, before=old_engine, after=new_engine,
+ ))
+ elif new_engine:
+ lines.append(runtime_message(
+ 'lxcUpdate.dockerEngineVerified', language, version=new_engine,
+ ))
+ else:
+ lines.append(runtime_message('lxcUpdate.dockerEngineUnverified', language))
+ if docker_requested and any(target != 'docker-engine' for target in targets):
+ before_pending = before_docker.get('update_count')
+ after_pending = after_docker.get('update_count')
+ if isinstance(before_pending, int) and isinstance(after_pending, int):
+ lines.append(runtime_message(
+ 'lxcUpdate.dockerImagesPending', language,
+ before=before_pending, after=after_pending,
+ ))
+ after_by_ref = {
+ str(item.get('reference')): item
+ for item in after_docker.get('images') or []
+ if isinstance(item, dict) and item.get('reference')
+ }
+ changed_images = []
+ for old_image in before_docker.get('images') or []:
+ if not isinstance(old_image, dict):
+ continue
+ reference = str(old_image.get('reference') or '')
+ new_image = _mapping(after_by_ref.get(reference))
+ if reference and old_image.get('local_digest') != new_image.get('local_digest'):
+ changed_images.append(reference)
+ if changed_images:
+ lines.append(runtime_message(
+ 'lxcUpdate.dockerImagesChanged', language,
+ images=', '.join(changed_images[:8]),
+ ))
+
+ deferred = _items(update.get('deferred_targets'))
+ if deferred:
+ lines.append(runtime_message(
+ 'lxcUpdate.deferredTargets', language, targets=', '.join(deferred),
+ ))
+ reason = str(update.get('reason') or '').strip()
+ if reason:
+ lines.append(runtime_message('lxcUpdate.reason', language, reason=reason))
+ reboot_required = update.get('reboot_required')
+ if reboot_required is not None:
+ value = runtime_message(
+ 'lxcUpdate.yes' if reboot_required else 'lxcUpdate.no', language,
+ )
+ lines.append(runtime_message(
+ 'lxcUpdate.restartRequired', language, value=value,
+ ))
+ if update.get('verification_pending'):
+ lines.append(runtime_message('lxcUpdate.verificationPending', language))
+ for error in _items(update.get('verification_errors'))[:4]:
+ lines.append(runtime_message(
+ 'lxcUpdate.verificationWarning', language, error=error,
+ ))
+ lines.append(runtime_message(
+ 'lxcUpdate.duration', language, duration=update.get('duration') or '',
+ ))
+ return '\n'.join(lines)
+
+
# ─── vzdump message parser ───────────────────────────────────────
def _parse_vzdump_message(message: str) -> Optional[Dict[str, Any]]:
@@ -248,7 +432,8 @@ def _parse_vzdump_message(message: str) -> Optional[Dict[str, Any]]:
}
-def _format_vzdump_body(parsed: Dict[str, Any], is_success: bool) -> str:
+def _format_vzdump_body(parsed: Dict[str, Any], is_success: bool,
+ language: str = 'en') -> str:
"""Format parsed vzdump data into a clean Telegram-friendly message."""
parts = []
@@ -285,9 +470,9 @@ def _format_vzdump_body(parsed: Dict[str, Any], is_success: bool) -> str:
# Size and Duration on same line with icons
detail_line = []
if vm.get('size'):
- detail_line.append(f"\U0001F4CF Size: {vm['size']}")
+ detail_line.append(f"\U0001F4CF {runtime_message('vzdump.size', language, value=vm['size'])}")
if vm.get('time'):
- detail_line.append(f"\u23F1\uFE0F Duration: {vm['time']}")
+ detail_line.append(f"\u23F1\uFE0F {runtime_message('vzdump.duration', language, value=vm['time'])}")
if detail_line:
parts.append(' | '.join(detail_line))
@@ -302,7 +487,7 @@ def _format_vzdump_body(parsed: Dict[str, Any], is_success: bool) -> str:
label = storage_name if storage_name else 'PBS'
parts.append(f"\U0001F5C4\uFE0F {label}: {fname}")
else:
- label = storage_name if storage_name else 'File'
+ label = storage_name if storage_name else runtime_message('vzdump.file', language)
parts.append(f"\U0001F4C1 {label}: {fname}")
# Error reason if failed
@@ -320,13 +505,13 @@ def _format_vzdump_body(parsed: Dict[str, Any], is_success: bool) -> str:
summary_parts = []
if vm_count:
- summary_parts.append(f"\U0001F4CA {vm_count} backups")
+ summary_parts.append(f"\U0001F4CA {runtime_message('vzdump.backups', language, count=vm_count)}")
if fail_count:
- summary_parts.append(f"\u274C {fail_count} failed")
+ summary_parts.append(f"\u274C {runtime_message('vzdump.failed', language, count=fail_count)}")
if parsed.get('total_size'):
- summary_parts.append(f"\U0001F4E6 Total: {parsed['total_size']}")
+ summary_parts.append(f"\U0001F4E6 {runtime_message('vzdump.total', language, value=parsed['total_size'])}")
if parsed.get('total_time'):
- summary_parts.append(f"\u23F1\uFE0F Time: {parsed['total_time']}")
+ summary_parts.append(f"\u23F1\uFE0F {runtime_message('vzdump.time', language, value=parsed['total_time'])}")
if summary_parts:
parts.append(' | '.join(summary_parts))
@@ -334,104 +519,92 @@ def _format_vzdump_body(parsed: Dict[str, Any], is_success: bool) -> str:
return '\n'.join(parts)
-def _format_system_startup(data: Dict[str, Any]) -> Tuple[str, str]:
- """
- Format comprehensive system startup report.
-
- Returns (title, body) tuple for the notification.
- Handles both simple startups (all OK) and those with issues.
- """
+def _format_system_startup(data: Dict[str, Any], language: str = 'en') -> Tuple[str, str]:
+ """Format the comprehensive startup report using runtime catalogs."""
hostname = data.get('hostname', 'unknown')
has_issues = data.get('has_issues', False)
-
- # Build title
if has_issues:
total_issues = (
- data.get('total_failed', 0) +
- len(data.get('services_failed', [])) +
- len(data.get('storage_unavailable', []))
+ data.get('total_failed', 0)
+ + len(data.get('services_failed', []))
+ + len(data.get('storage_unavailable', []))
)
- title = f"{hostname}: System startup - {total_issues} issue(s) detected"
+ title = runtime_message('startup.issuesTitle', language, hostname=hostname, count=total_issues)
else:
- title = f"{hostname}: System startup completed"
-
- # Build body
+ title = runtime_message('startup.completeTitle', language, hostname=hostname)
+
parts = []
-
- # Overall status
if not has_issues:
- parts.append("All systems operational.")
-
- # VMs/CTs started
+ parts.append(runtime_message('startup.operational', language))
+
vms_ok = len(data.get('vms_started', []))
cts_ok = len(data.get('cts_started', []))
if vms_ok or cts_ok:
- count_parts = []
+ counts = []
if vms_ok:
- count_parts.append(f"{vms_ok} VM{'s' if vms_ok > 1 else ''}")
+ key = 'startup.vmCountOne' if vms_ok == 1 else 'startup.vmCountMany'
+ counts.append(runtime_message(key, language, count=vms_ok))
if cts_ok:
- count_parts.append(f"{cts_ok} CT{'s' if cts_ok > 1 else ''}")
-
- # List names (up to 5)
- names = []
- for vm in data.get('vms_started', [])[:3]:
- names.append(f"{vm['name']} ({vm['vmid']})")
- for ct in data.get('cts_started', [])[:3]:
- names.append(f"{ct['name']} ({ct['vmid']})")
-
- line = f"\u2705 {' and '.join(count_parts)} started"
+ key = 'startup.ctCountOne' if cts_ok == 1 else 'startup.ctCountMany'
+ counts.append(runtime_message(key, language, count=cts_ok))
+ names = [
+ f"{item['name']} ({item['vmid']})"
+ for item in (data.get('vms_started', [])[:3] + data.get('cts_started', [])[:3])
+ ]
+ line = runtime_message('startup.started', language, counts=', '.join(counts))
if names:
- if len(names) <= 5:
- line += f": {', '.join(names)}"
- else:
- line += f": {', '.join(names[:5])}..."
+ line += f": {', '.join(names[:5])}"
+ if len(names) > 5:
+ line += '…'
parts.append(line)
-
- # Failed VMs/CTs
+
+ unknown_error = runtime_message('startup.unknownError', language)
for vm in data.get('vms_failed', []):
- reason = vm.get('reason', 'unknown error')
- parts.append(f"\u274C VM failed: {vm['name']} - {reason}")
-
+ parts.append(runtime_message('startup.vmFailed', language, name=vm['name'], reason=vm.get('reason', unknown_error)))
for ct in data.get('cts_failed', []):
- reason = ct.get('reason', 'unknown error')
- parts.append(f"\u274C CT failed: {ct['name']} - {reason}")
-
- # Storage issues
+ parts.append(runtime_message('startup.ctFailed', language, name=ct['name'], reason=ct.get('reason', unknown_error)))
+
storage_unavailable = data.get('storage_unavailable', [])
if storage_unavailable:
- names = [s['name'] for s in storage_unavailable[:3]]
- parts.append(f"\u26A0\uFE0F Storage: {len(storage_unavailable)} unavailable ({', '.join(names)})")
-
- # Service issues
+ parts.append(runtime_message(
+ 'startup.storageUnavailable', language, count=len(storage_unavailable),
+ names=', '.join(item['name'] for item in storage_unavailable[:3]),
+ ))
services_failed = data.get('services_failed', [])
if services_failed:
- names = [s['name'] for s in services_failed[:3]]
- parts.append(f"\u26A0\uFE0F Services: {len(services_failed)} failed ({', '.join(names)})")
-
- # Startup duration
+ parts.append(runtime_message(
+ 'startup.servicesFailed', language, count=len(services_failed),
+ names=', '.join(item['name'] for item in services_failed[:3]),
+ ))
duration = data.get('startup_duration_seconds', 0)
if duration:
- minutes = int(duration // 60)
- parts.append(f"\u23F1\uFE0F Startup completed in {minutes} min")
-
- body = '\n'.join(parts)
- return title, body
+ parts.append(runtime_message('startup.duration', language, minutes=int(duration // 60)))
+ return title, '\n'.join(parts)
-def _format_app_update_available(data: Dict[str, Any]) -> Tuple[str, str]:
+def _format_app_update_available(data: Dict[str, Any],
+ language: str = 'en') -> Tuple[str, str]:
"""Render one app update or a scheduled multi-app summary."""
- hostname = str(data.get("hostname") or _get_hostname())
- updates = data.get("updates")
+ hostname = str(data.get('hostname') or _get_hostname())
+ app_fallback = runtime_message('appUpdates.app', language) or 'app'
+ unknown = runtime_message('appUpdates.unknown', language) or 'unknown'
+ updates = data.get('updates')
if not isinstance(updates, list) or len(updates) < 2:
- app_name = str(data.get("app_name") or "app")
- vmid = data.get("vmid", "")
- ct_name = str(data.get("ct_name") or f"CT-{vmid}")
- installed = str(data.get("installed") or "unknown")
- latest = str(data.get("latest") or "unknown")
+ app_name = str(data.get('app_name') or app_fallback)
+ vmid = data.get('vmid', '')
+ ct_name = str(data.get('ct_name') or f'CT-{vmid}')
+ installed = str(data.get('installed') or unknown)
+ latest = str(data.get('latest') or unknown)
return (
- f"{hostname}: {app_name} update available on CT {vmid}",
- f"{app_name} on CT {vmid} ({ct_name}) has a new version:\n"
- f" {installed} → {latest}",
+ runtime_message(
+ 'appUpdates.singleTitle', language,
+ hostname=hostname, app_name=app_name, vmid=vmid,
+ ),
+ runtime_message(
+ 'appUpdates.singleBody', language,
+ app_name=app_name, vmid=vmid, ct_name=ct_name,
+ installed=installed, latest=latest,
+ ),
)
clean_updates = []
@@ -443,26 +616,31 @@ def _format_app_update_available(data: Dict[str, Any]) -> Tuple[str, str]:
except (TypeError, ValueError):
continue
clean_updates.append({
- "vmid": vmid,
- "app_name": str(item.get("app_name") or "app"),
- "installed": str(item.get("installed") or "unknown"),
- "latest": str(item.get("latest") or "unknown"),
+ 'vmid': vmid,
+ 'app_name': str(item.get('app_name') or app_fallback),
+ 'installed': str(item.get('installed') or unknown),
+ 'latest': str(item.get('latest') or unknown),
})
clean_updates.sort(
key=lambda item: (item["vmid"], item["app_name"].casefold())
)
if not clean_updates:
return (
- f"{hostname}: Application updates available",
- "Application updates are available.",
+ runtime_message('appUpdates.emptyTitle', language, hostname=hostname),
+ runtime_message('appUpdates.emptyBody', language),
)
count = len(clean_updates)
container_count = len({item["vmid"] for item in clean_updates})
- title = f"{hostname}: {count} application updates available"
- lead = (
- f"{count} applications in {container_count} LXC "
- f"container{'s' if container_count != 1 else ''} have a newer version:"
+ title = runtime_message(
+ 'appUpdates.batchTitle', language, hostname=hostname, count=count,
+ )
+ lead_key = (
+ 'appUpdates.batchLeadOneContainer'
+ if container_count == 1 else 'appUpdates.batchLeadManyContainers'
+ )
+ lead = runtime_message(
+ lead_key, language, count=count, container_count=container_count,
)
sections = []
omitted = 0
@@ -481,10 +659,79 @@ def _format_app_update_available(data: Dict[str, Any]) -> Tuple[str, str]:
continue
sections.append("\n".join(section))
if omitted:
- sections.append(f"… {omitted} additional application(s)")
+ sections.append(runtime_message('appUpdates.additional', language, count=omitted))
return title, "\n\n".join([lead, *sections])
+_CPU_SUSTAINED_REASON = re.compile(
+ r'^CPU >(?P [0-9.]+)% sustained for (?P\d+)s$'
+)
+
+
+def _format_health_degraded(data: Dict[str, Any],
+ language: str = 'en') -> Tuple[str, str]:
+ """Render Monitor-generated health degradations in the chosen locale."""
+ payload = data.get('health_degraded')
+ if not isinstance(payload, dict):
+ return str(data.get('title') or ''), str(data.get('reason') or '')
+
+ categories = payload.get('categories')
+ if not isinstance(categories, list) or not categories:
+ return str(data.get('title') or ''), str(data.get('reason') or '')
+
+ def category_label(item: Dict[str, Any]) -> str:
+ category = str(item.get('key') or '')
+ return (
+ runtime_message(f'healthDegraded.categories.{category}', language)
+ or str(item.get('category') or category)
+ )
+
+ def severity_label(item: Dict[str, Any]) -> str:
+ severity = str(item.get('status') or data.get('severity') or 'WARNING').lower()
+ return (
+ runtime_message(f'healthDegraded.severity.{severity}', language)
+ or str(item.get('status') or data.get('severity') or 'WARNING')
+ )
+
+ def localized_reason(item: Dict[str, Any]) -> str:
+ reason = str(item.get('reason') or '')
+ if str(item.get('key') or '') == 'cpu':
+ match = _CPU_SUSTAINED_REASON.fullmatch(reason)
+ if match:
+ return runtime_message(
+ 'healthDegraded.reasons.cpuSustained', language,
+ threshold=match.group('threshold'), duration=match.group('duration'),
+ ) or reason
+ return reason
+
+ hostname = str(data.get('hostname') or _get_hostname())
+ if len(categories) == 1:
+ item = categories[0] if isinstance(categories[0], dict) else {}
+ title = runtime_message(
+ 'healthDegraded.singleTitle', language,
+ hostname=hostname, severity=severity_label(item), category=category_label(item),
+ )
+ entity = str(item.get('entity') or '').strip()
+ if entity:
+ title = f'{title} — {entity}'
+ return title, localized_reason(item)
+
+ title = runtime_message(
+ 'healthDegraded.multipleTitle', language,
+ hostname=hostname, count=len(categories),
+ )
+ lines = []
+ for item in categories:
+ if not isinstance(item, dict):
+ continue
+ lines.append(runtime_message(
+ 'healthDegraded.multipleLine', language,
+ severity=severity_label(item), category=category_label(item),
+ reason=localized_reason(item),
+ ))
+ return title, '\n'.join(line for line in lines if line)
+
+
# ─── Severity Icons ──────────────────────────────────────────────
SEVERITY_ICONS = {
@@ -557,6 +804,7 @@ TEMPLATES = {
'label': 'Health check degraded',
'group': 'health',
'default_enabled': True,
+ 'formatter': '_format_health_degraded',
},
# ── VM / CT events ──
@@ -1522,6 +1770,8 @@ EVENT_GROUPS = {
'services': {'label': 'Services', 'description': 'System services, shutdown, reboot'},
'health': {'label': 'Health Monitor', 'description': 'Health checks, degradation, recovery'},
'updates': {'label': 'Updates', 'description': 'System and PVE updates'},
+ 'hardware': {'label': 'Hardware', 'description': 'GPU, PCIe and hardware events'},
+ 'system': {'label': 'System', 'description': 'System and internal service events'},
'other': {'label': 'Other', 'description': 'Uncategorized notifications'},
}
@@ -1568,7 +1818,8 @@ def _format_bytes_human(n: Any) -> str:
return f'{size:.1f} {units[i]}'
-def render_template(event_type: str, data: Dict[str, Any]) -> Dict[str, Any]:
+def render_template(event_type: str, data: Dict[str, Any],
+ language: str = 'en') -> Dict[str, Any]:
"""Render a template into a structured notification object.
Returns structured output usable by all channels:
@@ -1576,19 +1827,35 @@ def render_template(event_type: str, data: Dict[str, Any]) -> Dict[str, Any]:
"""
import html as html_mod
- template = TEMPLATES.get(event_type)
- if not template:
+ source_template = TEMPLATES.get(event_type)
+ if not source_template:
# Catch-all: unknown event types always get delivered (group 'other')
# so no Proxmox notification is ever silently dropped.
fallback_body = data.get('message', data.get('reason', str(data)))
severity = data.get('severity', 'INFO')
return {
- 'title': f"{_get_hostname()}: {event_type}",
+ 'title': runtime_message(
+ 'fallback.unknownTitle', language,
+ hostname=_get_hostname(), event_type=event_type,
+ ),
'body': fallback_body, 'body_text': fallback_body,
'body_html': f'{html_mod.escape(str(fallback_body))} ',
'fields': [], 'tags': [severity, 'other', event_type],
'severity': severity, 'group': 'other',
}
+
+ template = dict(source_template)
+ requested_language = (language or 'en').split('-', 1)[0].lower()
+ requested_catalog = _load_runtime_catalog(requested_language)
+ english_catalog = _load_runtime_catalog('en')
+ for field in ('title', 'body', 'label'):
+ key = f'templates.{event_type}.{field}'
+ localized = (
+ _catalog_value(requested_catalog, key)
+ or _catalog_value(english_catalog, key)
+ )
+ if localized:
+ template[field] = localized
# Ensure hostname is always available
variables = {
@@ -1610,7 +1877,7 @@ def render_template(event_type: str, data: Dict[str, Any]) -> Dict[str, Any]:
'packages': '', 'pve_packages': '', 'version': '',
'issue_list': '', 'error_key': '',
'storage_name': '', 'storage_type': '',
- 'important_list': 'none',
+ 'important_list': runtime_message('fallback.none', language),
# Host Backup specifics (run_scheduled_backup.sh + backup_host.sh).
'job_id': '', 'backend': '', 'backend_label': '',
'destination': '', 'profile_mode': '',
@@ -1619,6 +1886,13 @@ def render_template(event_type: str, data: Dict[str, Any]) -> Dict[str, Any]:
}
variables.update(data)
+ if event_type == 'lxc_update_applied' and isinstance(data.get('lxc_update'), dict):
+ status = str(data['lxc_update'].get('status') or '')
+ localized_status = runtime_message(f'lxcUpdate.status.{status}', language)
+ if localized_status:
+ variables['result'] = localized_status
+ variables['details'] = _format_lxc_update_details(data, language)
+
# Humanise raw-byte fields so templates can render '35.3 GiB' instead
# of '37952020480'. Producers keep emitting raw ints (needed by APIs
# and the dashboard); the humanised twin is derived on the fly here.
@@ -1626,9 +1900,10 @@ def render_template(event_type: str, data: Dict[str, Any]) -> Dict[str, Any]:
if _byte_key in data:
variables[f'{_byte_key}_human'] = _format_bytes_human(data[_byte_key])
- # Ensure important_list is never blank (fallback to 'none')
- if not variables.get('important_list', '').strip():
- variables['important_list'] = 'none'
+ # Ensure important_list is never blank (fallback to localized "none")
+ important_list = str(variables.get('important_list', '')).strip()
+ if not important_list or important_list.casefold() == 'none':
+ variables['important_list'] = runtime_message('fallback.none', language)
# Derive the affected object's display name for titles that use it.
# Priority: caller-supplied `entity` (health_monitor.emit_event) →
@@ -1657,7 +1932,8 @@ def render_template(event_type: str, data: Dict[str, Any]) -> Dict[str, Any]:
_caller_title = str(variables.get('title', '')).strip()
if not _caller_title:
hn = variables.get('hostname', '')
- _caller_title = f'{hn}: Health check degraded' if hn else 'Health check degraded'
+ degraded = runtime_message('fallback.healthCheckDegraded', language)
+ _caller_title = f'{hn}: {degraded}' if hn else degraded
variables['title_or_default'] = _caller_title
# `format_map` with a SafeDict avoids the KeyError → "show raw template
@@ -1685,7 +1961,7 @@ def render_template(event_type: str, data: Dict[str, Any]) -> Dict[str, Any]:
if formatter_name and formatter_name in globals():
formatter_func = globals()[formatter_name]
try:
- title, body_text = formatter_func(data)
+ title, body_text = formatter_func(data, language=language)
except Exception:
# Fallback to standard formatting if formatter fails
try:
@@ -1696,9 +1972,10 @@ def render_template(event_type: str, data: Dict[str, Any]) -> Dict[str, Any]:
parsed = _parse_vzdump_message(pve_message)
if parsed:
is_success = (event_type == 'backup_complete')
- body_text = _format_vzdump_body(parsed, is_success)
- # Use PVE's own title if available (contains hostname and status)
- if pve_title:
+ body_text = _format_vzdump_body(parsed, is_success, language=language)
+ # Preserve PVE's source title for English, but never leak it into a
+ # deterministic localized notification.
+ if pve_title and requested_language == 'en':
title = pve_title
else:
# Couldn't parse -- use PVE raw message as body
@@ -1722,15 +1999,15 @@ def render_template(event_type: str, data: Dict[str, Any]) -> Dict[str, Any]:
# Build structured fields for Discord embeds / rich notifications
fields = []
field_map = [
- ('vmid', 'VM/CT'), ('vmname', 'Name'), ('device', 'Device'),
- ('source_ip', 'Source IP'), ('node_name', 'Node'), ('category', 'Category'),
- ('service_name', 'Service'), ('jail', 'Jail'), ('username', 'User'),
- ('count', 'Count'), ('window', 'Window'), ('entity_list', 'Affected'),
+ ('vmid', 'fields.vmid'), ('vmname', 'fields.name'), ('device', 'fields.device'),
+ ('source_ip', 'fields.sourceIp'), ('node_name', 'fields.node'), ('category', 'fields.category'),
+ ('service_name', 'fields.service'), ('jail', 'fields.jail'), ('username', 'fields.user'),
+ ('count', 'fields.count'), ('window', 'fields.window'), ('entity_list', 'fields.affected'),
]
- for key, label in field_map:
+ for key, label_key in field_map:
val = variables.get(key, '')
if val:
- fields.append((label, str(val)))
+ fields.append((runtime_message(label_key, language), str(val)))
# Build HTML body with escaped content
body_html_parts = []
@@ -1919,6 +2196,7 @@ FIELD_EMOJI = {
'hostname': '\U0001F4BB', # laptop
'vmid': '\U0001F194', # ID button
'vmname': '\U0001F3F7\uFE0F', # label
+ 'ct_name': '\U0001F4E6', # package / container
'device': '\U0001F4BD', # disk
'mount': '\U0001F4C2', # open folder
'source_ip': '\U0001F310', # globe
@@ -1943,12 +2221,89 @@ FIELD_EMOJI = {
'kernel_count': '\u2699\uFE0F',
'important_list': '\U0001F4CB', # clipboard
'current_version': '\U0001F4E6', # package \u2014 installed version
+ 'new_version': '\U0001F195', # NEW button \u2014 offered version
'latest_version': '\U0001F195', # NEW button \u2014 upstream version
'kernel': '\u2699\uFE0F', # gear \u2014 running kernel
'menu_label': '\U0001F4D6', # open book \u2014 menu navigation hint
}
+_TEMPLATE_FIELD_RE = re.compile(r'\{([a-zA-Z_][a-zA-Z0-9_]*)[^}]*\}')
+
+
+def _localized_template_labels(event_type: str, language: str) -> Dict[str, List[str]]:
+ """Return the visible labels paired with template fields in one locale.
+
+ The old emoji pass compared rendered English words such as ``Duration``
+ and ``Total updates``. That necessarily stops matching after a template
+ is translated. The template itself still knows which field each label
+ describes, so derive the visible wording from that localized template.
+ """
+ requested = (language or 'en').split('-', 1)[0].lower()
+ key = f'templates.{event_type}.body'
+ # Use the raw catalog entry rather than runtime_message(): the latter
+ # deliberately formats unknown placeholders away, while this helper needs
+ # to inspect those placeholders to associate each label with its field.
+ body = (
+ _catalog_value(_load_runtime_catalog(requested), key)
+ or _catalog_value(_load_runtime_catalog('en'), key)
+ )
+ if not body:
+ body = TEMPLATES.get(event_type, {}).get('body', '')
+
+ labels: Dict[str, List[str]] = {}
+ lines = body.splitlines()
+ for index, line in enumerate(lines):
+ matches = list(_TEMPLATE_FIELD_RE.finditer(line))
+ for match in matches:
+ label = line[:match.start()].strip().rstrip(':').strip()
+ if label:
+ labels.setdefault(match.group(1), []).append(label)
+
+ # A heading on its own line (for example "Important packages:")
+ # labels the variable rendered on the following line.
+ if line.strip().endswith(':') and index + 1 < len(lines):
+ next_matches = list(_TEMPLATE_FIELD_RE.finditer(lines[index + 1]))
+ if len(next_matches) == 1:
+ label = line.strip().rstrip(':').strip()
+ if label:
+ labels.setdefault(next_matches[0].group(1), []).append(label)
+ return labels
+
+
+def _lxc_update_label_icons(language: str) -> Dict[str, str]:
+ """Return localized LXC-update detail prefixes with stable icons."""
+ values = {
+ 'before': '0', 'after': '0', 'items': 'item', 'targets': 'target',
+ 'reason': 'reason', 'value': 'value', 'error': 'error', 'duration': '0s',
+ }
+ definitions = (
+ ('lxcUpdate.sourceLabel', '🧭'),
+ ('lxcUpdate.targets', '🎯'),
+ ('lxcUpdate.osPending', '📦'),
+ ('lxcUpdate.osUnverified', '📦'),
+ ('lxcUpdate.applications', '🧩'),
+ ('lxcUpdate.applicationsUnverified', '🧩'),
+ ('lxcUpdate.dockerEngineChange', '🐳'),
+ ('lxcUpdate.dockerEngineVerified', '🐳'),
+ ('lxcUpdate.dockerEngineUnverified', '🐳'),
+ ('lxcUpdate.dockerImagesPending', '🐳'),
+ ('lxcUpdate.dockerImagesChanged', '🐳'),
+ ('lxcUpdate.deferredTargets', '⏳'),
+ ('lxcUpdate.reason', '📝'),
+ ('lxcUpdate.restartRequired', '🔄'),
+ ('lxcUpdate.verificationPending', '⏳'),
+ ('lxcUpdate.verificationWarning', '⚠️'),
+ ('lxcUpdate.duration', '⏱️'),
+ )
+ result = {}
+ for key, icon in definitions:
+ rendered = runtime_message(key, language, **values).strip()
+ if rendered:
+ result[rendered.split(':', 1)[0].strip()] = icon
+ return result
+
+
def enrich_with_emojis(event_type: str, title: str, body: str,
data: Dict[str, Any]) -> tuple:
"""Replace the plain title/body with emoji-enriched versions.
@@ -2009,6 +2364,13 @@ def enrich_with_emojis(event_type: str, title: str, body: str,
preprocessed = re.sub(r'^\n+', '', preprocessed)
preprocessed = preprocessed.strip()
+ language = str(data.get('_notification_language') or 'en')
+ localized_labels = _localized_template_labels(event_type, language)
+ lxc_update_labels = (
+ _lxc_update_label_icons(language)
+ if event_type == 'lxc_update_applied' else {}
+ )
+
# ── Extended emoji mappings for health/disk messages ──
HEALTH_EMOJI_MAP = {
# Disk patterns
@@ -2033,12 +2395,41 @@ def enrich_with_emojis(event_type: str, title: str, body: str,
# Build enriched body: prepend field emojis to recognizable lines
lines = preprocessed.split('\n')
enriched_lines = []
+ app_update_is_single = (
+ not isinstance(data.get('updates'), list) or len(data['updates']) < 2
+ )
+ app_update_lead_added = False
+ app_update_version = None
+ if event_type == 'app_update_available' and app_update_is_single:
+ app_update_version = f"{data.get('installed', '')} → {data.get('latest', '')}".strip()
for line in lines:
stripped = line.strip()
if not stripped:
enriched_lines.append(line)
continue
+
+ # App-update notifications deliberately use a compact, prose-like
+ # template rather than field labels. Keep their two structured lines
+ # as readable as other update notifications without depending on a
+ # translated phrase to recognize them.
+ if event_type == 'app_update_available' and app_update_is_single:
+ if not app_update_lead_added:
+ enriched_lines.append(f'📦 {stripped}')
+ app_update_lead_added = True
+ continue
+ if app_update_version and stripped == app_update_version:
+ enriched_lines.append(f'🔄 {stripped}')
+ continue
+
+ # The Proxmox VE detector emits the manager version as a concise,
+ # structured technical detail outside the localized template fields.
+ # Mark only that known detail; arbitrary detector text remains intact.
+ if event_type == 'pve_update' and re.match(
+ r'^pve-manager\s+\S+\s+(?:→|->)\s+\S+$', stripped,
+ ):
+ enriched_lines.append(f'🔧 {stripped}')
+ continue
# First, check health-specific patterns
health_enriched = False
@@ -2054,6 +2445,30 @@ def enrich_with_emojis(event_type: str, title: str, body: str,
if health_enriched:
continue
+
+ # LXC update outcomes are assembled from structured details rather
+ # than one static template line. Their translated labels come from
+ # the runtime catalog, so keep the mapping semantic rather than
+ # comparing an English translation.
+ matched_lxc_label = next(
+ (label for label in lxc_update_labels
+ if stripped.lower().startswith(label.lower())),
+ None,
+ )
+ if matched_lxc_label:
+ icon = lxc_update_labels[matched_lxc_label]
+ if not stripped.startswith(icon):
+ enriched_lines.append(f'{icon} {stripped}')
+ else:
+ enriched_lines.append(stripped)
+ continue
+
+ # Docker's engine inventory is generated by the detector, not the
+ # translated template. "Docker Engine" is its product name and stays
+ # stable across locales, so it is safe to decorate directly.
+ if event_type == 'docker_stack_update_available' and stripped.startswith('• Docker Engine:'):
+ enriched_lines.append(f'🐳 {stripped}')
+ continue
# Try to match "FieldName: value" patterns
enriched = False
@@ -2078,6 +2493,7 @@ def enrich_with_emojis(event_type: str, title: str, body: str,
}
if field_key in _LABEL_MAP:
label_variants.append(_LABEL_MAP[field_key])
+ label_variants.extend(localized_labels.get(field_key, []))
for label in label_variants:
if stripped.lower().startswith(label.lower() + ':'):
diff --git a/AppImage/scripts/tests/test_lxc_app_notification_batch.py b/AppImage/scripts/tests/test_lxc_app_notification_batch.py
index 48479eb3..daced8e3 100644
--- a/AppImage/scripts/tests/test_lxc_app_notification_batch.py
+++ b/AppImage/scripts/tests/test_lxc_app_notification_batch.py
@@ -135,6 +135,31 @@ class AppUpdateNotificationBatchTests(unittest.TestCase):
self.assertIn("• Docmost: 0.2 → 0.9", rendered["body"])
self.assertIn("• Redis: 7.0 → 8.1", rendered["body"])
+ def test_batch_formatter_uses_slovak_runtime_messages(self):
+ data = {
+ "hostname": "HomeLAB_2",
+ "updates": [
+ {"vmid": 115, "app_name": "Redis", "installed": "7.0", "latest": "8.1"},
+ {"vmid": 100, "app_name": "AdGuard Home", "installed": "1.0", "latest": "1.1"},
+ {"vmid": 115, "app_name": "Docmost", "installed": "0.2", "latest": "0.9"},
+ ],
+ }
+
+ rendered = notification_templates.render_template(
+ "app_update_available", data, language="sk",
+ )
+
+ self.assertEqual(
+ rendered["title"],
+ "HomeLAB_2: Dostupné aktualizácie aplikácií: 3",
+ )
+ self.assertIn(
+ "Aplikácie s dostupnou novšou verziou: 3 v 2 kontajneroch LXC:",
+ rendered["body"],
+ )
+ self.assertIn("• AdGuard Home: 1.0 → 1.1", rendered["body"])
+ self.assertNotIn("application updates available", rendered["title"])
+
if __name__ == "__main__":
unittest.main()
diff --git a/AppImage/scripts/tests/test_notification_runtime_i18n.py b/AppImage/scripts/tests/test_notification_runtime_i18n.py
new file mode 100644
index 00000000..3e1b2193
--- /dev/null
+++ b/AppImage/scripts/tests/test_notification_runtime_i18n.py
@@ -0,0 +1,807 @@
+import json
+import re
+import sqlite3
+import string
+import sys
+import tempfile
+import unittest
+from datetime import datetime
+from pathlib import Path
+from unittest import mock
+
+
+SCRIPTS_DIR = Path(__file__).resolve().parents[1]
+APPIMAGE_DIR = SCRIPTS_DIR.parent
+if str(SCRIPTS_DIR) not in sys.path:
+ sys.path.insert(0, str(SCRIPTS_DIR))
+
+import notification_manager
+import notification_templates
+import notification_channels
+
+
+def _placeholders(value):
+ return {
+ field_name
+ for _literal, field_name, _format_spec, _conversion
+ in string.Formatter().parse(value)
+ if field_name
+ }
+
+
+class RuntimeCatalogTests(unittest.TestCase):
+ RUNTIME_LANGUAGES = ("en", "de", "es", "fr", "it", "pt", "sk", "sv")
+
+ @classmethod
+ def setUpClass(cls):
+ cls.catalogs = {}
+ for language in cls.RUNTIME_LANGUAGES:
+ path = APPIMAGE_DIR / "messages" / language / "common.json"
+ cls.catalogs[language] = json.loads(path.read_text(encoding="utf-8"))["runtime"]["notifications"]
+
+ def test_runtime_catalog_covers_every_template_dynamically(self):
+ expected = set(notification_templates.TEMPLATES)
+ for language, catalog in self.catalogs.items():
+ templates = catalog["templates"]
+ self.assertEqual(set(templates), expected, language)
+ for event_type, source in notification_templates.TEMPLATES.items():
+ self.assertEqual(set(templates[event_type]), {"title", "body", "label"})
+ for field in ("title", "body", "label"):
+ self.assertIsInstance(templates[event_type][field], str)
+ self.assertTrue(templates[event_type][field])
+ if field in source:
+ self.assertEqual(
+ _placeholders(templates[event_type][field]),
+ _placeholders(source[field]),
+ f"{language}:{event_type}:{field}",
+ )
+
+ def test_runtime_catalog_keys_and_placeholders_match(self):
+ def flatten(value, prefix=""):
+ result = {}
+ for key, child in value.items():
+ dotted = f"{prefix}.{key}" if prefix else key
+ if isinstance(child, dict):
+ result.update(flatten(child, dotted))
+ else:
+ result[dotted] = child
+ return result
+
+ en = flatten(self.catalogs["en"])
+ for language, catalog in self.catalogs.items():
+ translated = flatten(catalog)
+ self.assertEqual(set(translated), set(en), language)
+ for key in en:
+ self.assertEqual(_placeholders(translated[key]), _placeholders(en[key]), f"{language}:{key}")
+
+ def test_notification_language_ui_keys_exist_in_both_catalogs(self):
+ required = {
+ "notificationLanguage",
+ "selectNotificationLanguage",
+ "notificationLanguageHint",
+ }
+ for language in self.RUNTIME_LANGUAGES:
+ path = APPIMAGE_DIR / "messages" / language / "common.json"
+ common = json.loads(path.read_text(encoding="utf-8"))
+ ui = common["settings"]["notifications"]["ui"]
+ self.assertTrue(required.issubset(ui), language)
+ for key in required:
+ self.assertIsInstance(ui[key], str)
+ self.assertTrue(ui[key].strip(), f"{language}:{key}")
+
+ def test_slovak_catalog_preserves_placeholders_and_translates_static_text(self):
+ en = self.catalogs["en"]["templates"]
+ sk = self.catalogs["sk"]["templates"]
+ for event_type, source in notification_templates.TEMPLATES.items():
+ for field in ("title", "body", "label"):
+ static_text = source.get(field, "")
+ for placeholder in _placeholders(static_text):
+ static_text = static_text.replace("{" + placeholder + "}", "")
+ if any(ch.isalpha() for ch in static_text):
+ self.assertNotEqual(sk[event_type][field], en[event_type][field], f"{event_type}:{field}")
+
+ def test_every_template_renders_in_slovak_and_preserves_dynamic_values(self):
+ values = {
+ name: f"DYNAMIC_{name.upper()}"
+ for template in notification_templates.TEMPLATES.values()
+ for field in ("title", "body")
+ for name in _placeholders(template.get(field, ""))
+ }
+ 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")
+ combined = rendered["title"] + "\n" + rendered["body"]
+ if notification_templates.TEMPLATES[event_type].get("formatter"):
+ continue
+ for name in _placeholders(
+ notification_templates.TEMPLATES[event_type]["title"]
+ + 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}")
+
+ def test_lxc_update_result_and_details_render_in_slovak(self):
+ data = {
+ "hostname": "homelab",
+ "ct_name": "iventoy",
+ "vmid": "105",
+ "result": "succeeded",
+ "details": "Source: Manual",
+ "lxc_update": {
+ "status": "success",
+ "source": "manual",
+ "targets": ["app:iventoy"],
+ "labels": ["iVentoy"],
+ "duration": "16s",
+ "before": {
+ "apps": {"iventoy": {"name": "iVentoy", "installed_version": "1.0.42"}},
+ },
+ "after": {
+ "apps": {"iventoy": {"name": "iVentoy", "installed_version": "1.0.43"}},
+ },
+ "verification_pending": False,
+ "verification_errors": [],
+ "reboot_required": False,
+ },
+ }
+ rendered = notification_templates.render_template("lxc_update_applied", data, language="sk")
+ self.assertEqual(rendered["title"], "homelab: LXC iventoy (105) aktualizácia úspešne dokončená")
+ self.assertIn("Zdroj: Manuálne", rendered["body"])
+ self.assertIn("Ciele: iVentoy", rendered["body"])
+ self.assertIn("Aplikácie: iVentoy: 1.0.42 → 1.0.43", rendered["body"])
+ self.assertIn("Vyžaduje sa reštart: nie", rendered["body"])
+ self.assertIn("Trvanie: 16s", rendered["body"])
+ self.assertNotIn("Source:", rendered["body"])
+ self.assertNotIn("succeeded", rendered["title"])
+
+ _title, enriched_body = notification_templates.enrich_with_emojis(
+ "lxc_update_applied", rendered["title"], rendered["body"],
+ {**data, "_notification_language": "sk", "severity": "INFO"},
+ )
+ self.assertIn("🧭 Zdroj: Manuálne", enriched_body)
+ self.assertIn("🎯 Ciele: iVentoy", enriched_body)
+ self.assertIn("🧩 Aplikácie: iVentoy: 1.0.42 → 1.0.43", enriched_body)
+ self.assertIn("🔄 Vyžaduje sa reštart: nie", enriched_body)
+ self.assertIn("⏱️ Trvanie: 16s", enriched_body)
+
+ for language in self.RUNTIME_LANGUAGES:
+ rendered_locale = notification_templates.render_template(
+ "lxc_update_applied", data, language=language,
+ )
+ _title, body_locale = notification_templates.enrich_with_emojis(
+ "lxc_update_applied", rendered_locale["title"], rendered_locale["body"],
+ {**data, "_notification_language": language, "severity": "INFO"},
+ )
+ self.assertIn("🧭", body_locale, language)
+ self.assertIn("⏱️", body_locale, language)
+
+ def test_update_summary_body_icons_follow_the_selected_language(self):
+ data = {
+ "hostname": "pve01", "total_count": "2", "security_count": "0",
+ "pve_count": "1", "kernel_count": "0", "important_list": "none",
+ "severity": "INFO", "_notification_language": "sk",
+ }
+ rendered = notification_templates.render_template("update_summary", data, language="sk")
+ _title, enriched_body = notification_templates.enrich_with_emojis(
+ "update_summary", rendered["title"], rendered["body"], data,
+ )
+ self.assertIn("📦 Aktualizácie spolu: 2", enriched_body)
+ self.assertIn("🛡️ Bezpečnostné aktualizácie: 0", enriched_body)
+ self.assertIn("⚙️ Aktualizácie jadra: 0", enriched_body)
+ self.assertIn("📋 Dôležité balíky:", enriched_body)
+ self.assertIn("žiadne", enriched_body)
+ self.assertNotIn("\nnone", enriched_body)
+
+ for language in self.RUNTIME_LANGUAGES:
+ locale_data = {**data, "_notification_language": language}
+ rendered_locale = notification_templates.render_template(
+ "update_summary", locale_data, language=language,
+ )
+ _title, body_locale = notification_templates.enrich_with_emojis(
+ "update_summary", rendered_locale["title"], rendered_locale["body"], locale_data,
+ )
+ total_label = notification_templates._localized_template_labels(
+ "update_summary", language,
+ )["total_count"][0]
+ self.assertIn(f"📦 {total_label}: 2", body_locale, language)
+
+ def test_docker_update_body_icons_preserve_localized_container_label(self):
+ data = {
+ "hostname": "pve01", "vmid": "210", "ct_name": "repopulse-labs-test",
+ "count": "1", "details": "• Docker Engine: 29.8.0 → 29.8.1",
+ "severity": "INFO", "_notification_language": "sk",
+ }
+ rendered = notification_templates.render_template(
+ "docker_stack_update_available", data, language="sk",
+ )
+ _title, enriched_body = notification_templates.enrich_with_emojis(
+ "docker_stack_update_available", rendered["title"], rendered["body"], data,
+ )
+ self.assertIn("📦 Kontajner repopulse-labs-test (CT 210) má 1 aktualizácií Docker:", enriched_body)
+ self.assertIn("🐳 • Docker Engine: 29.8.0 → 29.8.1", enriched_body)
+
+ def test_app_and_proxmox_update_body_icons_cover_versions(self):
+ app_data = {
+ "hostname": "HomeLAB_1", "app_name": "Uptime Kuma", "vmid": "108",
+ "ct_name": "uptime-kuma", "installed": "2.5.4", "latest": "2.5.5",
+ "severity": "INFO", "_notification_language": "sk",
+ }
+ app = notification_templates.render_template(
+ "app_update_available", app_data, language="sk",
+ )
+ _title, app_body = notification_templates.enrich_with_emojis(
+ "app_update_available", app["title"], app["body"], app_data,
+ )
+ self.assertIn("📦 Aplikácia Uptime Kuma na CT 108 (uptime-kuma) má novú verziu:", app_body)
+ self.assertIn("🔄 2.5.4 → 2.5.5", app_body)
+
+ pve_data = {
+ "hostname": "HomeLAB_1", "current_version": "9.2.18",
+ "new_version": "9.2.20", "details": "pve-manager 9.2.18 → 9.2.20",
+ "severity": "INFO", "_notification_language": "sk",
+ }
+ pve = notification_templates.render_template("pve_update", pve_data, language="sk")
+ _title, pve_body = notification_templates.enrich_with_emojis(
+ "pve_update", pve["title"], pve["body"], pve_data,
+ )
+ self.assertIn("📦 Aktuálna: 9.2.18", pve_body)
+ self.assertIn("🆕 Nová: 9.2.20", pve_body)
+ self.assertIn("🔧 pve-manager 9.2.18 → 9.2.20", pve_body)
+
+ for language in self.RUNTIME_LANGUAGES:
+ locale_data = {**pve_data, "_notification_language": language}
+ locale = notification_templates.render_template(
+ "pve_update", locale_data, language=language,
+ )
+ _title, locale_body = notification_templates.enrich_with_emojis(
+ "pve_update", locale["title"], locale["body"], locale_data,
+ )
+ label = notification_templates._localized_template_labels(
+ "pve_update", language,
+ )["new_version"][0]
+ self.assertIn(f"🆕 {label}: 9.2.20", locale_body, language)
+
+ def test_missing_slovak_key_falls_back_to_english(self):
+ with tempfile.TemporaryDirectory() as directory:
+ root = Path(directory)
+ (root / "en").mkdir()
+ (root / "sk").mkdir()
+ (root / "en" / "common.json").write_text(
+ json.dumps({"runtime": {"notifications": {"fallback": {"unknownTitle": "{hostname}: {event_type}"}}}}),
+ encoding="utf-8",
+ )
+ (root / "sk" / "common.json").write_text(
+ json.dumps({"runtime": {"notifications": {}}}), encoding="utf-8"
+ )
+ with mock.patch.object(notification_templates, "RUNTIME_CATALOG_DIR", root):
+ notification_templates._load_runtime_catalog.cache_clear()
+ self.assertEqual(
+ notification_templates.runtime_message(
+ "fallback.unknownTitle", "sk", hostname="pve01", event_type="vendor_event"
+ ),
+ "pve01: vendor_event",
+ )
+ notification_templates._load_runtime_catalog.cache_clear()
+
+ def test_special_formatters_digest_and_test_message_are_slovak(self):
+ startup = notification_templates.render_template(
+ "system_startup",
+ {"hostname": "pve01", "has_issues": False, "vms_started": [{"name": "alpha", "vmid": 100}]},
+ language="sk",
+ )
+ self.assertIn("Spustenie systému", startup["title"])
+ self.assertIn("Všetky systémy sú funkčné", startup["body"])
+ self.assertIn("alpha", startup["body"])
+
+ app = notification_templates.render_template(
+ "app_update_available",
+ {"hostname": "pve01", "app_name": "Redis", "vmid": 115, "ct_name": "cache", "installed": "7.0", "latest": "8.1"},
+ language="sk",
+ )
+ self.assertIn("dostupná aktualizácia", app["title"])
+ self.assertIn("Redis", app["body"])
+ self.assertIn("7.0 → 8.1", app["body"])
+
+ backup = notification_templates.render_template(
+ "backup_complete",
+ {
+ "hostname": "pve01", "storage": "pbs-main", "vmname": "alpha", "vmid": "100",
+ "pve_title": "Backup job finished",
+ "pve_message": (
+ "INFO: Starting Backup of VM 100 (qemu)\n"
+ "INFO: VM Name: alpha\n"
+ "INFO: transferred 1.5 GiB in 10 seconds\n"
+ "INFO: Finished Backup of VM 100 (00:00:10)"
+ ),
+ },
+ language="sk",
+ )
+ self.assertIn("Záloha dokončená", backup["title"])
+ self.assertNotIn("Backup job finished", backup["title"])
+ self.assertIn("Veľkosť: 1.5 GiB", backup["body"])
+ self.assertIn("Trvanie: 00:00:10", backup["body"])
+
+ manager = notification_manager.NotificationManager()
+ manager._config = {"notification_language": "sk"}
+ rows = [(1, "cpu_high", "resources", 0, "pve01: Vysoké využitie CPU", "body")]
+ digest = manager._compose_digest_body(rows)
+ self.assertIn("udalostí INFO zoskupených podľa kategórie", digest)
+ self.assertIn("Zdroje", digest)
+ rich_digest = manager._compose_digest_body(rows, use_icons=True)
+ self.assertIn("📊 Zdroje: 1", rich_digest)
+ self.assertIn("• 🔥 01:00 Vysoké využitie CPU", rich_digest)
+ title, body, caption = manager._build_test_message(False, False, "groq / sk")
+ self.assertEqual(title, "Test ProxMenux")
+ self.assertIn("Vitajte", body)
+ self.assertIn("profilovú fotografiu", caption)
+
+ def test_monitor_generated_cpu_health_degradation_is_slovak(self):
+ data = {
+ "hostname": "HomeLAB_2",
+ "severity": "CRITICAL",
+ "title": "HomeLAB_2: Health CRITICAL - CPU Usage & Temperature",
+ "reason": "CPU >95.0% sustained for 296s",
+ "health_degraded": {
+ "categories": [{
+ "key": "cpu",
+ "status": "CRITICAL",
+ "reason": "CPU >95.0% sustained for 296s",
+ "entity": "",
+ }],
+ },
+ }
+ rendered = notification_templates.render_template(
+ "health_degraded", data, language="sk",
+ )
+ self.assertEqual(
+ rendered["title"],
+ "HomeLAB_2: Kritický stav – Využitie CPU a teplota",
+ )
+ self.assertEqual(rendered["body"], "CPU je nad 95.0 % už 296 s.")
+ self.assertNotIn("Health CRITICAL", rendered["title"])
+ self.assertNotIn("sustained", rendered["body"])
+
+ enriched_title, _body = notification_templates.enrich_with_emojis(
+ "health_degraded", rendered["title"], rendered["body"],
+ {**data, "_notification_language": "sk"},
+ )
+ self.assertTrue(enriched_title.startswith("⚠️ "))
+
+ def test_batch_app_updates_render_from_every_runtime_catalog(self):
+ data = {
+ "hostname": "HOST-ŽILINA",
+ "updates": [
+ {"vmid": 100, "app_name": "AdGuard Home", "installed": "1.0", "latest": "1.1"},
+ {"vmid": 115, "app_name": "Redis", "installed": "7.0", "latest": "8.1"},
+ ],
+ }
+ for language in self.RUNTIME_LANGUAGES:
+ rendered = notification_templates.render_template(
+ "app_update_available", data, language=language,
+ )
+ self.assertEqual(
+ rendered["title"],
+ notification_templates.runtime_message(
+ "appUpdates.batchTitle", language,
+ hostname="HOST-ŽILINA", count=2,
+ ),
+ language,
+ )
+ self.assertIn("CT 100", rendered["body"], language)
+ self.assertIn("• Redis: 7.0 → 8.1", rendered["body"], language)
+
+ def test_ai_disabled_telegram_receives_deterministic_slovak(self):
+ class RecordingTelegram:
+ def __init__(self):
+ self.payload = None
+
+ def send(self, title, body, severity, data=None):
+ self.payload = (title, body, severity, data)
+ return {"success": True, "error": ""}
+
+ channel = RecordingTelegram()
+ manager = notification_manager.NotificationManager()
+ manager._channels = {"telegram": channel}
+ manager._config = {
+ "notification_language": "sk",
+ "ai_enabled": "false",
+ "telegram.rich_format": "false",
+ }
+ with mock.patch.object(manager, "_record_history"):
+ result = manager.send_notification(
+ "vm_start", "INFO", "", "",
+ data={"hostname": "pve01", "vmname": "účtovníctvo", "vmid": "123"},
+ skip_toggle_check=True,
+ )
+ self.assertTrue(result["success"])
+ self.assertIn("spustený", channel.payload[0])
+ self.assertIn("účtovníctvo", channel.payload[1])
+ self.assertNotIn("is now running", channel.payload[1])
+
+ def test_display_name_replaces_only_the_local_runtime_hostname(self):
+ config = {"hostname": "HomeLAB_2"}
+ with mock.patch.object(notification_manager.socket, "gethostname", return_value="homelab-2"), \
+ mock.patch.object(notification_manager.socket, "getfqdn", return_value="homelab-2.home.lab"):
+ self.assertEqual(
+ notification_manager.resolve_notification_hostname("homelab-2", config),
+ "HomeLAB_2",
+ )
+ self.assertEqual(
+ notification_manager.resolve_notification_hostname("remote-pve", config),
+ "remote-pve",
+ )
+
+ class RecordingChannel:
+ def __init__(self):
+ self.payload = None
+
+ def send(self, title, body, severity, data=None):
+ self.payload = (title, body, severity, data)
+ return {"success": True}
+
+ manager = notification_manager.NotificationManager()
+ channel = RecordingChannel()
+ manager._channels = {"telegram": channel}
+ manager._config = {
+ "notification_language": "sk",
+ "hostname": "HomeLAB_2",
+ "ai_enabled": "false",
+ "telegram.rich_format": "false",
+ }
+ with mock.patch.object(notification_manager.socket, "gethostname", return_value="homelab-2"), \
+ mock.patch.object(notification_manager.socket, "getfqdn", return_value="homelab-2.home.lab"), \
+ mock.patch.object(manager, "_record_history"):
+ result = manager.send_notification(
+ "docker_stack_update_available", "INFO", "", "",
+ data={"hostname": "homelab-2", "vmid": "210", "ct_name": "repopulse", "count": "1", "details": "Docker Engine"},
+ skip_toggle_check=True,
+ )
+
+ self.assertTrue(result["success"])
+ self.assertIn("HomeLAB_2: Na CT 210 sú dostupné aktualizácie Docker", channel.payload[0])
+ self.assertNotIn("homelab-2:", channel.payload[0])
+
+ def test_email_channel_chrome_uses_the_runtime_catalog(self):
+ channel = object.__new__(notification_channels.EmailChannel)
+ channel.subject_prefix = "[ProxMenux]"
+ html = channel._format_html(
+ "[ProxMenux] pve01: Vysoké využitie CPU",
+ "Využitie CPU dosiahlo 95 %.",
+ "WARNING",
+ {
+ "_notification_language": "sk", "_event_type": "cpu_high",
+ "_group": "resources", "hostname": "pve01", "value": "95",
+ "threshold": "90", "cores": "8",
+ },
+ )
+ self.assertIn("Systémové zdroje", html)
+ self.assertIn("UPOZORNENIE", html)
+ self.assertIn("Server:", html)
+ self.assertIn("Aktuálna hodnota", html)
+ self.assertNotIn(">Details<", html)
+ self.assertNotIn("System Resources Report", html)
+
+ vm_html = channel._format_html(
+ "pve01: VM účtovníctvo (123) spustený",
+ "Virtuálny stroj účtovníctvo (ID: 123) je spustený.",
+ "INFO",
+ {
+ "_notification_language": "sk", "_event_type": "vm_start",
+ "_group": "vm_ct", "hostname": "pve01", "vmid": "123",
+ "vmname": "účtovníctvo",
+ },
+ )
+ self.assertIn("VM bola spustená", vm_html)
+
+ def test_notification_language_precedence_and_roundtrip(self):
+ manager = notification_manager.NotificationManager()
+ manager._config = {"notification_language": "sk", "ai_language": "de"}
+ self.assertEqual(manager._notification_language(), "sk")
+ manager._config = {"ai_language": "sk"}
+ self.assertEqual(manager._notification_language(), "sk")
+ manager._config = {}
+ self.assertEqual(manager._notification_language(), "en")
+
+ with tempfile.TemporaryDirectory() as directory:
+ db_path = Path(directory) / "settings.db"
+ conn = sqlite3.connect(db_path)
+ conn.execute("CREATE TABLE user_settings (setting_key TEXT PRIMARY KEY, setting_value TEXT, updated_at TEXT)")
+ conn.commit()
+ conn.close()
+ with mock.patch.object(notification_manager, "DB_PATH", db_path):
+ saved = manager.save_settings({"notification_language": "sk", "ai_language": "de"})
+ self.assertTrue(saved["success"])
+ loaded = notification_manager.NotificationManager()
+ loaded._load_config()
+ self.assertEqual(loaded.get_settings()["config"]["notification_language"], "sk")
+ self.assertEqual(loaded.get_settings()["config"]["ai_language"], "de")
+
+ def test_future_digest_time_change_resets_only_the_digest_guard(self):
+ class FixedDatetime:
+ @classmethod
+ def now(cls):
+ return datetime(2026, 9, 17, 13, 30)
+
+ with tempfile.TemporaryDirectory() as directory:
+ db_path = Path(directory) / "settings.db"
+ conn = sqlite3.connect(db_path)
+ conn.execute("CREATE TABLE user_settings (setting_key TEXT PRIMARY KEY, setting_value TEXT, updated_at TEXT)")
+ conn.commit()
+ conn.close()
+
+ manager = notification_manager.NotificationManager()
+ manager._config = {
+ "telegram.digest_enabled": "true",
+ "telegram.digest_time": "13:20",
+ "telegram.digest_last_at": "2026-09-17T13:20:25",
+ }
+ with mock.patch.object(notification_manager, "DB_PATH", db_path), \
+ mock.patch.object(notification_manager, "datetime", FixedDatetime):
+ result = manager.save_settings({
+ "telegram.digest_enabled": "true",
+ "telegram.digest_time": "13:40",
+ })
+
+ self.assertTrue(result["success"], result)
+ self.assertEqual(manager._config["telegram.digest_last_at"], "")
+ conn = sqlite3.connect(db_path)
+ stored = conn.execute(
+ "SELECT setting_value FROM user_settings WHERE setting_key = ?",
+ ("notification.telegram.digest_last_at",),
+ ).fetchone()
+ conn.close()
+ self.assertEqual(stored[0], "")
+
+ manager = notification_manager.NotificationManager()
+ manager._config = {
+ "telegram.digest_enabled": "true",
+ "telegram.digest_time": "13:40",
+ "telegram.digest_last_at": "2026-09-17T13:20:25",
+ }
+ with tempfile.TemporaryDirectory() as directory:
+ db_path = Path(directory) / "settings.db"
+ conn = sqlite3.connect(db_path)
+ conn.execute("CREATE TABLE user_settings (setting_key TEXT PRIMARY KEY, setting_value TEXT, updated_at TEXT)")
+ conn.commit()
+ conn.close()
+ with mock.patch.object(notification_manager, "DB_PATH", db_path), \
+ mock.patch.object(notification_manager, "datetime", FixedDatetime):
+ result = manager.save_settings({
+ "telegram.digest_enabled": "true",
+ "telegram.digest_time": "13:40",
+ })
+ self.assertTrue(result["success"], result)
+ self.assertEqual(manager._config["telegram.digest_last_at"], "2026-09-17T13:20:25")
+
+ def test_ai_language_is_independent_from_runtime_notification_language(self):
+ manager = notification_manager.NotificationManager()
+ manager._config = {
+ "notification_language": "sk",
+ "ai_language": "de",
+ "ai_provider": "groq",
+ }
+ self.assertEqual(manager._notification_language(), "sk")
+ self.assertEqual(manager._build_ai_config()["ai_language"], "de")
+
+ def test_legacy_runtime_capable_ai_language_is_preserved(self):
+ manager = notification_manager.NotificationManager()
+ manager._config = {"ai_language": "de"}
+ self.assertEqual(manager._notification_language(), "de")
+ self.assertEqual(manager.get_settings()["config"]["notification_language"], "de")
+
+ manager._config = {"notification_language": "invalid", "ai_language": "sk"}
+ self.assertEqual(manager._notification_language(), "sk")
+ manager._config = {"notification_language": "invalid", "ai_language": "de"}
+ self.assertEqual(manager._notification_language(), "de")
+
+ with tempfile.TemporaryDirectory() as directory:
+ db_path = Path(directory) / "settings.db"
+ conn = sqlite3.connect(db_path)
+ conn.execute("CREATE TABLE user_settings (setting_key TEXT PRIMARY KEY, setting_value TEXT, updated_at TEXT)")
+ conn.commit()
+ conn.close()
+ with mock.patch.object(notification_manager, "DB_PATH", db_path):
+ result = manager.save_settings({
+ "notification_language": manager.get_settings()["config"]["notification_language"],
+ "ai_language": "de",
+ })
+ self.assertTrue(result["success"], result)
+
+ def test_backup_email_localizes_status_and_important_packages(self):
+ channel = object.__new__(notification_channels.EmailChannel)
+ channel.subject_prefix = "[ProxMenux]"
+ for event_type, localized_status, english_status in (
+ ("backup_fail", "Zlyhalo", "Failed"),
+ ("backup_complete", "Dokončené", "Completed"),
+ ("backup_start", "Spustené", "Started"),
+ ):
+ backup_html = channel._format_html(
+ "pve01: Záloha", "Stav zálohy VM 100.", "WARNING",
+ {
+ "_notification_language": "sk", "_event_type": event_type,
+ "_group": "backup", "hostname": "pve01", "vmid": "100",
+ "vmname": "alpha", "storage": "pbs-main",
+ },
+ )
+ self.assertIn(f">{localized_status}<", backup_html)
+ self.assertNotIn(f">{english_status}<", backup_html)
+
+ updates_html = channel._format_html(
+ "pve01: Aktualizácie", "Dostupné aktualizácie.", "INFO",
+ {
+ "_notification_language": "sk", "_event_type": "system_updates",
+ "_group": "updates", "hostname": "pve01",
+ "important_list": "pve-manager\nproxmox-kernel",
+ },
+ )
+ self.assertIn("Dôležité balíky", updates_html)
+ self.assertNotIn("Important Packages", updates_html)
+
+ def test_metric_and_system_email_values_are_localized(self):
+ channel = object.__new__(notification_channels.EmailChannel)
+ channel.subject_prefix = "[ProxMenux]"
+ metric_html = channel._format_html(
+ "pve01: Vysoké využitie CPU", "CPU dosiahlo 95 %.", "WARNING",
+ {
+ "_notification_language": "sk", "_event_type": "cpu_high",
+ "_group": "resources", "hostname": "pve01", "value": "95",
+ },
+ )
+ self.assertIn("Vysoké využitie CPU", metric_html)
+ self.assertNotIn("Cpu High", metric_html)
+
+ system_html = channel._format_html(
+ "pve01: Systémový problém", "Zistil sa problém.", "WARNING",
+ {
+ "_notification_language": "sk", "_event_type": "system_problem",
+ "_group": "system", "hostname": "pve01", "reason": "chyba",
+ },
+ )
+ self.assertIn("Prehľad: Systém", system_html)
+ self.assertNotIn("Prehľad: ", system_html)
+
+ def test_every_ui_ai_language_is_accepted_by_backend(self):
+ source = (APPIMAGE_DIR / "components" / "notification-settings.tsx").read_text(encoding="utf-8")
+ block = re.search(r"const AI_LANGUAGES = \[(.*?)\n\]", source, re.DOTALL)
+ self.assertIsNotNone(block)
+ ui_languages = set(re.findall(r'value: "([a-z]+)"', block.group(1)))
+ self.assertIn("sv", ui_languages)
+ self.assertIn("no", ui_languages)
+ self.assertEqual(ui_languages - set(notification_manager.ALLOWED_AI_LANGUAGES), set())
+
+ def test_quiet_hours_digest_localizes_system_group(self):
+ class RecordingChannel:
+ def __init__(self):
+ self.payload = None
+
+ def send(self, title, body, severity, data=None):
+ self.payload = (title, body, severity, data)
+ return {"success": True}
+
+ manager = notification_manager.NotificationManager()
+ manager._config = {"notification_language": "sk", "hostname": "pve01"}
+ channel = RecordingChannel()
+ with tempfile.TemporaryDirectory() as directory:
+ db_path = Path(directory) / "settings.db"
+ conn = sqlite3.connect(db_path)
+ conn.execute(
+ "CREATE TABLE quiet_pending ("
+ "id INTEGER PRIMARY KEY, channel TEXT, event_type TEXT, event_group TEXT, "
+ "ts REAL, title TEXT, body TEXT)"
+ )
+ conn.execute(
+ "INSERT INTO quiet_pending "
+ "(channel, event_type, event_group, ts, title, body) VALUES (?, ?, ?, ?, ?, ?)",
+ ("telegram", "ai_model_migrated", "system", 0, "pve01: AI model updated", "body"),
+ )
+ conn.commit()
+ conn.close()
+ with mock.patch.object(notification_manager, "DB_PATH", db_path), mock.patch.object(
+ manager, "_record_history"
+ ):
+ manager._flush_quiet_for_channel("telegram", channel)
+
+ self.assertIsNotNone(channel.payload)
+ self.assertIn("Systém: 1", channel.payload[1])
+ self.assertNotIn("System", channel.payload[1])
+ self.assertTrue(channel.payload[3]["_quiet_hours_summary"])
+
+ def test_digest_buffer_coalesces_lxc_results_despite_detail_changes(self):
+ manager = notification_manager.NotificationManager()
+ with tempfile.TemporaryDirectory() as directory:
+ db_path = Path(directory) / "settings.db"
+ conn = sqlite3.connect(db_path)
+ conn.execute(
+ "CREATE TABLE digest_pending ("
+ "id INTEGER PRIMARY KEY, channel TEXT, event_type TEXT, "
+ "event_group TEXT, severity TEXT, ts INTEGER, title TEXT, body TEXT)"
+ )
+ conn.commit()
+ conn.close()
+
+ with mock.patch.object(notification_manager, "DB_PATH", db_path), \
+ mock.patch.object(notification_manager.time, "time", side_effect=(1000, 1060, 1070, 1080, 1301)):
+ manager._buffer_digest_event(
+ "telegram", "lxc_update_applied", "vm_ct", "INFO",
+ "pve01: LXC wireguard (101) update completed", "Source: Manual",
+ )
+ # An adjacent collector can report the same completion with
+ # a different source or duration. It must not appear twice.
+ manager._buffer_digest_event(
+ "telegram", "lxc_update_applied", "vm_ct", "INFO",
+ "pve01: LXC wireguard (101) update completed", "Source: Scheduled",
+ )
+ # Other event types still require the full message to match.
+ manager._buffer_digest_event(
+ "telegram", "app_update_available", "applications", "INFO",
+ "pve01: Update available", "Version: 1.0 → 1.1",
+ )
+ manager._buffer_digest_event(
+ "telegram", "app_update_available", "applications", "INFO",
+ "pve01: Update available", "Version: 1.0 → 1.2",
+ )
+ # The same result is allowed again outside the short window.
+ manager._buffer_digest_event(
+ "telegram", "lxc_update_applied", "vm_ct", "INFO",
+ "pve01: LXC wireguard (101) update completed", "Source: Manual",
+ )
+
+ conn = sqlite3.connect(db_path)
+ rows = conn.execute(
+ "SELECT ts, body FROM digest_pending ORDER BY id"
+ ).fetchall()
+ conn.close()
+
+ self.assertEqual(
+ rows,
+ [
+ (1000, "Source: Manual"),
+ (1070, "Version: 1.0 → 1.1"),
+ (1080, "Version: 1.0 → 1.2"),
+ (1301, "Source: Manual"),
+ ],
+ )
+
+ def test_visible_templates_use_known_backend_and_frontend_groups(self):
+ visible_groups = {
+ template.get("group", "other")
+ for template in notification_templates.TEMPLATES.values()
+ if not template.get("hidden", False)
+ }
+ backend_groups = set(notification_templates.EVENT_GROUPS)
+
+ source = (APPIMAGE_DIR / "components" / "notification-settings.tsx").read_text(encoding="utf-8")
+ match = re.search(r"const EVENT_CATEGORIES = \[(.*?)\]\.map", source)
+ self.assertIsNotNone(match)
+ frontend_groups = set(re.findall(r'"([a-z_]+)"', match.group(1)))
+
+ self.assertEqual(visible_groups - backend_groups, set())
+ self.assertEqual(visible_groups - frontend_groups, set())
+ self.assertEqual(frontend_groups, backend_groups)
+ for language in self.RUNTIME_LANGUAGES:
+ catalog = json.loads(
+ (APPIMAGE_DIR / "messages" / language / "common.json").read_text(encoding="utf-8")
+ )
+ self.assertEqual(
+ set(catalog["settings"]["notifications"]["categories"]),
+ frontend_groups,
+ language,
+ )
+
+ def test_build_bundles_runtime_catalogs(self):
+ build = (SCRIPTS_DIR / "build_appimage.sh").read_text(encoding="utf-8")
+ for language in self.RUNTIME_LANGUAGES:
+ self.assertIn(language, build)
+ self.assertIn('$APP_DIR/usr/share/proxmenux/messages', build)
+
+ def test_missing_event_type_names_exist_in_both_ui_catalogs(self):
+ for language in self.RUNTIME_LANGUAGES:
+ path = APPIMAGE_DIR / "messages" / language / "common.json"
+ event_types = json.loads(path.read_text(encoding="utf-8"))["settings"]["notifications"]["eventTypes"]
+ self.assertIn("lxc_update_applied", event_types)
+ self.assertIn("docker_stack_update_available", event_types)
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/AppImage/scripts/tests/test_vzdump_ai_integrity.py b/AppImage/scripts/tests/test_vzdump_ai_integrity.py
new file mode 100644
index 00000000..458606bf
--- /dev/null
+++ b/AppImage/scripts/tests/test_vzdump_ai_integrity.py
@@ -0,0 +1,380 @@
+import sys
+import unittest
+from pathlib import Path
+from unittest.mock import patch
+
+
+SCRIPTS_DIR = Path(__file__).resolve().parents[1]
+if str(SCRIPTS_DIR) not in sys.path:
+ sys.path.insert(0, str(SCRIPTS_DIR))
+
+import notification_manager
+import notification_templates
+from notification_channels import EmailChannel, TelegramChannel
+from notification_manager import NotificationManager
+from notification_templates import render_template
+
+
+AI_CONFIG = {
+ "ai_enabled": "true",
+ "ai_provider": "ollama",
+ "ai_ollama_url": "http://localhost:11434",
+ "ai_model": "test-model",
+ "ai_language": "en",
+}
+
+
+def _vzdump_report(count=49, failed=False):
+ header = "{:<8}{:<22}{:<10}{:<10}{:<14}{}".format(
+ "VMID", "Name", "Status", "Time", "Size", "Filename"
+ )
+ rows = []
+ for index in range(count):
+ vmid = 100 + index
+ status = "ERROR" if failed and index == count - 1 else "OK"
+ rows.append(
+ "{:<8}{:<22}{:<10}{:<10}{:<14}{}".format(
+ str(vmid),
+ f"guest-{vmid}",
+ status,
+ f"00:00:{index + 10:02d}",
+ f"{index + 1}.25 GiB",
+ f"/mnt/pve/archive/dump/vzdump-lxc-{vmid}-2026_09_15-01_00_00.tar.zst",
+ )
+ )
+ return (
+ "Proxmox vzdump report\n\n"
+ + header
+ + "\n"
+ + "\n".join(rows)
+ + "\nTotal running time: 00:49:00\nTotal size: 1225.25 GiB\n"
+ )
+
+
+def _render(event_type, failed=False):
+ return render_template(
+ event_type,
+ {
+ "hostname": "pve-a",
+ "storage": "archive",
+ "vmname": "49 guests",
+ "vmid": "batch",
+ "size": "1225.25 GiB",
+ "reason": "last guest failed" if failed else "",
+ "pve_title": "pve-a: vzdump backup status",
+ "pve_message": _vzdump_report(failed=failed),
+ },
+ )
+
+
+class _CapturingChannel:
+ def __init__(self):
+ self.calls = []
+
+ def send(self, title, body, severity, data):
+ self.calls.append((title, body, severity, data))
+ return {"success": True, "error": None}
+
+
+class _ReplacingEnhancer:
+ calls = 0
+
+ def __init__(self, config):
+ self.config = config
+
+ def enhance(self, *args, **kwargs):
+ type(self).calls += 1
+ return {"title": "AI shortened title", "body": "AI kept only one guest."}
+
+
+class _FailingEnhancer:
+ def __init__(self, config):
+ self.config = config
+
+ def enhance(self, *args, **kwargs):
+ return None
+
+
+class VzdumpAIIntegrityTests(unittest.TestCase):
+ def setUp(self):
+ with notification_templates._AI_CACHE_LOCK:
+ notification_templates._AI_CACHE.clear()
+ _ReplacingEnhancer.calls = 0
+
+ def _dispatch(self, event_type, rendered, enhancer):
+ manager = NotificationManager()
+ channel = _CapturingChannel()
+ manager._channels = {"telegram": channel}
+ with (
+ patch.object(manager, "_build_ai_config", return_value=dict(AI_CONFIG)),
+ patch.object(manager, "_record_history"),
+ patch.object(notification_manager, "enrich_context_for_ai", return_value=""),
+ patch.object(notification_templates, "AIEnhancer", enhancer),
+ ):
+ delivered = manager._dispatch_to_channels(
+ rendered["title"], rendered["body"],
+ "CRITICAL" if event_type == "backup_fail" else "INFO",
+ event_type, {}, "test",
+ )
+ self.assertTrue(delivered)
+ self.assertEqual(len(channel.calls), 1)
+ return channel.calls[0]
+
+ def _send_public(self, event_type, title, body):
+ manager = NotificationManager()
+ channel = _CapturingChannel()
+ manager._channels = {"telegram": channel}
+ manager._config = {"telegram.rich_format": "false"}
+ with (
+ patch.object(manager, "_build_ai_config", return_value=dict(AI_CONFIG)),
+ patch.object(manager, "_record_history"),
+ patch.object(notification_templates, "AIEnhancer", _ReplacingEnhancer),
+ ):
+ result = manager.send_notification(
+ event_type,
+ "CRITICAL" if event_type == "backup_fail" else "INFO",
+ title,
+ body,
+ source="test",
+ skip_toggle_check=True,
+ )
+ self.assertTrue(result["success"])
+ self.assertEqual(len(channel.calls), 1)
+ return channel.calls[0]
+
+ def test_public_send_backup_complete_skips_ai_and_keeps_all_49_items(self):
+ rendered = _render("backup_complete")
+
+ title, body, _, _ = self._send_public(
+ "backup_complete", rendered["title"], rendered["body"]
+ )
+
+ self.assertEqual(title, rendered["title"])
+ self.assertEqual(body, rendered["body"])
+ self.assertEqual(_ReplacingEnhancer.calls, 0)
+ self.assertEqual(body.count("✅ CT guest-"), 49)
+ self.assertIn("✅ CT guest-100 (100)", body)
+ self.assertIn("✅ CT guest-148 (148)", body)
+
+ def test_public_send_backup_fail_skips_ai_and_keeps_all_49_items(self):
+ rendered = _render("backup_fail", failed=True)
+
+ title, body, _, _ = self._send_public(
+ "backup_fail", rendered["title"], rendered["body"]
+ )
+
+ self.assertEqual(title, rendered["title"])
+ self.assertEqual(body, rendered["body"])
+ self.assertEqual(_ReplacingEnhancer.calls, 0)
+ self.assertEqual(body.count("✅ CT guest-") + body.count("❌ CT guest-"), 49)
+ self.assertIn("✅ CT guest-100 (100)", body)
+ self.assertIn("❌ CT guest-148 (148)", body)
+
+ def test_public_send_non_backup_event_still_uses_ai(self):
+ title, body, _, _ = self._send_public(
+ "cpu_high", "pve-a: CPU high", "CPU reached 95%."
+ )
+
+ self.assertEqual(title, "AI shortened title")
+ self.assertEqual(body, "AI kept only one guest.")
+ self.assertEqual(_ReplacingEnhancer.calls, 1)
+
+ def test_backup_complete_skips_ai_and_keeps_full_49_item_inventory(self):
+ rendered = _render("backup_complete")
+
+ title, body, _, _ = self._dispatch(
+ "backup_complete", rendered, _ReplacingEnhancer
+ )
+
+ self.assertEqual(title, rendered["title"])
+ self.assertEqual(body, rendered["body"])
+ self.assertEqual(_ReplacingEnhancer.calls, 0)
+ self.assertIn("✅ CT guest-100 (100)", body)
+ self.assertIn("📏 Size: 1.25 GiB | ⏱️ Duration: 00:00:10", body)
+ self.assertIn("✅ CT guest-148 (148)", body)
+ self.assertIn("📏 Size: 49.25 GiB | ⏱️ Duration: 00:00:58", body)
+ self.assertIn("📊 49 backups", body)
+ self.assertEqual(body.count("✅ CT guest-"), 49)
+
+ def test_backup_fail_skips_ai_and_keeps_inventory_and_failure_details(self):
+ rendered = _render("backup_fail", failed=True)
+
+ title, body, _, _ = self._dispatch(
+ "backup_fail", rendered, _ReplacingEnhancer
+ )
+
+ self.assertEqual(title, rendered["title"])
+ self.assertEqual(body, rendered["body"])
+ self.assertEqual(_ReplacingEnhancer.calls, 0)
+ self.assertIn("✅ CT guest-100 (100)", body)
+ self.assertIn("❌ CT guest-148 (148)", body)
+ self.assertIn("📏 Size: 49.25 GiB | ⏱️ Duration: 00:00:58", body)
+ self.assertIn("📊 49 backups | ❌ 1 failed", body)
+ self.assertEqual(body.count("✅ CT guest-"), 48)
+
+ def test_backup_ai_failure_sends_original_title_and_body_unchanged(self):
+ rendered = _render("backup_complete")
+
+ title, body, _, _ = self._dispatch(
+ "backup_complete", rendered, _FailingEnhancer
+ )
+
+ self.assertEqual(title, rendered["title"])
+ self.assertEqual(body, rendered["body"])
+
+ def test_non_backup_event_retains_existing_ai_rewrite(self):
+ rendered = {"title": "pve-a: CPU high", "body": "CPU reached 95%."}
+
+ title, body, _, _ = self._dispatch(
+ "cpu_high", rendered, _ReplacingEnhancer
+ )
+
+ self.assertEqual(title, "AI shortened title")
+ self.assertEqual(body, "AI kept only one guest.")
+
+ def test_backup_complete_email_html_contains_each_inventory_edge_and_summary_once(self):
+ rendered = _render("backup_complete")
+ channel = EmailChannel({})
+ data = {
+ "_event_type": "backup_complete",
+ "_group": "backup",
+ "hostname": "pve-a",
+ "storage": "archive",
+ "vmname": "49 guests",
+ "vmid": "batch",
+ "size": "1225.25 GiB",
+ }
+
+ html = channel._format_html(
+ "[ProxMenux] [INFO] " + rendered["title"],
+ rendered["body"], "INFO", data,
+ )
+
+ for vmid in range(100, 149):
+ self.assertEqual(html.count(f"guest-{vmid} ({vmid})"), 1, vmid)
+ self.assertEqual(html.count("49 backups"), 1)
+
+ def test_backup_fail_email_html_keeps_inventory_and_localized_status_once(self):
+ rendered = _render("backup_fail", failed=True)
+ channel = EmailChannel({})
+ data = {
+ "_event_type": "backup_fail",
+ "_group": "backup",
+ "_notification_language": "sk",
+ "hostname": "pve-a",
+ "storage": "archive",
+ "vmname": "49 guests",
+ "vmid": "batch",
+ "status": "failed",
+ "size": "1225.25 GiB",
+ "reason": "last guest failed",
+ }
+
+ html = channel._format_html(
+ "[ProxMenux] [CRITICAL] " + rendered["title"],
+ rendered["body"], "CRITICAL", data,
+ )
+
+ for vmid in range(100, 149):
+ self.assertEqual(html.count(f"guest-{vmid} ({vmid})"), 1, vmid)
+ self.assertEqual(html.count("49 backups"), 1)
+ self.assertEqual(html.count("1 failed"), 1)
+ self.assertEqual(html.count(">Zlyhalo<"), 1)
+ self.assertNotIn(">Failed<", html)
+ self.assertLessEqual(html.count("last guest failed"), 1)
+
+ def test_telegram_chunks_preserve_complete_49_item_message(self):
+ rendered = _render("backup_complete")
+ body = rendered["body"]
+ channel = TelegramChannel("123:token", "456")
+ html_message = (
+ f"🔵 {channel._escape_html(rendered['title'])}\n\n"
+ f"{channel._escape_html(body)}"
+ )
+
+ chunks = channel._split_message(html_message)
+
+ self.assertGreater(len(chunks), 1)
+ self.assertTrue(all(len(chunk) <= 4096 for chunk in chunks))
+ joined = "".join(chunks)
+ self.assertIn("guest-100 (100)", joined)
+ self.assertIn("guest-148 (148)", joined)
+ self.assertIn("49.25 GiB", joined)
+
+ def test_telegram_chunks_do_not_split_entities_or_open_tags(self):
+ from html.parser import HTMLParser
+
+ class _BalancedParser(HTMLParser):
+ def __init__(self):
+ super().__init__(convert_charrefs=False)
+ self.stack = []
+
+ def handle_starttag(self, tag, attrs):
+ self.stack.append(tag)
+
+ def handle_endtag(self, tag):
+ if not self.stack or self.stack.pop() != tag:
+ raise AssertionError(f"unbalanced closing tag: {tag}")
+
+ channel = TelegramChannel("123:token", "456")
+ html_message = "" + ("A & B " * 900) + ""
+
+ chunks = channel._split_message(html_message)
+
+ self.assertGreater(len(chunks), 1)
+ self.assertTrue(all(len(chunk) <= 4096 for chunk in chunks))
+ for chunk in chunks:
+ parser = _BalancedParser()
+ parser.feed(chunk)
+ parser.close()
+ self.assertEqual(parser.stack, [])
+ self.assertNotRegex(chunk, r"&(?:amp)?$")
+ self.assertNotRegex(chunk, r"^amp;")
+
+ def test_telegram_chunks_bound_an_oversized_entity(self):
+ channel = TelegramChannel("123:token", "456")
+ chunks = channel._split_message("&" + ("entity" * 900) + ";")
+
+ self.assertTrue(chunks)
+ self.assertTrue(all(len(chunk) <= 4096 for chunk in chunks))
+
+ def test_telegram_chunks_bound_an_oversized_tag(self):
+ channel = TelegramChannel("123:token", "456")
+ html_message = 'visible text'
+ chunks = channel._split_message(html_message)
+
+ self.assertTrue(chunks)
+ self.assertTrue(all(len(chunk) <= 4096 for chunk in chunks))
+ self.assertIn("visible text", "".join(chunks))
+
+ def test_telegram_chunks_bound_deeply_nested_formatting(self):
+ from html.parser import HTMLParser
+
+ class _BalancedParser(HTMLParser):
+ def __init__(self):
+ super().__init__(convert_charrefs=False)
+ self.stack = []
+
+ def handle_starttag(self, tag, attrs):
+ self.stack.append(tag)
+
+ def handle_endtag(self, tag):
+ if not self.stack or self.stack.pop() != tag:
+ raise AssertionError(f"unbalanced closing tag: {tag}")
+
+ html_message = ("" * 700) + ("A" * 5000) + ("" * 700)
+ channel = TelegramChannel("123:token", "456")
+ chunks = channel._split_message(html_message)
+
+ self.assertTrue(chunks)
+ self.assertTrue(all(len(chunk) <= 4096 for chunk in chunks))
+ self.assertEqual("".join(chunks).count("A"), 5000)
+ for chunk in chunks:
+ parser = _BalancedParser()
+ parser.feed(chunk)
+ parser.close()
+ self.assertEqual(parser.stack, [])
+
+
+if __name__ == "__main__":
+ unittest.main()
| |