From 85b88f709a229e1da072436ce223a44c29dda027 Mon Sep 17 00:00:00 2001 From: MacRimi Date: Tue, 6 Oct 2026 17:08:18 +0200 Subject: [PATCH] fix: make messages say what was verified (source questions from the Italian review) --- AppImage/messages/de/common.json | 4 +- AppImage/messages/en/common.json | 2 +- AppImage/messages/es/common.json | 4 +- AppImage/messages/fr/common.json | 4 +- AppImage/messages/it/common.json | 4 +- AppImage/messages/pt/common.json | 4 +- AppImage/messages/sk/common.json | 4 +- AppImage/messages/sv/common.json | 4 +- AppImage/scripts/managed_installs.py | 2 +- AppImage/scripts/notification_templates.py | 4 +- lang/es.json | 10 ++++ oci/remote/oci_immich_recognition.py | 23 ++++++--- oci/remote/oci_instance_transaction.py | 25 +++++++--- oci/remote/oci_restore_recovery.py | 12 +++-- oci/src/proxmenux_oci/management.py | 9 ++-- oci/tests/test_immich_recognition.py | 22 +++++++- oci/tests/test_restore_recovery.py | 50 +++++++++++++++++++ oci/tests/test_stage_release.py | 18 ++++++- .../backup_restore/lib_host_backup_common.sh | 2 +- scripts/post_install/uninstall-tools.sh | 7 ++- scripts/share/nfs_lxc_server.sh | 2 +- 21 files changed, 166 insertions(+), 50 deletions(-) diff --git a/AppImage/messages/de/common.json b/AppImage/messages/de/common.json index 7de1b2e4..36e0ffef 100644 --- a/AppImage/messages/de/common.json +++ b/AppImage/messages/de/common.json @@ -6755,12 +6755,12 @@ }, "post_install_update": { "title": "{hostname}: {count} ProxMenux Optimierungsupdate(s) verfügbar", - "body": "{count} ProxMenux Optimierungsupdate(s) auf diesem Host verfügbar.\n\n🛠️ Verfügbare Versionen:\n{tool_list}\n\n💡 Bewerben Sie sich unter:\n • ProxMenux Monitor → Einstellungen → ProxMenux Optimierungen\n • Oder führen Sie das Nachinstallationsmenü (Option 2) → „Verfügbare Updates anwenden“ aus.", + "body": "{count} ProxMenux Optimierungsupdate(s) auf diesem Host verfügbar.\n\n🛠️ Verfügbare Versionen:\n{tool_list}\n\n💡 Bewerben Sie sich unter:\n • ProxMenux Monitor → Einstellungen → ProxMenux Optimierungen\n • Oder öffnen Sie das ProxMenux-Menü → Einstellungen nach der Installation von Proxmox → „Verfügbare Updates anwenden“", "label": "ProxMenux Optimierungsupdates verfügbar" }, "secure_gateway_update_available": { "title": "{hostname}: {app_name} Update verfügbar{update_title_suffix}", - "body": "{app_name} (verwaltet von ProxMenux) hat 📦 {package_count} Paketaktualisierung(en) in seinem Container ausstehend.\n{version_line}\n\n💡 Öffnen Sie ProxMenux Monitor > Einstellungen > Secure Gateway und klicken Sie zum Übernehmen auf „Aktualisieren“.\n\n🗂️ Pakete:\n{package_list}", + "body": "{app_name} (verwaltet von ProxMenux) hat 📦 {package_count} Paketaktualisierung(en) in seinem Container ausstehend.\n{version_line}\n\n💡 Öffnen Sie ProxMenux Monitor > Sicherheit > Secure Gateway und klicken Sie zum Übernehmen auf „Aktualisieren“.\n\n🗂️ Pakete:\n{package_list}", "label": "Secure Gateway-Update verfügbar" }, "nvidia_driver_update_available": { diff --git a/AppImage/messages/en/common.json b/AppImage/messages/en/common.json index 99d3ab15..e0770328 100644 --- a/AppImage/messages/en/common.json +++ b/AppImage/messages/en/common.json @@ -6299,5 +6299,5 @@ "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"},"oci_update_completed":{"title":"{hostname}: {app_name} updated","body":"{app_name} was updated to its new image and its data was kept.\nContainers: {containers}","label":"OCI application updated"},"oci_update_failed":{"title":"{hostname}: {app_name} update did not complete","body":"The update of {app_name} did not complete.\nReason: {reason}\nContainers: {containers}\nOpen Manage installed OCI applications to check its state.","label":"OCI application update failed"},"oci_recreate_completed":{"title":"{hostname}: {app_name} recreated","body":"{app_name} was recreated with its new options and its data was kept.\nContainers: {containers}","label":"OCI application recreated"},"oci_recreate_failed":{"title":"{hostname}: {app_name} recreation did not complete","body":"The recreation of {app_name} did not complete.\nReason: {reason}\nContainers: {containers}\nOpen Manage installed OCI applications to check its state.","label":"OCI application recreation failed"},"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 sensor temperature — {value}°C","body":"Sensor 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}: Cluster event reported","body":"A cluster event was reported:\n{reason}","label":"Cluster event"},"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","temperatureAlertTitle":"{hostname}: Sensor temperature alert","temperatureAlertBody":"Temperature alert recorded without a complete measurement payload.","recordedReason":"Recorded reason: {reason}","recordedDetails":"Recorded details: {details}"},"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","unconfirmed":"Unconfirmed","completed_with_warnings":"Completed with warnings"}}},"temperature":{"sampleSpan":"High samples span {duration}."},"backup":{"unconfirmedTitle":"{hostname}: Backup outcome unconfirmed","confirmedTitle":"{hostname}: Backup complete","confirmedBody":"Backup completed successfully.","errorTitle":"{hostname}: Backup error reported","errorBody":"The backup report contains an error.","unconfirmedBody":"The backup outcome is not confirmed.","warningTitle":"{hostname}: Backup completed with warnings","warningBody":"Backup completed with warnings.","diagnosticsOmitted":"Additional diagnostic lines or text omitted: {count}. Original report retained."}}} + "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"},"oci_update_completed":{"title":"{hostname}: {app_name} updated","body":"{app_name} was updated to its new image and its data was kept.\nContainers: {containers}","label":"OCI application updated"},"oci_update_failed":{"title":"{hostname}: {app_name} update did not complete","body":"The update of {app_name} did not complete.\nReason: {reason}\nContainers: {containers}\nOpen Manage installed OCI applications to check its state.","label":"OCI application update failed"},"oci_recreate_completed":{"title":"{hostname}: {app_name} recreated","body":"{app_name} was recreated with its new options and its data was kept.\nContainers: {containers}","label":"OCI application recreated"},"oci_recreate_failed":{"title":"{hostname}: {app_name} recreation did not complete","body":"The recreation of {app_name} did not complete.\nReason: {reason}\nContainers: {containers}\nOpen Manage installed OCI applications to check its state.","label":"OCI application recreation failed"},"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 sensor temperature — {value}°C","body":"Sensor 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}: Cluster event reported","body":"A cluster event was reported:\n{reason}","label":"Cluster event"},"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 open the ProxMenux menu → Settings post-install Proxmox → \"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 > Security > 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","temperatureAlertTitle":"{hostname}: Sensor temperature alert","temperatureAlertBody":"Temperature alert recorded without a complete measurement payload.","recordedReason":"Recorded reason: {reason}","recordedDetails":"Recorded details: {details}"},"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","unconfirmed":"Unconfirmed","completed_with_warnings":"Completed with warnings"}}},"temperature":{"sampleSpan":"High samples span {duration}."},"backup":{"unconfirmedTitle":"{hostname}: Backup outcome unconfirmed","confirmedTitle":"{hostname}: Backup complete","confirmedBody":"Backup completed successfully.","errorTitle":"{hostname}: Backup error reported","errorBody":"The backup report contains an error.","unconfirmedBody":"The backup outcome is not confirmed.","warningTitle":"{hostname}: Backup completed with warnings","warningBody":"Backup completed with warnings.","diagnosticsOmitted":"Additional diagnostic lines or text omitted: {count}. Original report retained."}}} } diff --git a/AppImage/messages/es/common.json b/AppImage/messages/es/common.json index 577f8d61..09971d6c 100644 --- a/AppImage/messages/es/common.json +++ b/AppImage/messages/es/common.json @@ -6755,12 +6755,12 @@ }, "post_install_update": { "title": "{hostname}: {count} actualización(es) de optimizaciones de ProxMenux disponible(s)", - "body": "{count} actualización(es) de optimizaciones de ProxMenux disponible(s) en este host.\n\n🛠️ Versiones disponibles:\n{tool_list}\n\n💡 Aplicar desde:\n • ProxMenux Monitor → Ajustes → ProxMenux Optimizations\n • O ejecute el menú de post-instalación (opción 2) → \"Aplicar actualizaciones disponibles\"", + "body": "{count} actualización(es) de optimizaciones de ProxMenux disponible(s) en este host.\n\n🛠️ Versiones disponibles:\n{tool_list}\n\n💡 Aplicar desde:\n • ProxMenux Monitor → Ajustes → ProxMenux Optimizations\n • O abra el menú de ProxMenux → Configuración post instalación de Proxmox → \"Aplicar actualizaciones disponibles\"", "label": "Actualizaciones de optimizaciones de ProxMenux disponibles" }, "secure_gateway_update_available": { "title": "{hostname}: actualización de {app_name} disponible{update_title_suffix}", - "body": "{app_name} (gestionado por ProxMenux) tiene 📦 {package_count} actualización(es) de paquetes pendiente(s) en su contenedor.\n{version_line}\n\n💡 Abra ProxMenux Monitor > Ajustes > Secure Gateway y haga clic en \"Actualizar\" para aplicarlas.\n\n🗂️ Paquetes:\n{package_list}", + "body": "{app_name} (gestionado por ProxMenux) tiene 📦 {package_count} actualización(es) de paquetes pendiente(s) en su contenedor.\n{version_line}\n\n💡 Abra ProxMenux Monitor > Seguridad > Secure Gateway y haga clic en \"Actualizar\" para aplicarlas.\n\n🗂️ Paquetes:\n{package_list}", "label": "Actualización de Secure Gateway disponible" }, "nvidia_driver_update_available": { diff --git a/AppImage/messages/fr/common.json b/AppImage/messages/fr/common.json index 47e6cac8..80c75b1e 100644 --- a/AppImage/messages/fr/common.json +++ b/AppImage/messages/fr/common.json @@ -6755,12 +6755,12 @@ }, "post_install_update": { "title": "{hostname} : {count} ProxMenux mise à jour(s) d'optimisation disponible(s)", - "body": "{count} ProxMenux mise à jour(s) d'optimisation disponible sur cet hôte.\n\n🛠️ Versions disponibles :\n{tool_list}\n\n💡 Postulez à partir de :\n • ProxMenux Moniteur → Paramètres → ProxMenux Optimisations\n • Ou exécutez le menu post-installation (option 2) → \"Appliquer les mises à jour disponibles\"", + "body": "{count} ProxMenux mise à jour(s) d'optimisation disponible sur cet hôte.\n\n🛠️ Versions disponibles :\n{tool_list}\n\n💡 Postulez à partir de :\n • ProxMenux Moniteur → Paramètres → ProxMenux Optimisations\n • Ou ouvrez le menu ProxMenux → Paramètres post-installation de Proxmox → \"Appliquer les mises à jour disponibles\"", "label": "ProxMenux mises à jour d'optimisation disponibles" }, "secure_gateway_update_available": { "title": "{hostname} : {app_name} mise à jour disponible{update_title_suffix}", - "body": "{app_name} (géré par ProxMenux) a 📦 {package_count} mise à jour(s) du package en attente dans son conteneur.\n{version_line}\n\n💡 Ouvrez ProxMenux Monitor > Paramètres > Secure Gateway et cliquez sur « Mettre à jour » pour appliquer.\n\n🗂️ Forfaits :\n{package_list}", + "body": "{app_name} (géré par ProxMenux) a 📦 {package_count} mise à jour(s) du package en attente dans son conteneur.\n{version_line}\n\n💡 Ouvrez ProxMenux Monitor > Sécurité > Secure Gateway et cliquez sur « Mettre à jour » pour appliquer.\n\n🗂️ Forfaits :\n{package_list}", "label": "Mise à jour de Secure Gateway disponible" }, "nvidia_driver_update_available": { diff --git a/AppImage/messages/it/common.json b/AppImage/messages/it/common.json index 33bafe5a..0e83b689 100644 --- a/AppImage/messages/it/common.json +++ b/AppImage/messages/it/common.json @@ -6755,12 +6755,12 @@ }, "post_install_update": { "title": "{hostname}: aggiornamenti ottimizzazioni ProxMenux disponibili: {count}", - "body": "{count} aggiornamenti delle ottimizzazioni ProxMenux disponibili su questo host.\n\n🛠️ Versioni disponibili:\n{tool_list}\n\n💡 Per applicarli:\n • ProxMenux Monitor → Settings → ProxMenux Optimizations\n • Oppure aprire il menu post-installazione (opzione 2) → \"Apply available updates\"", + "body": "{count} aggiornamenti delle ottimizzazioni ProxMenux disponibili su questo host.\n\n🛠️ Versioni disponibili:\n{tool_list}\n\n💡 Per applicarli:\n • ProxMenux Monitor → Settings → ProxMenux Optimizations\n • Oppure aprire il menu ProxMenux → Impostazioni post-installazione Proxmox → \"Applica aggiornamenti disponibili\"", "label": "Aggiornamenti ottimizzazioni ProxMenux disponibili" }, "secure_gateway_update_available": { "title": "{hostname}: aggiornamento disponibile per {app_name}{update_title_suffix}", - "body": "{app_name} (gestita da ProxMenux) ha 📦 {package_count} aggiornamenti dei pacchetti in sospeso nel proprio container.\n{version_line}\n\n💡 Aprire ProxMenux Monitor > Settings > Secure Gateway e fare clic su \"Update\" per applicarli.\n\n🗂️ Pacchetti:\n{package_list}", + "body": "{app_name} (gestita da ProxMenux) ha 📦 {package_count} aggiornamenti dei pacchetti in sospeso nel proprio container.\n{version_line}\n\n💡 Aprire ProxMenux Monitor > Sicurezza > Secure Gateway e fare clic su \"Update\" per applicarli.\n\n🗂️ Pacchetti:\n{package_list}", "label": "Aggiornamento Secure Gateway disponibile" }, "nvidia_driver_update_available": { diff --git a/AppImage/messages/pt/common.json b/AppImage/messages/pt/common.json index b5fb65ea..df42e524 100644 --- a/AppImage/messages/pt/common.json +++ b/AppImage/messages/pt/common.json @@ -6755,12 +6755,12 @@ }, "post_install_update": { "title": "{hostname}: {count} ProxMenux atualizações de otimização disponíveis", - "body": "{count} ProxMenux atualizações de otimização disponíveis neste host.\n\n🛠️ Versões disponíveis:\n{tool_list}\n\n💡 Inscreva-se em:\n • ProxMenux Monitor → Configurações → ProxMenux Otimizações\n • Ou execute o menu pós-instalação (opção 2) → \"Aplicar atualizações disponíveis\"", + "body": "{count} ProxMenux atualizações de otimização disponíveis neste host.\n\n🛠️ Versões disponíveis:\n{tool_list}\n\n💡 Inscreva-se em:\n • ProxMenux Monitor → Configurações → ProxMenux Otimizações\n • Ou abra o menu ProxMenux → Configurações pós-instalação do Proxmox → \"Aplicar atualizações disponíveis\"", "label": "Atualizações de otimização ProxMenux disponíveis" }, "secure_gateway_update_available": { "title": "{hostname}: {app_name} atualização disponível{update_title_suffix}", - "body": "{app_name} (gerenciado por ProxMenux) tem 📦 {package_count} atualizações de pacote pendentes em seu contêiner.\n{version_line}\n\n💡 Abra ProxMenux Monitor > Configurações > Gateway Seguro e clique em \"Atualizar\" para aplicar.\n\n🗂️ Pacotes:\n{package_list}", + "body": "{app_name} (gerenciado por ProxMenux) tem 📦 {package_count} atualizações de pacote pendentes em seu contêiner.\n{version_line}\n\n💡 Abra ProxMenux Monitor > Segurança > Gateway Seguro e clique em \"Atualizar\" para aplicar.\n\n🗂️ Pacotes:\n{package_list}", "label": "Atualização do Secure Gateway disponível" }, "nvidia_driver_update_available": { diff --git a/AppImage/messages/sk/common.json b/AppImage/messages/sk/common.json index f4ff23f8..ecf9e1e7 100644 --- a/AppImage/messages/sk/common.json +++ b/AppImage/messages/sk/common.json @@ -6754,12 +6754,12 @@ }, "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“", + "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 otvorte ponuku ProxMenux → Nastavenie Proxmoxu po inštalácii → „Nainštalovať 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}", + "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 > Bezpečnosť > 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": { diff --git a/AppImage/messages/sv/common.json b/AppImage/messages/sv/common.json index b4860100..93ae8bb3 100644 --- a/AppImage/messages/sv/common.json +++ b/AppImage/messages/sv/common.json @@ -6755,12 +6755,12 @@ }, "post_install_update": { "title": "{hostname}: {count} ProxMenux optimeringsuppdatering(ar) tillgängliga", - "body": "{count} ProxMenux optimeringsuppdatering(ar) tillgängliga på denna värd.\n\n🛠️ Tillgängliga versioner:\n{tool_list}\n\n💡 Ansök från:\n • ProxMenux Bildskärm → Inställningar → ProxMenux Optimering\n • Eller kör efterinstallationsmenyn (alternativ 2) → \"Använd tillgängliga uppdateringar\"", + "body": "{count} ProxMenux optimeringsuppdatering(ar) tillgängliga på denna värd.\n\n🛠️ Tillgängliga versioner:\n{tool_list}\n\n💡 Ansök från:\n • ProxMenux Bildskärm → Inställningar → ProxMenux Optimering\n • Eller öppna ProxMenux-menyn → Inställningar efter installation av Proxmox → \"Tillämpa tillgängliga uppdateringar\"", "label": "ProxMenux optimeringsuppdateringar tillgängliga" }, "secure_gateway_update_available": { "title": "{hostname}: {app_name} uppdatering tillgänglig{update_title_suffix}", - "body": "{app_name} (hanteras av ProxMenux) har 📦 {package_count} paketuppdatering(ar) väntande i sin behållare.\n{version_line}\n\n💡 Öppna ProxMenux Monitor > Inställningar > Säker gateway och klicka på \"Uppdatera\" för att ansöka.\n\n🗂️ Paket:\n{package_list}", + "body": "{app_name} (hanteras av ProxMenux) har 📦 {package_count} paketuppdatering(ar) väntande i sin behållare.\n{version_line}\n\n💡 Öppna ProxMenux Monitor > Säkerhet > Säker gateway och klicka på \"Uppdatera\" för att ansöka.\n\n🗂️ Paket:\n{package_list}", "label": "Säker gateway-uppdatering tillgänglig" }, "nvidia_driver_update_available": { diff --git a/AppImage/scripts/managed_installs.py b/AppImage/scripts/managed_installs.py index 6dd161f3..6d2bc323 100644 --- a/AppImage/scripts/managed_installs.py +++ b/AppImage/scripts/managed_installs.py @@ -356,7 +356,7 @@ def _detect_oci_apps() -> list[dict]: "type": "oci_app", "name": app.get("name") or app_id, "current_version": None, # filled by checker - "menu_label": "Settings → Secure Gateway", + "menu_label": "Security → Secure Gateway", "menu_script": None, # OCI apps update via the dashboard, no bash script # Stash the raw app_id so the checker can find it without # parsing the prefixed registry id. diff --git a/AppImage/scripts/notification_templates.py b/AppImage/scripts/notification_templates.py index 20bdadfd..e62f37d4 100644 --- a/AppImage/scripts/notification_templates.py +++ b/AppImage/scripts/notification_templates.py @@ -1685,7 +1685,7 @@ TEMPLATES = { '🛠️ 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"' + ' • Or open the ProxMenux menu → Settings post-install Proxmox → "Apply available updates"' ), 'label': 'ProxMenux optimization updates available', 'group': 'updates', @@ -1713,7 +1713,7 @@ TEMPLATES = { '{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 ' + '💡 Open ProxMenux Monitor > Security > Secure Gateway and click ' '"Update" to apply.\n\n' '🗂️ Packages:\n{package_list}' ), diff --git a/lang/es.json b/lang/es.json index 27fb56c4..34b2214a 100644 --- a/lang/es.json +++ b/lang/es.json @@ -3281,6 +3281,7 @@ "Keeping GPU in source VM config": "Mantener la GPU en la configuración de la VM de origen", "Keeping the settings changed in Proxmox:": "Mantener la configuración cambiada en Proxmox:", "Kept sharedfiles group (has regular users assigned).": "Se mantiene el grupo de archivos compartidos (tiene usuarios habituales asignados).", + "Kept sharedfiles group (it still has members that are not remapped users).": "Se conserva el grupo sharedfiles (todavía tiene miembros que no son usuarios remapeados).", "Kernel and architecture info": "Información sobre el kernel y la arquitectura", "Kernel headers and build tools verified.": "Encabezados del kernel y herramientas de compilación verificados.", "Kernel headers install failed — DKMS rebuild will likely fail:": "Falló la instalación de los encabezados del kernel; es probable que la reconstrucción de DKMS falle:", @@ -3632,6 +3633,7 @@ "Mounted ISO on device": "ISO montado en el dispositivo", "Mounted at": "Montado en", "Mounted external disk": "Disco externo montado", + "Mounted external disk (no network needed, single-machine dedup)": "Disco externo montado (no necesita red, desduplicación de una sola máquina)", "Mounted external disk (offline-safe, single-machine dedup)": "Disco externo montado (desduplicación de una sola máquina, segura sin conexión)", "Mounted filesystem detected": "Sistema de archivos montado detectado", "Mounting CIFS share...": "Montando recurso compartido CIFS...", @@ -4654,6 +4656,7 @@ "ProxMenux only acts as a launcher — once the script starts, control leaves ProxMenux.": "ProxMenux solo actúa como Lanzador del script.", "ProxMenux saved it locally at:": "ProxMenux lo guardó localmente en:", "ProxMenux-managed .link file(s). User-authored .link files were left in place.": "Archivo(s) .link gestionado(s) por ProxMenux. Los archivos .link creados por el usuario se han conservado.", + "ProxMenux-managed .link files removed: {count}. User-authored .link files were left in place.": "Archivos .link gestionados por ProxMenux eliminados: {count}. Los archivos .link creados por el usuario se han conservado.", "Proxmology logo applied": "Logotipo de Proxmology aplicado", "Proxmox 9 system update allready": "Actualización del sistema Proxmox 9 ya", "Proxmox APT repositories configured": "Repositorios Proxmox APT configurados", @@ -4816,6 +4819,7 @@ "Recovery blob:": "Blob de recuperación:", "Recovery completed. The container had not been modified yet.": "Recuperación completada. El contenedor aún no había sido modificado.", "Recovery completed. The disks of the failed attempt were removed; the backup is kept.": "Recuperación completada. Los discos del intento fallido se han eliminado; el backup se mantiene.", + "Recovery completed; the backup is kept.": "Recuperación completada; el backup se mantiene.", "Recovery failed": "La recuperación falló", "Recovery passphrase": "Frase de contraseña de recuperación", "Recovery setup failed": "Error en la configuración de recuperación", @@ -6115,6 +6119,7 @@ "The disk does not belong to this CT; automatic adoption is unsafe": "El disco no pertenece a este CT; incorporarlo automáticamente no es seguro", "The disk size cannot be reproduced": "El tamaño del disco no se puede reproducir", "The disk usage of the container could not be read": "El uso del disco del contenedor no se podía leer", + "The disks of the failed attempt could not be removed and stay in the temporary container:": "Los discos del intento fallido no se pudieron eliminar y siguen en el contenedor temporal:", "The domain must resolve to the public address of this network before the certificate can be issued.": "El dominio debe resolver a la dirección pública de esta red antes de poder emitir el certificado.", "The download client still needs to be configured.": "El cliente de descarga todavía necesita ser configurado.", "The download stopped progressing; cancelling this attempt.": "La descarga dejó de progresar; cancelando este intento.", @@ -6276,6 +6281,7 @@ "The prepared directory escapes the rootfs:": "El directorio preparado escapa a los rootfs:", "The preselected VMID does not exist on this host:": "El VMID preseleccionado no existe en este host:", "The previous native backup will be restored. Shared host directories are not reverted. Displaced disks are kept.": "Se restaurará el backup nativo anterior. Los directorios compartidos del host no se revierten. Los discos desplazados se conservan.", + "The previous native backup will be restored. Shared host directories are not reverted. The disks of the failed attempt are removed.": "Se restaurará el backup nativo anterior. Los directorios compartidos del host no se revierten. Los discos del intento fallido se eliminan.", "The previous recognition choice could not be put back:": "No se pudo volver a la opción de reconocimiento anterior:", "The previous stack contract is not safe; review it before reusing it": "El contrato de pila anterior no es seguro; revíselo antes de reutilizarlo", "The previous stack contract is not valid; it is not archived automatically": "El contrato de pila anterior no es válido; no se archiva automáticamente", @@ -6297,6 +6303,7 @@ "The read-only view was not published": "La vista de solo lectura no se publicó", "The read/write view was not published": "La vista de lectura/escritura no se publicó", "The recipe requires configuration at startup; its coordinated replay is not available": "La configuración de la receta requiere al inicio; su reproducción coordinada no está disponible", + "The recognition change of Immich did not complete:": "El cambio de reconocimiento de Immich no se completó:", "The recognition of Immich was not changed:": "No se cambió el reconocimiento de Immich:", "The record belongs to another container": "El registro pertenece a otro contenedor", "The record does not belong to this operation": "El registro no pertenece a esta operación", @@ -6456,6 +6463,7 @@ "This NFS share is fully restricted — even the host root cannot write to it.": "Este recurso compartido NFS está completamente restringido: ni siquiera la raíz del host puede escribir en él.", "This VM requires extra installation steps, see install guide at:\nhttps://github.com/community-scripts/ProxmoxVE/discussions/144": "Esta máquina virtual requiere pasos de instalación adicionales; consulte la guía de instalación en:\nhttps://github.com/community-scripts/ProxmoxVE/discussions/144", "This action will:": "Esta acción:", + "This application cannot be recovered yet:": "Esta aplicación todavía no se puede recuperar:", "This application cannot be recovered yet; nothing was changed:": "Esta aplicación todavía no se puede recuperar; no se ha cambiado nada:", "This application is not an Immich installed by ProxMenux": "Esta aplicación no es un Immich instalado por ProxMenux", "This archive does not contain a recognized backup layout.": "Este archivo no contiene un diseño de backup reconocido.", @@ -6472,6 +6480,7 @@ "This container is not a member of a multi-container application": "Este contenedor no forma parte de una aplicación de varios contenedores", "This container is not a registered OCI instance.": "Este contenedor no es una instancia OCI registrada.", "This container was restored or came back from another host after its record on this host was written. Open this menu again to recover it; nothing was changed.": "Este contenedor se restauró o volvió de otro host después de que se escribiera su registro en este host. Abre de nuevo este menú para recuperarlo; no se ha cambiado nada.", + "This container was restored or came back from another host after its record on this host was written. Open this menu again to recover it; the container was not modified.": "Este contenedor se restauró o volvió de otro host después de que se escribiera su registro en este host. Abre de nuevo este menú para recuperarlo; el contenedor no se ha modificado.", "This converts all directory UIDs/GIDs by adding 100000": "Esto convierte todos los UID/GID del directorio agregando 100000", "This converts all file UIDs/GIDs by adding 100000": "Esto convierte todos los archivos UID/GID agregando 100000", "This creates a backup in case you need to revert changes": "Esto crea un backup en caso de que necesite revertir los cambios", @@ -6776,6 +6785,7 @@ "Unsupported tmpfs options": "Opciones de tmpfs sin soporte", "Unsupported volume options:": "Opciones de volumen no admitidas:", "Until they are registered again they cannot be updated or managed from here. The recovery checks first that everything they need is on this host and changes nothing otherwise.": "Hasta que se registren de nuevo no se pueden actualizar ni gestionar desde aquí. La recuperación comprueba antes que este host tiene todo lo que necesitan y, si no es así, no cambia nada.", + "Until they are registered again they cannot be updated or managed from here. The recovery checks first that everything they need is on this host; an application that lacks something is not registered.": "Hasta que se registren de nuevo no se pueden actualizar ni gestionar desde aquí. La recuperación comprueba antes que este host tiene todo lo que necesitan; una aplicación a la que le falta algo no se registra.", "Untrusted or modified NVIDIA hook": "Hook NVIDIA sin confianza o modificado", "Unused image removed from the cache:": "Imagen sin uso eliminada de la caché:", "Unused images removed from the cache:": "Imágenes sin uso eliminadas de la caché:", diff --git a/oci/remote/oci_immich_recognition.py b/oci/remote/oci_immich_recognition.py index fba2d9a6..721b79ff 100644 --- a/oci/remote/oci_immich_recognition.py +++ b/oci/remote/oci_immich_recognition.py @@ -228,16 +228,18 @@ def apply(root, primary_id, vmid, images, acceleration, render_device, gfx_overr def revert(root, primary_id, vmid, images, previous, was_running): - """Give the container and the records the choice they had.""" + """Give the container and the records the choice they had. Returns + whether they have it again.""" try: if oci_stack_modify.is_running(vmid): stop(vmid) apply(root, primary_id, vmid, images, *previous) except (OSError, ValueError, KeyError, RuntimeError, subprocess.SubprocessError) as error: msg_warn(f"{translate('The previous recognition choice could not be put back:')} {error}") - return + return False if was_running: subprocess.run(['pct', 'start', str(vmid)], capture_output=True, check=False) + return True def change(root, primary_id, acceleration, render_device=None, gfx_override=None): @@ -262,17 +264,17 @@ def change(root, primary_id, acceleration, render_device=None, gfx_override=None msg_info(translate('Preparing the machine learning container for the new choice...')) apply(root, primary_id, vmid, images, acceleration, render_device, gfx_override) msg_ok(translate('Machine learning container prepared')) - except BaseException: - revert(root, primary_id, vmid, images, previous, was_running) + except BaseException as error: + error.recognition_kept = revert(root, primary_id, vmid, images, previous, was_running) raise try: # The image of the new choice replaces the one in use, as an update does. oci_stack_native.run(primary_id, acknowledge_external_data=True) - except BaseException: + except BaseException as error: with instances.locked(root): # An update left halfway keeps its own record of what to restore. - if not instances.read(root, primary_id).get('pending_stack_transaction'): - revert(root, primary_id, vmid, images, previous, was_running) + error.recognition_kept = (not instances.read(root, primary_id).get('pending_stack_transaction') + and revert(root, primary_id, vmid, images, previous, was_running)) raise if was_running and not oci_stack_modify.is_running(vmid): # An update leaves each container as it found it, and this one was @@ -297,7 +299,12 @@ def main(): return 1 except (OSError, ValueError, KeyError, RuntimeError, StopIteration, subprocess.SubprocessError) as error: if not getattr(error, 'oci_reported', False): - msg_error(f"{translate('The recognition of Immich was not changed:')} {error}") + # Only an error raised before anything was touched, or one that + # was put back, leaves the previous choice in place. + kept = getattr(error, 'recognition_kept', True) + message = (translate('The recognition of Immich was not changed:') if kept + else translate('The recognition change of Immich did not complete:')) + msg_error(f"{message} {error}") return 1 msg_ok(translate('Recognition of Immich changed; the application was updated with the new choice.')) return 0 diff --git a/oci/remote/oci_instance_transaction.py b/oci/remote/oci_instance_transaction.py index 65841efb..3de6349c 100644 --- a/oci/remote/oci_instance_transaction.py +++ b/oci/remote/oci_instance_transaction.py @@ -768,29 +768,33 @@ def release_stage(state, discard=False): """After a commit the holder CT only keeps its own rootfs: every parked volume went back to the application. After a verified recovery it holds the disks of the failed attempt, which the restored backup replaced: - `discard` removes them with it. Otherwise anything still attached keeps it.""" + `discard` removes them with it. Otherwise anything still attached keeps it. + Returns whether the holder CT was removed.""" stage = state.get('stage') if not stage: - return + return False try: config = owned(stage, 'proxmenux-transaction=' + state['id']) except (ValueError, RuntimeError, subprocess.CalledProcessError): - return + return False held = mounts(config) or any(re.fullmatch(r'unused[0-9]+', key) for key in parse_config(config)) if held and not discard: - return + return False if discard: stop(stage) run('pct', 'destroy', str(stage)) + return True def discard_stage(state): """Remove the holder CT of an operation that ended in a verified recovery. - A failure here leaves it in place and does not undo the recovery.""" + A failure here leaves it in place and does not undo the recovery. + Returns whether the holder CT was removed.""" try: - release_stage(state, discard=True) + return release_stage(state, discard=True) except (OSError, ValueError, RuntimeError, subprocess.SubprocessError) as error: log(f'cleanup: {error}') + return False def gib(size): @@ -1139,8 +1143,13 @@ def complete_recovery(root, journal, state): checkpoint(journal, state, 'rolled-back') if show: # A member of a stack is released when the whole stack is back. - discard_stage(state) - msg_ok(translate('Recovery completed. The disks of the failed attempt were removed; the backup is kept.')) + if discard_stage(state): + msg_ok(translate('Recovery completed. The disks of the failed attempt were removed; the backup is kept.')) + else: + msg_ok(translate('Recovery completed; the backup is kept.')) + if state.get('stage'): + msg_warn(f"{translate('The disks of the failed attempt could not be removed and stay in the temporary container:')} " + f"CT {state['stage']}") if state.get('original_host_sources') or state.get('desired_host_sources'): msg_info2(translate('Shared host files are kept as they are; the backup does not restore their content.')) diff --git a/oci/remote/oci_restore_recovery.py b/oci/remote/oci_restore_recovery.py index e5a28b65..9845edd0 100644 --- a/oci/remote/oci_restore_recovery.py +++ b/oci/remote/oci_restore_recovery.py @@ -943,7 +943,7 @@ def start_order(entry): return int(order[1]) if order else 9999 -def recover(root, plan, start=False, host_firewall=False): +def recover(root, plan, start=False, host_firewall=()): for bridge, subnet in plan['bridges'].items(): msg_info(f"{translate('Creating the private network...')} {bridge}") create_bridge(bridge, subnet) @@ -974,7 +974,8 @@ def recover(root, plan, start=False, host_firewall=False): if not rule: continue text = f"TCP {rule['port']} {translate('from')} {rule['source']}" - if not host_firewall: + # Each rule is confirmed on its own; one answer does not cover the rest. + if rule['vmid'] not in host_firewall: plan['notes'].append(f"CT {entry['vmid']}: {translate('its host firewall rule was not added:')} {text}") elif add_firewall_rule(rule): msg_ok(f"{translate('Host firewall rule added:')} {text}") @@ -1020,7 +1021,8 @@ def main(): parser.add_argument('action', choices=('list', 'plan', 'recover')) parser.add_argument('--root', type=Path, default=instances.ROOT) parser.add_argument('--start', action='store_true') - parser.add_argument('--host-firewall', action='store_true') + parser.add_argument('--host-firewall', type=int, action='append', default=[], metavar='VMID', + help='Add the host firewall rule of this container; one option for each confirmed rule') parser.add_argument('--automatic', action='store_true', help='Only the applications that need no question: the ones the cluster keeps a record of') args = parser.parse_args() @@ -1041,13 +1043,13 @@ def main(): failed = 0 if args.automatic: plans = [plan for plan in plans if automatic(plan)] - args.start = args.host_firewall = False + args.start, args.host_firewall = False, [] for plan in plans: containers = ', '.join(f"CT {entry['vmid']}" for entry in plan['restored']) msg_info2(f"{plan['title']} · {containers}") if plan['blockers']: failed += 1 - msg_error(translate('This application cannot be recovered yet; nothing was changed:')) + msg_error(translate('This application cannot be recovered yet:')) for line in plan['blockers']: msg_info2(f" - {line}") lost = [entry for entry in plan['restored'] if entry.get('unrecoverable')] diff --git a/oci/src/proxmenux_oci/management.py b/oci/src/proxmenux_oci/management.py index c63bdb96..b95d934f 100644 --- a/oci/src/proxmenux_oci/management.py +++ b/oci/src/proxmenux_oci/management.py @@ -186,7 +186,7 @@ def offer_recovery(project, ui): lines = '\n'.join(f" CT {row['vmid']} {row['hostname']}" for row in found) text = (f"{translate('These containers were restored from a backup and this host has no record of their OCI application:')}" f"\n\n{lines}\n\n" - f"{translate('Until they are registered again they cannot be updated or managed from here. The recovery checks first that everything they need is on this host and changes nothing otherwise.')}" + f"{translate('Until they are registered again they cannot be updated or managed from here. The recovery checks first that everything they need is on this host; an application that lacks something is not registered.')}" f"\n\n{translate('Recover them now?')}") if not ui.confirm(text, default=True): return @@ -194,8 +194,7 @@ def offer_recovery(project, ui): for rule in _restored_firewall_rules(project): if ui.confirm(translate('CT {vmid} is a host monitor and had a rule in the host firewall. Allow TCP port {port} from {subnet} through the firewall of this host? Existing firewall rules are not changed.').format( vmid=rule['vmid'], port=rule['port'], subnet=rule['source']), default=False): - command.append('--host-firewall') - break + command.extend(['--host-firewall', str(rule['vmid'])]) if ui.confirm(translate('Start the applications once they are registered? Answer No if the original containers are still running on another host: both would use the same addresses.'), default=False): command.append('--start') @@ -277,7 +276,7 @@ def manage_instance(project, ui, row, action=None, lifecycle_args=()): # A container that came back from another node or from an older backup # carries the record that describes it; the one of this host is not used. if carry_records(project, vmids=[row['vmid']], verify=True).get(row['vmid']) == 'stale': - ui.message(translate('This container was restored or came back from another host after its record on this host was written. Open this menu again to recover it; nothing was changed.'), + ui.message(translate('This container was restored or came back from another host after its record on this host was written. Open this menu again to recover it; the container was not modified.'), translate('OCI management')) return False row = check_selected(project, row) @@ -370,7 +369,7 @@ def manage_instance(project, ui, row, action=None, lifecycle_args=()): if action is None: return False if action == 'recover' and not ui.review( - translate('The previous native backup will be restored. Shared host directories are not reverted. Displaced disks are kept.'), + translate('The previous native backup will be restored. Shared host directories are not reverted. The disks of the failed attempt are removed.'), translate('Recover OCI'), question=translate('Recover now?'), default=True): return False return _run_lifecycle([sys.executable, str(project / 'remote/oci_instance_transaction.py'), diff --git a/oci/tests/test_immich_recognition.py b/oci/tests/test_immich_recognition.py index 3d7393a5..81d02b4d 100644 --- a/oci/tests/test_immich_recognition.py +++ b/oci/tests/test_immich_recognition.py @@ -171,7 +171,8 @@ class RevertTests(unittest.TestCase): patch.object(recognition, "stop", side_effect=lambda vmid: calls.append(("stop", vmid))), \ patch.object(recognition, "apply", side_effect=lambda *arguments: calls.append(("apply",) + arguments[2:])), \ patch.object(recognition.subprocess, "run", side_effect=lambda command, **_: calls.append(tuple(command[:3]))): - recognition.revert(Path("/nonexistent"), 100, 101, {"cpu": "image"}, ("cpu", None, None), True) + kept = recognition.revert(Path("/nonexistent"), 100, 101, {"cpu": "image"}, ("cpu", None, None), True) + self.assertTrue(kept) self.assertEqual(calls, [("stop", 101), ("apply", 101, {"cpu": "image"}, "cpu", None, None), ("pct", "start", "101")]) @@ -180,9 +181,26 @@ class RevertTests(unittest.TestCase): patch.object(recognition, "apply", side_effect=RuntimeError("pct set: locked")), \ patch.object(recognition, "msg_warn") as warned, \ patch.object(recognition.subprocess, "run") as started: - recognition.revert(Path("/nonexistent"), 100, 101, {}, ("cpu", None, None), True) + kept = recognition.revert(Path("/nonexistent"), 100, 101, {}, ("cpu", None, None), True) self.assertIn("pct set: locked", warned.call_args[0][0]) started.assert_not_called() + self.assertFalse(kept) + + def test_the_error_only_says_nothing_changed_when_the_choice_was_kept(self): + def reported(error): + with patch.object(recognition.os, "geteuid", return_value=0), \ + patch.object(recognition.sys, "argv", ["x", "100", "--acceleration", "cpu"]), \ + patch.object(recognition, "change", side_effect=error), \ + patch.object(recognition, "translate", side_effect=lambda text: text), \ + patch.object(recognition, "msg_error") as shown: + self.assertEqual(recognition.main(), 1) + return shown.call_args[0][0] + self.assertIn("was not changed", reported(ValueError("already on that choice"))) + put_back = RuntimeError("update failed"); put_back.recognition_kept = True + self.assertIn("was not changed", reported(put_back)) + left = RuntimeError("update failed"); left.recognition_kept = False + self.assertIn("did not complete", reported(left)) + self.assertNotIn("was not changed", reported(left)) class RocmProbeTests(unittest.TestCase): diff --git a/oci/tests/test_restore_recovery.py b/oci/tests/test_restore_recovery.py index a179db4c..6d2deabe 100644 --- a/oci/tests/test_restore_recovery.py +++ b/oci/tests/test_restore_recovery.py @@ -546,5 +546,55 @@ class RegistrationTests(unittest.TestCase): self.assertNotIn("deployment", services[2]) +class FirewallConsentTests(unittest.TestCase): + """A host firewall rule is added only for the container it was confirmed for.""" + + def recover(self, confirmed): + rule = lambda vmid, port: {"vmid": vmid, "port": port, "source": "192.0.2.0/24"} + plan = {"bridges": {}, "renumbered": {}, "hold": False, "notes": [], + "restored": [{"vmid": 120, "firewall": rule(120, 61208)}, {"vmid": 121, "firewall": rule(121, 19999)}]} + added = [] + with patch.object(recovery, "restore_host_files"), patch.object(recovery, "clean_format_directories"), \ + patch.object(recovery, "register", return_value=[120, 121]), patch.object(recovery.carried, "carry"), \ + patch.object(recovery, "add_firewall_rule", side_effect=lambda rule: added.append(rule["vmid"]) or True), \ + patch.object(recovery, "msg_info"), patch.object(recovery, "msg_ok"): + recovery.recover(Path("/nonexistent"), plan, host_firewall=confirmed) + return added, plan["notes"] + + def test_one_confirmed_rule_does_not_add_the_others(self): + added, notes = self.recover([121]) + self.assertEqual(added, [121]) + self.assertEqual([note[:7] for note in notes], ["CT 120:"]) + + def test_without_an_answer_no_rule_is_added(self): + added, notes = self.recover([]) + self.assertEqual(added, []) + self.assertEqual(len(notes), 2) + + def test_the_menu_asks_for_each_rule_and_passes_only_the_confirmed_ones(self): + sys.path.insert(0, str(ROOT / "src")) + from proxmenux_oci import management + rules = [{"vmid": 120, "port": 61208, "source": "192.0.2.0/24"}, + {"vmid": 121, "port": 19999, "source": "192.0.2.0/24"}] + answers = iter([True, False, True, False]) # recover, rule of 120, rule of 121, start + + class Ui: + asked = [] + + def confirm(self, text, default=False): + self.asked.append(text) + return next(answers) + + ui = Ui() + with patch.object(management, "restored_applications", return_value=[{"vmid": 120, "hostname": "glances"}, + {"vmid": 121, "hostname": "netdata"}]), \ + patch.object(management, "_restored_firewall_rules", return_value=rules), \ + patch.object(management, "_run_lifecycle") as run: + management.offer_recovery(ROOT, ui) + command = run.call_args[0][0] + self.assertEqual(len(ui.asked), 4) + self.assertEqual(command[command.index("--host-firewall"):], ["--host-firewall", "121"]) + + if __name__ == "__main__": unittest.main() diff --git a/oci/tests/test_stage_release.py b/oci/tests/test_stage_release.py index 32ea94c7..8f0ebf4c 100644 --- a/oci/tests/test_stage_release.py +++ b/oci/tests/test_stage_release.py @@ -54,8 +54,24 @@ class StageReleaseTests(unittest.TestCase): patch.object(transaction, "stop"), \ patch.object(transaction, "run", side_effect=RuntimeError("pct failed with exit code 255")), \ patch.object(transaction, "log") as logged: - transaction.discard_stage(STATE) + removed = transaction.discard_stage(STATE) self.assertIn("pct failed", logged.call_args[0][0]) + self.assertFalse(removed) + + def test_the_result_says_whether_the_container_was_removed(self): + with patch.object(transaction, "owned", return_value=HOLDING), patch.object(transaction, "stop"), \ + patch.object(transaction, "run"): + self.assertTrue(transaction.discard_stage(STATE)) + self.assertFalse(transaction.release_stage(STATE)) + with patch.object(transaction, "owned", side_effect=ValueError("not ours")): + self.assertFalse(transaction.discard_stage(STATE)) + self.assertFalse(transaction.discard_stage({"id": "x"})) + + def test_the_recovery_only_says_the_disks_were_removed_when_they_were(self): + source = (ROOT / "remote/oci_instance_transaction.py").read_text() + claim = source.index("The disks of the failed attempt were removed") + self.assertIn("if discard_stage(state):", source[source.rindex("\n", 0, claim - 80):claim]) + self.assertIn("could not be removed and stay in the temporary container", source) if __name__ == "__main__": diff --git a/scripts/backup_restore/lib_host_backup_common.sh b/scripts/backup_restore/lib_host_backup_common.sh index 80b8f7f3..a1177676 100755 --- a/scripts/backup_restore/lib_host_backup_common.sh +++ b/scripts/backup_restore/lib_host_backup_common.sh @@ -2507,7 +2507,7 @@ hb_configure_borg_manual() { --menu "\n$(hb_translate "Select repository destination:")" \ "$HB_UI_MENU_H" "$HB_UI_MENU_W" "$HB_UI_MENU_LIST" \ "remote" "$(hb_translate 'Remote server via SSH (recommended — off-host, dedup across machines)')" \ - "usb" "$(hb_translate 'Mounted external disk (offline-safe, single-machine dedup)')" \ + "usb" "$(hb_translate 'Mounted external disk (no network needed, single-machine dedup)')" \ "local" "$(hb_translate 'Local directory (single-machine — only use if it is a SEPARATE disk)')" \ 3>&1 1>&2 2>&3) || return 1 diff --git a/scripts/post_install/uninstall-tools.sh b/scripts/post_install/uninstall-tools.sh index a5daa9a3..b8050e2f 100644 --- a/scripts/post_install/uninstall-tools.sh +++ b/scripts/post_install/uninstall-tools.sh @@ -742,7 +742,12 @@ uninstall_persistent_network() { done < <(pmx_uninstall_persistent_network) if (( removed > 0 )); then - msg_ok "$(translate "Removed") $removed $(translate "ProxMenux-managed .link file(s). User-authored .link files were left in place.")" + local removed_message + removed_message="$(translate "ProxMenux-managed .link files removed: {count}. User-authored .link files were left in place.")" + if [[ "$removed_message" != *'{count}'* ]]; then + removed_message="ProxMenux-managed .link files removed: {count}. User-authored .link files were left in place." + fi + msg_ok "${removed_message//\{count\}/$removed}" msg_info "$(translate "Interface names will return to default systemd behavior.")" NECESSARY_REBOOT=1 else diff --git a/scripts/share/nfs_lxc_server.sh b/scripts/share/nfs_lxc_server.sh index 870dd4de..c60ce604 100644 --- a/scripts/share/nfs_lxc_server.sh +++ b/scripts/share/nfs_lxc_server.sh @@ -551,7 +551,7 @@ uninstall_nfs() { pct exec "$CTID" -- groupdel sharedfiles 2>/dev/null || true msg_ok "$(translate "Removed sharedfiles group.")" else - msg_warn "$(translate "Kept sharedfiles group (has regular users assigned).")" + msg_warn "$(translate "Kept sharedfiles group (it still has members that are not remapped users).")" fi fi