diff --git a/AppImage/components/notification-settings.tsx b/AppImage/components/notification-settings.tsx index ecebac0a..b82af9a4 100644 --- a/AppImage/components/notification-settings.tsx +++ b/AppImage/components/notification-settings.tsx @@ -77,6 +77,7 @@ interface NotificationConfig { ai_api_keys: Record // Per-provider API keys ai_models: Record // Per-provider selected models ai_model: string // Current active model (for the selected provider) + notification_language: string ai_language: string ai_ollama_url: string ai_openai_base_url: string @@ -150,7 +151,7 @@ function validateGotifyUrl(url: string): { error?: string; warning?: string } { return {} } -const EVENT_CATEGORIES = ["vm_ct", "backup", "resources", "storage", "network", "security", "cluster", "services", "health", "updates", "other"].map(key => ({ key })) +const EVENT_CATEGORIES = ["vm_ct", "backup", "resources", "storage", "network", "security", "cluster", "services", "health", "updates", "hardware", "system", "other"].map(key => ({ key })) const CHANNEL_TYPES = ["telegram", "gotify", "discord", "email", "pushover", "apprise"] as const @@ -199,6 +200,24 @@ const AI_PROVIDERS = [ }, ] +const NOTIFICATION_LANGUAGES = [ + { value: "en", label: "English" }, + { value: "de", label: "Deutsch" }, + { value: "es", label: "Español" }, + { value: "fr", label: "Français" }, + { value: "it", label: "Italiano" }, + { value: "pt", label: "Português" }, + { value: "sk", label: "Slovenčina" }, + { value: "sv", label: "Svenska" }, +] + +function normalizeNotificationLanguage(notificationLanguage?: string, legacyAiLanguage?: string): string { + const runtimeLanguages = new Set(NOTIFICATION_LANGUAGES.map(language => language.value)) + if (notificationLanguage && runtimeLanguages.has(notificationLanguage)) return notificationLanguage + if (legacyAiLanguage && runtimeLanguages.has(legacyAiLanguage)) return legacyAiLanguage + return "en" +} + const AI_LANGUAGES = [ { value: "en", label: "English" }, { value: "sk", label: "Slovenčina" }, @@ -254,7 +273,7 @@ const DEFAULT_CONFIG: NotificationConfig = { event_categories: { vm_ct: true, backup: true, resources: true, storage: true, network: true, security: true, cluster: true, services: true, - health: true, updates: true, other: true, + health: true, updates: true, hardware: true, system: true, other: true, }, event_toggles: {}, event_types_by_group: {}, @@ -284,6 +303,7 @@ const DEFAULT_CONFIG: NotificationConfig = { openrouter: "", }, ai_model: "", + notification_language: "en", ai_language: "en", ai_ollama_url: "http://localhost:11434", ai_openai_base_url: "", @@ -401,6 +421,10 @@ export function NotificationSettings() { ai_prompt_mode: data.config.ai_prompt_mode || "default", ai_custom_prompt: data.config.ai_custom_prompt || "", ai_allow_suggestions: data.config.ai_allow_suggestions || "false", + notification_language: normalizeNotificationLanguage( + data.config.notification_language, + data.config.ai_language, + ), } // If ai_model exists but ai_models doesn't have it, save it if (configWithDefaults.ai_model && !configWithDefaults.ai_models[configWithDefaults.ai_provider]) { @@ -834,6 +858,7 @@ export function NotificationSettings() { ai_enabled: String(cfg.ai_enabled), ai_provider: cfg.ai_provider, ai_model: cfg.ai_model, + notification_language: normalizeNotificationLanguage(cfg.notification_language, cfg.ai_language), ai_language: cfg.ai_language, ai_ollama_url: cfg.ai_ollama_url, ai_openai_base_url: cfg.ai_openai_base_url, @@ -2176,6 +2201,31 @@ export function NotificationSettings() {

+ {/* ── Runtime notification language (independent of AI) ── */} +
+
+ + +
+ +

+ {t("settings.notifications.ui.notificationLanguageHint")} +

+
+ {/* ── Advanced: AI Enhancement ── */}
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'' 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()