diff --git a/AppImage/components/hardware.tsx b/AppImage/components/hardware.tsx index ea9c3a2d..6afcd8e1 100644 --- a/AppImage/components/hardware.tsx +++ b/AppImage/components/hardware.tsx @@ -1048,7 +1048,8 @@ return ( {nvidiaInstall.update_check.available ? ( <>
- {t(feature.key)} + {linkifyGithubMentions(t(feature.key))}
))} diff --git a/AppImage/components/storage-overview.tsx b/AppImage/components/storage-overview.tsx index f4796b75..07e60325 100644 --- a/AppImage/components/storage-overview.tsx +++ b/AppImage/components/storage-overview.tsx @@ -250,6 +250,23 @@ export function StorageOverview() { const [diskObservations, setDiskObservations] = useState{t("storage.model")}
@@ -3082,10 +3121,12 @@ function openSmartReport(disk: DiskInfo, testStatus: SmartTestStatus, smartAttri .top-bar-title { font-weight: 600; } .top-bar-subtitle { font-size: 11px; color: #94a3b8; } .top-bar button { - background: #06b6d4; color: #fff; border: none; padding: 10px 20px; border-radius: 6px; - font-size: 14px; font-weight: 600; cursor: pointer; + background: #06b6d4; color: #fff; border: none; padding: 8px 12px; border-radius: 6px; + font-size: 14px; font-weight: 600; cursor: pointer; display: inline-flex; align-items: center; justify-content: center; } .top-bar button:hover { background: #0891b2; } + .top-bar .btn-group { display: flex; gap: 8px; } + .top-bar button svg { width: 18px; height: 18px; display: block; } /* Header */ .rpt-header { @@ -3184,13 +3225,25 @@ function pmxPrint(){ } - + @@ -4026,7 +4079,12 @@ function SmartTestTab({ disk, observations = [], lastTestDate }: SmartTestTabPro } return ( -- {t("storage.smartTest.reportHelp")} -
{t("storage.historyTab.loading")}
+
{t("storage.historyTab.note")}
{displayedFirewallLogs.map((entry, idx) => {
const text = entry.t || ""
diff --git a/AppImage/lib/pve-tag-color.ts b/AppImage/lib/pve-tag-color.ts
new file mode 100644
index 00000000..c859f0d0
--- /dev/null
+++ b/AppImage/lib/pve-tag-color.ts
@@ -0,0 +1,79 @@
+// Proxmox VE tag color scheme — 1:1 port of the algorithm in
+// proxmoxlib.js (`Proxmox.Utils.stringToRGB` +
+// `Proxmox.Utils.getTextContrastClass`). Same input → same color
+// as the PVE web UI, so tags render identically in both places.
+
+export type TagColor = {
+ bg: string // css `background-color`
+ fg: string // css `color` — auto-picked for contrast (SAPC)
+ border: string // css `border-color`
+}
+
+// Verbatim port of stringToRGB from proxmoxlib.js. The `+ 'prox'`
+// suffix, the `<< 5` hash, and the `alpha=0.7 / bg=255` blend
+// keep the output in the [76.5, 255] range per channel — that's
+// why every PVE tag is a "washed" bright color instead of a raw
+// hash-hue.
+function stringToRGB(input: string): [number, number, number] {
+ let hash = 0
+ if (!input) return [255, 255, 255]
+ const source = input + "prox"
+ for (let i = 0; i < source.length; i++) {
+ // eslint-disable-next-line no-bitwise
+ hash = source.charCodeAt(i) + ((hash << 5) - hash)
+ // eslint-disable-next-line no-bitwise
+ hash = hash & hash
+ }
+ const alpha = 0.7
+ const bg = 255
+ return [
+ // eslint-disable-next-line no-bitwise
+ (hash & 255) * alpha + bg * (1 - alpha),
+ // eslint-disable-next-line no-bitwise
+ ((hash >> 8) & 255) * alpha + bg * (1 - alpha),
+ // eslint-disable-next-line no-bitwise
+ ((hash >> 16) & 255) * alpha + bg * (1 - alpha),
+ ]
+}
+
+// SAPC-based light/dark text picker — verbatim port of
+// getTextContrastClass. Same tag → same text color as PVE.
+function getTextContrastClass(rgb: [number, number, number]): "light" | "dark" {
+ const blkThrs = 0.022
+ const blkClmp = 1.414
+ const r = (rgb[0] / 255) ** 2.4
+ const g = (rgb[1] / 255) ** 2.4
+ const b = (rgb[2] / 255) ** 2.4
+ let bg = r * 0.2126729 + g * 0.7151522 + b * 0.072175
+ bg = bg > blkThrs ? bg : bg + (blkThrs - bg) ** blkClmp
+ const contrastLight = bg ** 0.65 - 1
+ const contrastDark = bg ** 0.56 - 0.046134502
+ return Math.abs(contrastLight) >= Math.abs(contrastDark) ? "light" : "dark"
+}
+
+function rgbToCss(rgb: [number, number, number]): string {
+ return `rgb(${Math.round(rgb[0])}, ${Math.round(rgb[1])}, ${Math.round(rgb[2])})`
+}
+
+export function tagToColor(tag: string): TagColor {
+ const rgb = stringToRGB(tag)
+ const bg = rgbToCss(rgb)
+ const fg = getTextContrastClass(rgb) === "light" ? "#ffffff" : "#000000"
+ return { bg, fg, border: bg }
+}
+
+// Split a PVE tags string into an array. PVE separators are ';' and
+// ',' (both accepted); whitespace around tokens is stripped and
+// empty tokens dropped.
+export function parseTags(raw: string | null | undefined): string[] {
+ if (!raw) return []
+ return raw
+ .split(/[;,]/)
+ .map((t) => t.trim())
+ .filter(Boolean)
+}
+
+// Join back into the canonical PVE format (';' separator).
+export function stringifyTags(tags: string[]): string {
+ return tags.map((t) => t.trim()).filter(Boolean).join(";")
+}
diff --git a/AppImage/messages/de/common.json b/AppImage/messages/de/common.json
index 24a8c5f7..41120c8f 100644
--- a/AppImage/messages/de/common.json
+++ b/AppImage/messages/de/common.json
@@ -21,6 +21,7 @@
"cancel": "Stornieren",
"save": "Speichern",
"edit": "Bearbeiten",
+ "remove": "",
"close": "Schließen",
"resetAll": "Alles zurücksetzen",
"undo": "Rückgängig machen",
@@ -297,10 +298,10 @@
"installFailedManual": "Die Installation ist fehlgeschlagen. Versuchen Sie es manuell: apt-get install smartmontools nvme-cli",
"installToolsManual": "Tools konnten nicht installiert werden. Versuchen Sie es manuell: apt-get install smartmontools nvme-cli",
"runTest": "Führen Sie den SMART-Test durch",
- "shortTest": "Kurztest (~2 Min.)",
+ "shortTest": "Kurztest",
"longTest": "Langer Test (1-4 Stunden)",
- "extendedTest": "Erweiterter Test (Hintergrund)",
- "testHelp": "Ein kurzer Test dauert etwa 2 Minuten. Der erweiterte Test läuft im Hintergrund und kann auf großen Datenträgern mehrere Stunden dauern. Sie erhalten eine Benachrichtigung, wenn der Vorgang abgeschlossen ist.",
+ "extendedTest": "Erweiterter Test",
+ "testHelp": "",
"startFailed": "Der Test konnte nicht gestartet werden",
"short": "Kurz",
"extended": "Erweitert",
@@ -316,7 +317,6 @@
"worst": "Am schlimmsten",
"status": "Status",
"viewFullReport": "Vollständigen SMART-Bericht anzeigen",
- "reportHelp": "Erstellen Sie einen detaillierten SMART-Bericht mit Analysen und Empfehlungen.",
"loadingReport": "Bericht wird geladen...",
"reportLoadFailed": "Berichtsdaten konnten nicht geladen werden.",
"statusValues": {
@@ -1021,7 +1021,11 @@
"readOnly": "schreibgeschützt",
"stopped": "gestoppt",
"mounted": "montiert"
- }
+ },
+ "startOnBoot": "",
+ "tags": "",
+ "tagsPlaceholder": "",
+ "tagsNone": ""
},
"logs": {
"header": "Protokolle für {name} (VMID: {vmid})",
@@ -1421,7 +1425,9 @@
"notificationsEnabled": "Upstream-Update-Benachrichtigungen EIN – zum Stummschalten klicken",
"notificationsMuted": "Upstream-Update-Benachrichtigungen stummgeschaltet – zum Aktivieren klicken",
"notifyUpstreamLabel": "Benachrichtigen Sie mich, wenn eine neue Upstream-Version verfügbar ist",
- "notifyUpstreamHelp": "Sendet „app_update_available“ an die Kanäle, die in Einstellungen → Benachrichtigungen aktiviert sind.Deaktivieren Sie diese Option, wenn diese App auf Ihrer Box nicht aktualisiert werden kann."
+ "notifyUpstreamHelp": "Sendet „app_update_available“ an die Kanäle, die in Einstellungen → Benachrichtigungen aktiviert sind.Deaktivieren Sie diese Option, wenn diese App auf Ihrer Box nicht aktualisiert werden kann.",
+ "excludeFromBadgeLabel": "",
+ "excludeFromBadgeHelp": ""
}
},
"settings": {
@@ -3001,6 +3007,7 @@
"currentFeatures": {
"hostUpdate": "Host-Update mit einem Klick über den Health Monitor. Die neue Schaltfläche „Jetzt aktualisieren“ in System Updates führt den Proxmox-Update-Flow in einem Dashboard-Terminal aus, ohne den Browser zu verlassen.",
"mobileInstall": "In-App-Installationsaufforderung für Mobilgeräte. Erstbesucher von Android und iOS Safari sehen jetzt einfache Schritte zum Hinzufügen des Monitors als PWA zu ihrem Startbildschirm.",
+ "i18n": "",
"pageSpeed": "Schnellere Seitenladevorgänge und reibungslosere Navigation im Dashboard.Die Übersicht wird sofort geöffnet und auf der Seite „VMs und LXCs“ blinkt nie wieder „Laden…“ zwischen den Gastmodalitäten.",
"appTab": "Neuer App-Tab im VM- und LXC-Modal – insbesondere für LXCs.Registrieren Sie die in einem Container installierten Apps, erfassen Sie ihre Weblinks und erhalten Sie Benachrichtigungen, wenn eine neue Upstream-Version ausgeliefert wird.",
"updatesTab": "Überarbeitete Registerkarte „Updates“ für LXCs: Wenden Sie Betriebssystempakete und Updates für registrierte Apps über eine einzige Schaltfläche an und planen Sie einen wiederkehrenden automatischen Update-Job, der das Betriebssystem des Containers und seine verfolgte App bei jeder Ausführung überprüft.",
@@ -4579,7 +4586,6 @@
"tryDifferentSearch": "Versuchen Sie es mit einem anderen Befehl oder überprüfen Sie die Rechtschreibung.",
"searchAnyCommand": "Suchen Sie nach einem beliebigen Befehl",
"trySearchingFor": "Suchen Sie nach:",
- "searchTip": "Tipp: Suchen Sie nach einem beliebigen Linux- oder Proxmox-Befehl (qm, pct, zpool).",
"noExamplesFound": "Keine Beispiele gefunden",
"reconnecting": "Wieder verbinden…",
"reconnected": "Erfolgreich wiederhergestellt",
diff --git a/AppImage/messages/en/common.json b/AppImage/messages/en/common.json
index 332c97d5..869c2e2d 100644
--- a/AppImage/messages/en/common.json
+++ b/AppImage/messages/en/common.json
@@ -20,6 +20,7 @@
"cancel": "Cancel",
"save": "Save",
"edit": "Edit",
+ "remove": "Remove",
"close": "Close",
"resetAll": "Reset all",
"undo": "Undo",
@@ -296,10 +297,10 @@
"installFailedManual": "Installation failed. Try manually: apt-get install smartmontools nvme-cli",
"installToolsManual": "Failed to install tools. Try manually: apt-get install smartmontools nvme-cli",
"runTest": "Run SMART test",
- "shortTest": "Short test (~2 min)",
+ "shortTest": "Short test",
"longTest": "Long test (1-4 hours)",
- "extendedTest": "Extended test (background)",
- "testHelp": "A short test takes about 2 minutes. The extended test runs in the background and can take several hours on large disks. You will receive a notification when it finishes.",
+ "extendedTest": "Extended test",
+ "testHelp": "A short test takes about 2 minutes. The extended test runs in the background and can take several hours on large disks. The result will show up in the History tab when it finishes.",
"startFailed": "Failed to start test",
"short": "Short",
"extended": "Extended",
@@ -315,7 +316,6 @@
"worst": "Worst",
"status": "Status",
"viewFullReport": "View full SMART report",
- "reportHelp": "Generate a detailed SMART report with analysis and recommendations.",
"loadingReport": "Loading report...",
"reportLoadFailed": "Failed to load report data.",
"statusValues": {
@@ -1020,7 +1020,11 @@
"readOnly": "read-only",
"stopped": "stopped",
"mounted": "mounted"
- }
+ },
+ "startOnBoot": "Start at boot",
+ "tags": "Tags",
+ "tagsPlaceholder": "Add tag…",
+ "tagsNone": "No tags"
},
"logs": {
"header": "Logs for {name} (VMID: {vmid})",
@@ -1420,7 +1424,9 @@
"notificationsEnabled": "Upstream update notifications ON — click to mute",
"notificationsMuted": "Upstream update notifications MUTED — click to enable",
"notifyUpstreamLabel": "Notify me when a new upstream version is available",
- "notifyUpstreamHelp": "Sends `app_update_available` to the channels enabled in Settings → Notifications. Turn off if this app can't be updated on your box."
+ "notifyUpstreamHelp": "Sends `app_update_available` to the channels enabled in Settings → Notifications. Turn off if this app can't be updated on your box.",
+ "excludeFromBadgeLabel": "Exclude from the LXC updates counter",
+ "excludeFromBadgeHelp": "Don't count this app in the aggregate updates badge on the LXC list card. Useful when you're pinned to a specific version on purpose (tracker requirement, compatibility freeze). Doesn't affect the App tab's own state or the outbound notification."
}
},
"settings": {
@@ -3000,6 +3006,7 @@
"currentFeatures": {
"hostUpdate": "One-click host update from the Health Monitor. The new Update Now button in System Updates runs the Proxmox update flow inside a dashboard terminal, without leaving the browser.",
"mobileInstall": "In-app install prompt for mobile. First-time visitors on Android and iOS Safari now see simple steps for adding the Monitor to their home screen as a PWA.",
+ "i18n": "The Monitor now speaks 8 languages: English, Spanish, German, French, Italian, Portuguese, Swedish and Slovak. Huge thanks to @vaso73 for building the i18n scaffolding that made this possible.",
"pageSpeed": "Faster page loads and smoother navigation across the dashboard. Overview opens instantly and the VMs & LXCs page never flashes 'Loading…' between guest modals again.",
"appTab": "New App tab inside the VM & LXC modal — especially for LXCs. Register the apps installed in a container, capture their weblinks, and get notifications when a new upstream version ships.",
"updatesTab": "Reworked Updates tab for LXCs: apply OS packages and registered-app updates from a single button, and schedule a recurring auto-update job that checks the container's OS and its tracked app on every run.",
@@ -4579,7 +4586,6 @@
"tryDifferentSearch": "Try another command or check the spelling.",
"searchAnyCommand": "Search for any command",
"trySearchingFor": "Try searching for:",
- "searchTip": "Tip: Search for any Linux or Proxmox command (qm, pct, zpool).",
"noExamplesFound": "No examples found",
"reconnecting": "Reconnecting…",
"reconnected": "Reconnected successfully",
diff --git a/AppImage/messages/es/common.json b/AppImage/messages/es/common.json
index ac2267cb..eb423b2a 100644
--- a/AppImage/messages/es/common.json
+++ b/AppImage/messages/es/common.json
@@ -21,6 +21,7 @@
"cancel": "Cancelar",
"save": "Guardar",
"edit": "Editar",
+ "remove": "Eliminar",
"close": "Cerrar",
"resetAll": "Restablecer todo",
"undo": "Deshacer",
@@ -216,7 +217,7 @@
"physicalDisk": "Disco físico",
"overview": "General",
"smart": "SMART",
- "history": "Historia",
+ "history": "Historial",
"schedule": "Programación",
"serialNumber": "Número de serie",
"healthStatus": "Estado de salud",
@@ -245,10 +246,10 @@
"estimatedYears": "~{value} años",
"estimatedMonths": "~{value} meses"
},
- "availableSpare": "Aprovechar. Repuesto",
+ "availableSpare": "Repuesto disp.",
"smartAttributes": "Atributos SMART",
"powerOnHours": "Horas de encendido",
- "rotationRate": "Tasa de rotación",
+ "rotationRate": "Velocidad de rotación",
"smartStatus": "Estado SMART",
"reallocatedSectors": "Sectores reasignados",
"pendingSectors": "Sectores Pendientes",
@@ -297,10 +298,10 @@
"installFailedManual": "La instalación falló. Pruebe manualmente: apt-get install smartmontools nvme-cli",
"installToolsManual": "No se pudieron instalar las herramientas. Pruebe manualmente: apt-get install smartmontools nvme-cli",
"runTest": "Ejecutar prueba SMART",
- "shortTest": "Prueba corta (~2 min)",
+ "shortTest": "Prueba corta",
"longTest": "Prueba larga (1-4 horas)",
- "extendedTest": "Prueba extendida (antecedentes)",
- "testHelp": "Una prueba corta dura unos 2 minutos. La prueba extendida se ejecuta en segundo plano y puede tardar varias horas en discos grandes. Recibirás una notificación cuando finalice.",
+ "extendedTest": "Prueba extendida",
+ "testHelp": "Una prueba corta dura unos 2 minutos. La prueba extendida se ejecuta en segundo plano y puede tardar varias horas en discos grandes. El resultado aparecerá en la pestaña Historial cuando finalice.",
"startFailed": "No se pudo iniciar la prueba",
"short": "Corto",
"extended": "Extendido",
@@ -316,7 +317,6 @@
"worst": "El peor",
"status": "Estado",
"viewFullReport": "Ver informe SMART completo",
- "reportHelp": "Genere un informe SMART detallado con análisis y recomendaciones.",
"loadingReport": "Cargando informe...",
"reportLoadFailed": "No se pudieron cargar los datos del informe.",
"statusValues": {
@@ -337,7 +337,7 @@
"yesterday": "Ayer",
"daysAgo": "Hace {count} días",
"downloadJson": "Descargar JSON",
- "delete": "Borrar",
+ "delete": "Eliminar",
"confirmDelete": "¿Eliminar este registro de prueba?",
"note": "Los resultados de las pruebas se almacenan localmente y se utilizan para generar informes SMART detallados."
},
@@ -945,7 +945,7 @@
"dhm": "{days}d {hours}h {minutes}m"
},
"details": {
- "sourceHost": "Fuente (anfitrión)",
+ "sourceHost": "Origen (host)",
"mountedAtCt": "Montado en (CT)",
"total": "Total",
"used": "Usado",
@@ -968,7 +968,7 @@
"dnsNameserver": "Servidor de nombres DNS",
"searchDomain": "Dominio de búsqueda",
"hostname": "Nombre de host",
- "storageType": "{type} almacenamiento",
+ "storageType": "Almacenamiento {type}",
"mountAttributes": "Atributos de montaje (configuración LXC)",
"runtimeMountOptions": "Opciones de montaje en tiempo de ejecución",
"privileged": "Privilegiado",
@@ -1009,10 +1009,10 @@
"preEnrolledKeys": "Claves preinscritas",
"serial": "De serie",
"mountTypes": {
- "pveVolume": "volumen PVE",
- "pveStorageBind": "unirse desde el almacenamiento PVE",
- "hostBind": "enlazar desde el host",
- "adHoc": "ad-hoc dentro de CT"
+ "pveVolume": "Volumen PVE",
+ "pveStorageBind": "Montado desde almacenamiento PVE",
+ "hostBind": "Montado desde el host",
+ "adHoc": "Ad-hoc dentro del CT"
},
"mountStatus": {
"stale": "duro",
@@ -1021,7 +1021,11 @@
"readOnly": "solo lectura",
"stopped": "Detenido",
"mounted": "montado"
- }
+ },
+ "startOnBoot": "Iniciar al arrancar",
+ "tags": "Etiquetas",
+ "tagsPlaceholder": "Añadir etiqueta…",
+ "tagsNone": "Sin etiquetas"
},
"logs": {
"header": "Registros para {name} (VMID: {vmid})",
@@ -1275,7 +1279,7 @@
"closePanel": "Cerrar panel",
"cancelButton": "Cancelar",
"saveButton": "Guardar",
- "hideButton": "Esconder",
+ "hideButton": "Ocultar",
"hidePermanentlyTooltip": "Ocultar esta detección permanentemente",
"hiddenBadge": "Actualmente oculto: volverá a aparecer en la lista de detección",
"registerDifferent": "Registrar una aplicación diferente",
@@ -1410,7 +1414,7 @@
"httpJsonHelp": "Punto final JSON público que devuelve una versión en algún lugar de la carga útil.",
"jsonPathHelp": "Ruta de puntos con índices de matriz [N] opcionales.",
"restoreButton": "Restaurar",
- "registerButton": "Registro",
+ "registerButton": "Registrar",
"removeButton": "Eliminar",
"checkButton": "Controlar",
"editFieldsButton": "Editar campos",
@@ -1421,7 +1425,9 @@
"notificationsEnabled": "Notificaciones de actualización activas — clic para silenciar",
"notificationsMuted": "Notificaciones de actualización silenciadas — clic para activar",
"notifyUpstreamLabel": "Notificarme cuando haya una nueva versión disponible",
- "notifyUpstreamHelp": "Envía `app_update_available` a los canales activos en Ajustes → Notificaciones. Desactívalo si esta app no se puede actualizar en tu instalación."
+ "notifyUpstreamHelp": "Envía `app_update_available` a los canales activos en Ajustes → Notificaciones. Desactívalo si esta app no se puede actualizar en tu instalación.",
+ "excludeFromBadgeLabel": "Excluir del contador de actualizaciones del LXC",
+ "excludeFromBadgeHelp": "No sumar esta app al contador agregado de actualizaciones del card del LXC. Útil cuando mantienes una versión concreta a propósito (requisito de tracker, compatibilidad). No afecta al estado que se muestra en la pestaña App ni al envío de la notificación."
}
},
"settings": {
@@ -2006,7 +2012,7 @@
"perm": "permanente",
"enabled": "Activado",
"disabled": "Desactivado",
- "enable": "Permitir",
+ "enable": "Activar",
"disable": "Desactivar",
"enabledLower": "activado",
"disabledLower": "desactivado",
@@ -2014,7 +2020,7 @@
"hostLower": "anfitrión",
"remove": "Eliminar",
"allow": "Permitir",
- "delete": "Borrar",
+ "delete": "Eliminar",
"uninstall": "Desinstalar",
"uninstalling": "Desinstalando...",
"serviceRunning": "Servicio en ejecución",
@@ -2124,7 +2130,7 @@
"confirmPassword": "Confirmar Contraseña",
"confirmPasswordPlaceholder": "Ingrese la contraseña nuevamente",
"enabling": "Habilitando...",
- "enableShort": "Permitir",
+ "enableShort": "Activar",
"changePassword": "Cambiar la contraseña",
"currentPassword": "Contraseña actual",
"currentPasswordPlaceholder": "Ingrese la contraseña actual",
@@ -2388,7 +2394,7 @@
"singleIp": "dirección IP",
"network": "Red",
"edit": "Editar",
- "delete": "Borrar",
+ "delete": "Eliminar",
"save": "Guardar cambios",
"trusted": "Confiable",
"system": "Sistema",
@@ -3001,6 +3007,7 @@
"currentFeatures": {
"hostUpdate": "Actualización del host con un solo clic desde Health Monitor. El nuevo botón Actualizar ahora en Actualizaciones del sistema ejecuta el flujo de actualización de Proxmox dentro de una terminal del tablero, sin salir del navegador.",
"mobileInstall": "Aviso de instalación en la aplicación para dispositivos móviles. Quienes visitan Safari por primera vez en Android e iOS ahora ven pasos sencillos para agregar el monitor a su pantalla de inicio como PWA.",
+ "i18n": "El Monitor ahora está traducido a 8 idiomas: inglés, español, alemán, francés, italiano, portugués, sueco y eslovaco. Muchas gracias a @vaso73 por crear la estructura de i18n que hizo esto posible.",
"pageSpeed": "Carga de páginas más rápida y navegación más fluida en todo el panel. La página de Inicio abre al instante y la página de VMs y LXC ya no muestra 'Cargando…' al abrir los modales de cada máquina.",
"appTab": "Nueva pestaña App dentro del modal de VM y LXC — especialmente para los LXC. Registra las aplicaciones instaladas en un contenedor, guarda sus enlaces web y recibe notificaciones cuando aparece una nueva versión.",
"updatesTab": "Pestaña Updates rediseñada para LXC: aplica las actualizaciones del SO y de las apps registradas desde un mismo botón, y programa una tarea de auto-actualización recurrente que revisa el SO del contenedor y la app registrada en cada ejecución.",
@@ -3327,7 +3334,7 @@
"edgeTpuRuntime": "Tiempo de ejecución de Edge TPU",
"thresholds": "Umbrales",
"hardwareWarnings": "Advertencias de hardware",
- "currentDraw": "Sorteo actual",
+ "currentDraw": "Consumo actual",
"currentOutput": "Salida actual",
"remote": "Remoto",
"batteryCharge": "Carga de la batería",
@@ -3361,7 +3368,7 @@
"linkSpeed": "Velocidad de enlace",
"family": "Familia",
"interface": "Interfaz",
- "rotationRate": "Tasa de rotación",
+ "rotationRate": "Velocidad de rotación",
"classCode": "Código de clase",
"serial": "De serie"
},
@@ -3506,11 +3513,11 @@
"back": "Atrás",
"change": "Cambiar",
"clear": "Limpiar",
- "delete": "Borrar",
+ "delete": "Eliminar",
"disable": "Desactivar",
"download": "Descargar",
"edit": "Editar",
- "enable": "Permitir",
+ "enable": "Activar",
"format": "Formato",
"generateKey": "Generar clave",
"import": "Importar",
@@ -3521,12 +3528,12 @@
"regenerate": "Regenerado",
"restore": "Restaurar",
"restoreSelected": "Restaurar seleccionado",
- "runNow": "Corre ahora",
+ "runNow": "Ejecutar",
"saveChanges": "Guardar cambios",
"unmount": "Desmontar",
"upload": "Subir",
"use": "Usar",
- "viewContents": "Ver contenidos"
+ "viewContents": "Ver contenido"
},
"archives": {
"archive": "Archivo",
@@ -4548,8 +4555,8 @@
},
"actions": {
"details": "Detalles",
- "history": "Historia",
- "dismiss": "Despedir"
+ "history": "Historial",
+ "dismiss": "Descartar"
},
"summary": {
"guests": "Huéspedes",
@@ -4579,7 +4586,6 @@
"tryDifferentSearch": "Pruebe con otro comando o revise la ortografía.",
"searchAnyCommand": "Buscar cualquier comando",
"trySearchingFor": "Intente buscar:",
- "searchTip": "Consejo: busque cualquier comando de Linux o Proxmox (qm, pct, zpool).",
"noExamplesFound": "No se encontraron ejemplos",
"reconnecting": "Reconectando…",
"reconnected": "Reconectado exitosamente",
diff --git a/AppImage/messages/fr/common.json b/AppImage/messages/fr/common.json
index cd361bd5..e75ea500 100644
--- a/AppImage/messages/fr/common.json
+++ b/AppImage/messages/fr/common.json
@@ -21,6 +21,7 @@
"cancel": "Annuler",
"save": "Sauvegarder",
"edit": "Modifier",
+ "remove": "",
"close": "Fermer",
"resetAll": "Tout réinitialiser",
"undo": "Défaire",
@@ -297,10 +298,10 @@
"installFailedManual": "L'installation a échoué. Essayez manuellement : apt-get install smartmontools nvme-cli",
"installToolsManual": "Échec de l'installation des outils. Essayez manuellement : apt-get install smartmontools nvme-cli",
"runTest": "Exécuter le test SMART",
- "shortTest": "Test court (~2 min)",
+ "shortTest": "Test court",
"longTest": "Test long (1 à 4 heures)",
- "extendedTest": "Test étendu (contexte)",
- "testHelp": "Un court test prend environ 2 minutes. Le test étendu s'exécute en arrière-plan et peut prendre plusieurs heures sur des disques volumineux. Vous recevrez une notification une fois terminé.",
+ "extendedTest": "Test étendu",
+ "testHelp": "",
"startFailed": "Échec du démarrage du test",
"short": "Court",
"extended": "Étendu",
@@ -316,7 +317,6 @@
"worst": "Pire",
"status": "Statut",
"viewFullReport": "Afficher le rapport SMART complet",
- "reportHelp": "Générez un rapport SMART détaillé avec analyse et recommandations.",
"loadingReport": "Chargement du rapport...",
"reportLoadFailed": "Échec du chargement des données du rapport.",
"statusValues": {
@@ -1021,7 +1021,11 @@
"readOnly": "en lecture seule",
"stopped": "arrêté",
"mounted": "monté"
- }
+ },
+ "startOnBoot": "",
+ "tags": "",
+ "tagsPlaceholder": "",
+ "tagsNone": ""
},
"logs": {
"header": "Journaux pour {name} (VMID : {vmid})",
@@ -1421,7 +1425,9 @@
"notificationsEnabled": "Notifications de mise à jour en amont activées – cliquez pour désactiver le son",
"notificationsMuted": "Notifications de mise à jour en amont MUTED – cliquez pour activer",
"notifyUpstreamLabel": "Me prévenir lorsqu'une nouvelle version en amont est disponible",
- "notifyUpstreamHelp": "envoie `app_update_available` aux canaux activés dans Paramètres → Notifications.Désactivez-la si cette application ne peut pas être mise à jour sur votre box."
+ "notifyUpstreamHelp": "envoie `app_update_available` aux canaux activés dans Paramètres → Notifications.Désactivez-la si cette application ne peut pas être mise à jour sur votre box.",
+ "excludeFromBadgeLabel": "",
+ "excludeFromBadgeHelp": ""
}
},
"settings": {
@@ -3001,6 +3007,7 @@
"currentFeatures": {
"hostUpdate": "Mise à jour de l'hôte en un clic depuis Health Monitor. Le nouveau bouton Mettre à jour maintenant dans les mises à jour système exécute le flux de mise à jour Proxmox dans un terminal de tableau de bord, sans quitter le navigateur.",
"mobileInstall": "Invite d'installation dans l'application pour mobile. Les nouveaux visiteurs sur Android et iOS Safari voient désormais des étapes simples pour ajouter le moniteur à leur écran d'accueil en tant que PWA.",
+ "i18n": "",
"pageSpeed": "chargements de pages plus rapides et navigation plus fluide dans le tableau de bord.La présentation s'ouvre instantanément et la page VM et LXC ne clignote plus jamais « Chargement… » entre les modaux invités.",
"appTab": "nouvel onglet Application dans le modal VM et LXC, en particulier pour les LXC.Enregistrez les applications installées dans un conteneur, capturez leurs liens Web et recevez des notifications lorsqu'une nouvelle version en amont est livrée.",
"updatesTab": "onglet Mises à jour retravaillées pour les LXC : appliquez les packages de système d'exploitation et les mises à jour des applications enregistrées à partir d'un seul bouton, et planifiez une tâche de mise à jour automatique récurrente qui vérifie le système d'exploitation du conteneur et son application suivie à chaque exécution.",
@@ -4579,7 +4586,6 @@
"tryDifferentSearch": "Essayez une autre commande ou vérifiez l'orthographe.",
"searchAnyCommand": "Rechercher n'importe quelle commande",
"trySearchingFor": "Essayez de rechercher :",
- "searchTip": "Astuce : recherchez n'importe quelle commande Linux ou Proxmox (qm, pct, zpool).",
"noExamplesFound": "Aucun exemple trouvé",
"reconnecting": "Reconnexion…",
"reconnected": "Reconnecté avec succès",
diff --git a/AppImage/messages/it/common.json b/AppImage/messages/it/common.json
index aa8a7948..b0fd704f 100644
--- a/AppImage/messages/it/common.json
+++ b/AppImage/messages/it/common.json
@@ -21,6 +21,7 @@
"cancel": "Cancellare",
"save": "Salva",
"edit": "Modificare",
+ "remove": "",
"close": "Vicino",
"resetAll": "Reimposta tutto",
"undo": "Disfare",
@@ -297,10 +298,10 @@
"installFailedManual": "Installazione non riuscita. Prova manualmente: apt-get install smartmontools nvme-cli",
"installToolsManual": "Impossibile installare gli strumenti. Prova manualmente: apt-get install smartmontools nvme-cli",
"runTest": "Esegui il test SMART",
- "shortTest": "Test breve (~2 minuti)",
+ "shortTest": "Test breve",
"longTest": "Test lungo (1-4 ore)",
- "extendedTest": "Test esteso (contesto)",
- "testHelp": "Un breve test dura circa 2 minuti. Il test esteso viene eseguito in background e può richiedere diverse ore su dischi di grandi dimensioni. Riceverai una notifica al termine.",
+ "extendedTest": "Test esteso",
+ "testHelp": "",
"startFailed": "Impossibile avviare il test",
"short": "Corto",
"extended": "Esteso",
@@ -316,7 +317,6 @@
"worst": "Peggio",
"status": "Stato",
"viewFullReport": "Visualizza il rapporto SMART completo",
- "reportHelp": "Genera un report SMART dettagliato con analisi e raccomandazioni.",
"loadingReport": "Caricamento rapporto...",
"reportLoadFailed": "Impossibile caricare i dati del rapporto.",
"statusValues": {
@@ -1021,7 +1021,11 @@
"readOnly": "sola lettura",
"stopped": "fermato",
"mounted": "montato"
- }
+ },
+ "startOnBoot": "",
+ "tags": "",
+ "tagsPlaceholder": "",
+ "tagsNone": ""
},
"logs": {
"header": "Registri per {name} (VMID: {vmid})",
@@ -1421,7 +1425,9 @@
"notificationsEnabled": "notifiche di aggiornamento upstream attivate: fai clic per disattivare l'audio",
"notificationsMuted": "notifiche di aggiornamento upstream MUTED: fare clic per abilitare",
"notifyUpstreamLabel": "avvisami quando è disponibile una nuova versione upstream",
- "notifyUpstreamHelp": "invia `app_update_available` ai canali abilitati in Impostazioni → Notifiche.Disattiva se questa app non può essere aggiornata sul tuo box."
+ "notifyUpstreamHelp": "invia `app_update_available` ai canali abilitati in Impostazioni → Notifiche.Disattiva se questa app non può essere aggiornata sul tuo box.",
+ "excludeFromBadgeLabel": "",
+ "excludeFromBadgeHelp": ""
}
},
"settings": {
@@ -3001,6 +3007,7 @@
"currentFeatures": {
"hostUpdate": "Aggiornamento host con un clic da Health Monitor. Il nuovo pulsante Aggiorna ora in Aggiornamenti di sistema esegue il flusso di aggiornamento di Proxmox all'interno di un terminale dashboard, senza uscire dal browser.",
"mobileInstall": "Richiesta di installazione in-app per dispositivi mobili. Chi visita per la prima volta Android e iOS Safari ora vede semplici passaggi per aggiungere il monitor alla propria schermata iniziale come PWA.",
+ "i18n": "",
"pageSpeed": "caricamenti delle pagine più rapidi e navigazione più fluida nella dashboard.La panoramica si apre immediatamente e la pagina VM e LXC non lampeggia mai più con la dicitura \"Caricamento in corso...\" tra le modalità guest.",
"appTab": "nuova scheda App all'interno della modalità VM e LXC, in particolare per LXC.Registra le app installate in un contenitore, acquisisci i relativi collegamenti web e ricevi notifiche quando viene fornita una nuova versione upstream.",
"updatesTab": "scheda Aggiornamenti rielaborati per LXC: applica pacchetti del sistema operativo e aggiornamenti delle app registrate da un singolo pulsante e pianifica un processo di aggiornamento automatico ricorrente che controlla il sistema operativo del contenitore e la relativa app monitorata a ogni esecuzione.",
@@ -4579,7 +4586,6 @@
"tryDifferentSearch": "Prova un altro comando o controlla l'ortografia.",
"searchAnyCommand": "Cerca qualsiasi comando",
"trySearchingFor": "Prova a cercare:",
- "searchTip": "Suggerimento: cerca qualsiasi comando Linux o Proxmox (qm, pct, zpool).",
"noExamplesFound": "Nessun esempio trovato",
"reconnecting": "Riconnessione…",
"reconnected": "Ricollegato con successo",
diff --git a/AppImage/messages/pt/common.json b/AppImage/messages/pt/common.json
index 3cc15f5e..2c1f9624 100644
--- a/AppImage/messages/pt/common.json
+++ b/AppImage/messages/pt/common.json
@@ -21,6 +21,7 @@
"cancel": "Cancelar",
"save": "Salvar",
"edit": "Editar",
+ "remove": "",
"close": "Fechar",
"resetAll": "Redefinir tudo",
"undo": "Desfazer",
@@ -297,10 +298,10 @@
"installFailedManual": "A instalação falhou. Tente manualmente: apt-get install smartmontools nvme-cli",
"installToolsManual": "Falha ao instalar ferramentas. Tente manualmente: apt-get install smartmontools nvme-cli",
"runTest": "Execute o teste SMART",
- "shortTest": "Teste curto (~2 min)",
+ "shortTest": "Teste curto",
"longTest": "Teste longo (1-4 horas)",
- "extendedTest": "Teste estendido (fundo)",
- "testHelp": "Um pequeno teste leva cerca de 2 minutos. O teste estendido é executado em segundo plano e pode levar várias horas em discos grandes. Você receberá uma notificação quando terminar.",
+ "extendedTest": "Teste estendido",
+ "testHelp": "",
"startFailed": "Falha ao iniciar o teste",
"short": "Curto",
"extended": "Estendido",
@@ -316,7 +317,6 @@
"worst": "Pior",
"status": "Status",
"viewFullReport": "Veja o relatório SMART completo",
- "reportHelp": "Gere um relatório SMART detalhado com análises e recomendações.",
"loadingReport": "Carregando relatório...",
"reportLoadFailed": "Falha ao carregar dados do relatório.",
"statusValues": {
@@ -1021,7 +1021,11 @@
"readOnly": "somente leitura",
"stopped": "parou",
"mounted": "montado"
- }
+ },
+ "startOnBoot": "",
+ "tags": "",
+ "tagsPlaceholder": "",
+ "tagsNone": ""
},
"logs": {
"header": "Registros para {name} (VMID: {vmid})",
@@ -1421,7 +1425,9 @@
"notificationsEnabled": "Notificações de atualização upstream ATIVADAS – clique para silenciar",
"notificationsMuted": "notificações de atualização upstream silenciadas – clique para ativar",
"notifyUpstreamLabel": "Notifique-me quando uma nova versão upstream estiver disponível",
- "notifyUpstreamHelp": "Envia `app_update_available` para os canais habilitados em Configurações → Notificações.Desligue se este aplicativo não puder ser atualizado em sua caixa."
+ "notifyUpstreamHelp": "Envia `app_update_available` para os canais habilitados em Configurações → Notificações.Desligue se este aplicativo não puder ser atualizado em sua caixa.",
+ "excludeFromBadgeLabel": "",
+ "excludeFromBadgeHelp": ""
}
},
"settings": {
@@ -3001,6 +3007,7 @@
"currentFeatures": {
"hostUpdate": "Atualização do host com um clique no Health Monitor. O novo botão Atualizar agora em Atualizações do sistema executa o fluxo de atualização do Proxmox dentro de um terminal de painel, sem sair do navegador.",
"mobileInstall": "Prompt de instalação no aplicativo para celular. Visitantes iniciantes no Android e iOS Safari agora veem etapas simples para adicionar o Monitor à tela inicial como um PWA.",
+ "i18n": "",
"pageSpeed": "carregamentos de página mais rápidos e navegação mais suave no painel.A visão geral abre instantaneamente e a página VMs e LXCs nunca mais exibe 'Carregando…' entre os modais convidados.",
"appTab": "Nova guia de aplicativo dentro do modal VM e LXC - especialmente para LXCs.Registre os aplicativos instalados em um contêiner, capture seus links da web e receba notificações quando uma nova versão upstream for enviada.",
"updatesTab": "guia Atualizações reformuladas para LXCs: aplique pacotes de sistema operacional e atualizações de aplicativos registrados a partir de um único botão e agende um trabalho de atualização automática recorrente que verifica o sistema operacional do contêiner e seu aplicativo rastreado em cada execução.",
@@ -4579,7 +4586,6 @@
"tryDifferentSearch": "Tente outro comando ou verifique a ortografia.",
"searchAnyCommand": "Procure qualquer comando",
"trySearchingFor": "Tente pesquisar por:",
- "searchTip": "Dica: Procure qualquer comando Linux ou Proxmox (qm, pct, zpool).",
"noExamplesFound": "Nenhum exemplo encontrado",
"reconnecting": "Reconectando…",
"reconnected": "Reconectado com sucesso",
diff --git a/AppImage/messages/sk/common.json b/AppImage/messages/sk/common.json
index 2b5e2648..351f8ee2 100644
--- a/AppImage/messages/sk/common.json
+++ b/AppImage/messages/sk/common.json
@@ -20,6 +20,7 @@
"cancel": "Zrušiť",
"save": "Uložiť",
"edit": "Upraviť",
+ "remove": "",
"close": "Zavrieť",
"resetAll": "Obnoviť všetko",
"undo": "Vrátiť späť",
@@ -296,10 +297,10 @@
"installFailedManual": "Inštalácia zlyhala. Skúste ručne: apt-get install smartmontools nvme-cli",
"installToolsManual": "Nástroje sa nepodarilo nainštalovať. Skúste ručne: apt-get install smartmontools nvme-cli",
"runTest": "Spustiť SMART test",
- "shortTest": "Krátky test (~2 min)",
+ "shortTest": "Krátky test",
"longTest": "Dlhý test (1-4 hodiny)",
- "extendedTest": "Rozšírený test (na pozadí)",
- "testHelp": "Krátky test trvá približne 2 minúty. Rozšírený test beží na pozadí a pri veľkých diskoch môže trvať aj niekoľko hodín. Po dokončení dostanete upozornenie.",
+ "extendedTest": "Rozšírený test",
+ "testHelp": "",
"startFailed": "Test sa nepodarilo spustiť",
"short": "Krátky",
"extended": "Rozšírený",
@@ -315,7 +316,6 @@
"worst": "Najhoršie",
"status": "Stav",
"viewFullReport": "Zobraziť celý SMART report",
- "reportHelp": "Vygeneruje podrobný SMART report s analýzou a odporúčaniami.",
"loadingReport": "Načítavam report...",
"reportLoadFailed": "Údaje pre report sa nepodarilo načítať.",
"statusValues": {
@@ -1020,7 +1020,11 @@
"readOnly": "iba na čítanie",
"stopped": "vypnuté",
"mounted": "pripojené"
- }
+ },
+ "startOnBoot": "",
+ "tags": "",
+ "tagsPlaceholder": "",
+ "tagsNone": ""
},
"logs": {
"header": "Logy pre {name} (VMID: {vmid})",
@@ -1420,7 +1424,9 @@
"notificationsEnabled": "Upozornenia na upstream aktualizácie sú ZAPNUTÉ – kliknutím ich stlmíte",
"notificationsMuted": "Upstream upozornenia na aktualizácie MUTED – kliknutím aktivujete",
"notifyUpstreamLabel": "Upozorniť ma, keď bude k dispozícii nová upstream verzia",
- "notifyUpstreamHelp": "Odošle `app_update_available` do kanálov povolených v Nastaveniach → Upozornenia.Vypnite, ak túto aplikáciu nie je možné aktualizovať na vašom boxe."
+ "notifyUpstreamHelp": "Odošle `app_update_available` do kanálov povolených v Nastaveniach → Upozornenia.Vypnite, ak túto aplikáciu nie je možné aktualizovať na vašom boxe.",
+ "excludeFromBadgeLabel": "",
+ "excludeFromBadgeHelp": ""
}
},
"settings": {
@@ -3000,6 +3006,7 @@
"currentFeatures": {
"hostUpdate": "Aktualizácia hosta jedným kliknutím z kontroly stavu. Nové tlačidlo Aktualizovať teraz v časti systémových aktualizácií spustí aktualizáciu Proxmoxu priamo v termináli dashboardu, bez odchodu z prehliadača.",
"mobileInstall": "Výzva na inštaláciu aplikácie v mobile. Prví návštevníci v Androide a iOS Safari uvidia jednoduchý spodný panel s krokmi na pridanie Monitoru na domovskú obrazovku.",
+ "i18n": "",
"pageSpeed": "Rýchlejšie načítanie stránok a plynulejšia navigácia na informačnom paneli.Prehľad sa otvorí okamžite a stránka VMs & LXCs už nikdy medzi hosťovskými modálmi nebliká „Načítava sa...“.",
"appTab": "Nová karta aplikácie vo vnútri modálu VM a LXC – najmä pre LXC.Zaregistrujte aplikácie nainštalované v kontajneri, zaznamenajte ich webové odkazy a získajte upozornenia, keď sa odošle nová upstream verzia.",
"updatesTab": "Karta Prepracované aktualizácie pre LXC: použite balíky OS a aktualizácie registrovaných aplikácií jediným tlačidlom a naplánujte si opakujúcu sa úlohu automatickej aktualizácie, ktorá skontroluje OS kontajnera a jeho sledovanú aplikáciu pri každom spustení.",
@@ -4579,7 +4586,6 @@
"tryDifferentSearch": "Skúste iný príkaz alebo skontrolujte preklepy.",
"searchAnyCommand": "Vyhľadajte ľubovoľný príkaz",
"trySearchingFor": "Môžete skúsiť:",
- "searchTip": "Tip: Vyhľadajte ľubovoľný príkaz pre Linux alebo Proxmox (qm, pct, zpool).",
"noExamplesFound": "Nenašli sa žiadne príklady",
"reconnecting": "Znova sa pripájam…",
"reconnected": "Pripojenie bolo obnovené",
diff --git a/AppImage/messages/sv/common.json b/AppImage/messages/sv/common.json
index d352af2f..aecf63ed 100644
--- a/AppImage/messages/sv/common.json
+++ b/AppImage/messages/sv/common.json
@@ -21,6 +21,7 @@
"cancel": "Avbryt",
"save": "Spara",
"edit": "Redigera",
+ "remove": "",
"close": "Stäng",
"resetAll": "Återställ alla",
"undo": "Ångra",
@@ -297,10 +298,10 @@
"installFailedManual": "Installationen misslyckades. Försök manuellt: apt-get install smartmontools nvme-cli",
"installToolsManual": "Det gick inte att installera verktyg. Försök manuellt: apt-get install smartmontools nvme-cli",
"runTest": "Kör SMART-test",
- "shortTest": "Kort test (~2 min)",
+ "shortTest": "Kort test",
"longTest": "Långt test (1-4 timmar)",
- "extendedTest": "Utökat test (bakgrund)",
- "testHelp": "Ett kort test tar cirka 2 minuter. Det utökade testet körs i bakgrunden och kan ta flera timmar på stora diskar. Du kommer att få ett meddelande när det är klart.",
+ "extendedTest": "Utökat test",
+ "testHelp": "",
"startFailed": "Det gick inte att starta testet",
"short": "Kort",
"extended": "Förlängd",
@@ -316,7 +317,6 @@
"worst": "Värst",
"status": "Status",
"viewFullReport": "Se hela SMART-rapporten",
- "reportHelp": "Skapa en detaljerad SMART-rapport med analyser och rekommendationer.",
"loadingReport": "Laddar rapport...",
"reportLoadFailed": "Det gick inte att läsa in rapportdata.",
"statusValues": {
@@ -1021,7 +1021,11 @@
"readOnly": "skrivskyddad",
"stopped": "stannade",
"mounted": "monterad"
- }
+ },
+ "startOnBoot": "",
+ "tags": "",
+ "tagsPlaceholder": "",
+ "tagsNone": ""
},
"logs": {
"header": "Loggar för {name} (VMID: {vmid})",
@@ -1421,7 +1425,9 @@
"notificationsEnabled": "Uppströmsuppdateringsmeddelanden PÅ – klicka för att stänga av ljudet",
"notificationsMuted": "Uppströmsuppdateringsmeddelanden AVSTÄLLD – klicka för att aktivera",
"notifyUpstreamLabel": "Meddela mig när en ny uppströmsversion är tillgänglig",
- "notifyUpstreamHelp": "Skickar `app_update_available` till de kanaler som är aktiverade i Inställningar → Aviseringar.Stäng av om den här appen inte kan uppdateras på din box."
+ "notifyUpstreamHelp": "Skickar `app_update_available` till de kanaler som är aktiverade i Inställningar → Aviseringar.Stäng av om den här appen inte kan uppdateras på din box.",
+ "excludeFromBadgeLabel": "",
+ "excludeFromBadgeHelp": ""
}
},
"settings": {
@@ -3001,6 +3007,7 @@
"currentFeatures": {
"hostUpdate": "Värduppdatering med ett klick från Health Monitor. Den nya knappen Uppdatera nu i Systemuppdateringar kör Proxmox-uppdateringsflödet i en instrumentpanelsterminal utan att lämna webbläsaren.",
"mobileInstall": "Uppmaning om installation i appen för mobil. Förstagångsbesökare på Android och iOS Safari ser nu enkla steg för att lägga till monitorn på sin startskärm som en PWA.",
+ "i18n": "",
"pageSpeed": "Snabbare sidladdningar och smidigare navigering över instrumentpanelen.Översikten öppnas omedelbart och sidan för virtuella datorer och LXC:er blinkar aldrig \"Laddar...\" mellan gästmodalerna igen.",
"appTab": "Ny appflik i VM- och LXC-modalerna — speciellt för LXC.Registrera apparna installerade i en behållare, fånga deras webblänkar och få meddelanden när en ny uppströmsversion skickas.",
"updatesTab": "Fliken Omarbetade uppdateringar för LXC:er: applicera OS-paket och uppdateringar av registrerade appar från en enda knapp och schemalägg ett återkommande automatiskt uppdateringsjobb som kontrollerar containerns OS och dess spårade app vid varje körning.",
@@ -4580,7 +4587,6 @@
"tryDifferentSearch": "Prova ett annat kommando eller kontrollera stavningen.",
"searchAnyCommand": "Sök efter valfritt kommando",
"trySearchingFor": "Prova att söka efter:",
- "searchTip": "Tips: Sök efter valfritt Linux- eller Proxmox-kommando (qm, pct, zpool).",
"noExamplesFound": "Inga exempel hittades",
"reconnecting": "Återansluter...",
"reconnected": "Återansluten framgångsrikt",
diff --git a/AppImage/scripts/flask_notification_routes.py b/AppImage/scripts/flask_notification_routes.py
index ede757a5..13ddc42f 100644
--- a/AppImage/scripts/flask_notification_routes.py
+++ b/AppImage/scripts/flask_notification_routes.py
@@ -808,12 +808,21 @@ def send_notification():
if not _validate_severity(severity):
return _bad_request('Invalid severity')
+ # Accept `title`/`message` either at the root of the payload
+ # or nested under `data` — the public docs show the nested
+ # form (`data.message`) as the primary example, so falling
+ # back to it prevents "empty title/message" custom events
+ # (issue #297).
+ payload_body = data.get('data') if isinstance(data.get('data'), dict) else {}
+ title = data.get('title') or payload_body.get('title') or ''
+ message = data.get('message') or payload_body.get('message') or ''
+
result = notification_manager.send_notification(
event_type=event_type,
severity=severity,
- title=data.get('title', ''),
- message=data.get('message', ''),
- data=data.get('data', {}),
+ title=title,
+ message=message,
+ data=payload_body,
source='api'
)
return jsonify(result)
diff --git a/AppImage/scripts/flask_server.py b/AppImage/scripts/flask_server.py
index 42cf3751..39bddc62 100644
--- a/AppImage/scripts/flask_server.py
+++ b/AppImage/scripts/flask_server.py
@@ -6196,7 +6196,12 @@ def get_proxmox_vms():
'netout': resource.get('netout', 0),
'diskread': resource.get('diskread', 0),
'diskwrite': resource.get('diskwrite', 0),
- 'maxcpu': resource.get('maxcpu', 0)
+ 'maxcpu': resource.get('maxcpu', 0),
+ # PVE tags carried straight through — the string
+ # comes back from `pvesh get /cluster/resources`
+ # already in PVE's own canonical `tag1;tag2`
+ # format; the client splits + colours them.
+ 'tags': resource.get('tags', ''),
}
# Decorate LXC rows with the apt update status if the
# managed_installs registry has it. Absent key means
@@ -6214,6 +6219,32 @@ def get_proxmox_vms():
if app_list:
vm_data['app_watches'] = app_list
+ # Fold registered-app updates into the CT's
+ # aggregate updates badge so the list card
+ # counter reflects OS + apps in one number.
+ # Apps flagged `exclude_from_badge` are
+ # omitted from the count (pinned versions,
+ # tracker-locked apps, etc.) — see the
+ # validator in lxc_apps.py for the full
+ # rationale. Independent from
+ # `notifications_enabled`.
+ if app_list:
+ app_upd_count = sum(
+ 1 for a in app_list
+ if a.get('update_available') is True
+ and not a.get('exclude_from_badge')
+ )
+ if app_upd_count:
+ uc = vm_data.get('update_check') or {}
+ # Synthesize a minimal update_check
+ # entry when the CT has no apt/apk
+ # data (OCI, non-Debian, checker off)
+ # but at least one counted app.
+ uc = dict(uc) if uc else {}
+ uc['count'] = int(uc.get('count') or 0) + app_upd_count
+ uc['available'] = True
+ vm_data['update_check'] = uc
+
# PVE's cluster resources API reports disk=0 for most
# QEMU VMs — it can't see inside the guest filesystem
# for the common storage backends. For running QEMU
@@ -14160,6 +14191,109 @@ def api_vm_firewall_log(vmid):
return jsonify({'error': str(e)}), 500
+@app.route('/api/vms//config', methods=['POST'])
+@require_auth
+def api_vm_config_set(vmid):
+ """Update a small allow-list of `.conf` fields on a VM or LXC.
+
+ Distinct from `api_vm_config_update` (PUT on the same path plus
+ /description) which is the legacy notes editor. Flask keys
+ endpoints by function name, so this one has its own.
+
+ Currently only `onboot` (start-with-host). Kept intentionally
+ narrow — the Status tab exposes one toggle for it and this
+ endpoint is what backs it. Adding a new field is one line in
+ ALLOWED + one line in the payload handler; every field must
+ map to a `qm set` / `pct set` --option that PVE applies
+ without a reboot.
+
+ Body: {"onboot": 0|1} (bool accepted too, coerced)
+
+ Returns 200 with the applied value on success; the modal cache
+ is invalidated so the next open renders the fresh state.
+ """
+ ALLOWED = {'onboot', 'tags'}
+ try:
+ data = request.get_json(silent=True) or {}
+ updates = {k: v for k, v in data.items() if k in ALLOWED}
+ if not updates:
+ return jsonify({'error': f'No allowed fields in body. Allowed: {sorted(ALLOWED)}'}), 400
+
+ # Coerce onboot to strict 0/1
+ if 'onboot' in updates:
+ v = updates['onboot']
+ if isinstance(v, bool):
+ v = 1 if v else 0
+ try:
+ v = int(v)
+ except (TypeError, ValueError):
+ return jsonify({'error': 'onboot must be 0 or 1'}), 400
+ if v not in (0, 1):
+ return jsonify({'error': 'onboot must be 0 or 1'}), 400
+ updates['onboot'] = v
+
+ # tags: canonicalise to PVE's `tag1;tag2;tag3` form.
+ # Accepts either a list (client-friendly) or an already-joined
+ # string. Reject anything with characters PVE would refuse
+ # (whitespace, backslash) — spaces inside a tag are the
+ # commonest slip and PVE just drops them silently, so we
+ # fail loud instead. Empty string clears all tags.
+ if 'tags' in updates:
+ v = updates['tags']
+ if isinstance(v, list):
+ parts = [str(t).strip() for t in v]
+ elif isinstance(v, str):
+ # Accept both ';' and ',' as separators, same as PVE
+ parts = [t.strip() for t in re.split(r'[;,]', v)]
+ else:
+ return jsonify({'error': 'tags must be a list or a string'}), 400
+ parts = [p for p in parts if p]
+ for p in parts:
+ if not re.match(r'^[a-zA-Z0-9._\-+]+$', p):
+ return jsonify({
+ 'error': f'Invalid tag "{p}": use letters, digits, and . _ - + only',
+ }), 400
+ updates['tags'] = ';'.join(parts)
+
+ # Resolve VM type + node from cluster resources cache
+ resources = get_cached_pvesh_cluster_resources_vm()
+ if not resources:
+ return jsonify({'error': 'Failed to enumerate cluster VMs'}), 500
+ vm_info = next((r for r in resources if r.get('vmid') == vmid), None)
+ if not vm_info:
+ return jsonify({'error': f'VM/LXC {vmid} not found'}), 404
+ vm_type = 'lxc' if vm_info.get('type') == 'lxc' else 'qemu'
+ node = vm_info.get('node', 'pve')
+
+ # `qm set` / `pct set` — hot-applied for onboot, no reboot
+ # needed. Build the argv from the ALLOWED map so a future
+ # extension of the payload naturally lands here.
+ binary = '/usr/sbin/pct' if vm_type == 'lxc' else '/usr/sbin/qm'
+ argv = [binary, 'set', str(vmid)]
+ for k, v in updates.items():
+ argv.extend([f'--{k}', str(v)])
+
+ result = subprocess.run(argv, capture_output=True, text=True, timeout=15)
+ if result.returncode != 0:
+ stderr = (result.stderr or result.stdout or '').strip()
+ return jsonify({
+ 'error': stderr[:500] or f'{binary} set failed with exit {result.returncode}',
+ }), 500
+
+ # Reflect the change in the modal cache immediately so the
+ # next open of the guest shows the new value without waiting
+ # for a natural refresh.
+ _vm_cache_invalidate(vmid, _vm_details_cache)
+
+ return jsonify({
+ 'success': True,
+ 'vmid': vmid,
+ 'applied': updates,
+ })
+ except Exception as e:
+ return jsonify({'error': str(e)}), 500
+
+
@app.route('/api/vms//control', methods=['POST'])
@require_auth
def api_vm_control(vmid):
diff --git a/AppImage/scripts/lxc_apps.py b/AppImage/scripts/lxc_apps.py
index 41ada3a4..1600737b 100644
--- a/AppImage/scripts/lxc_apps.py
+++ b/AppImage/scripts/lxc_apps.py
@@ -816,6 +816,19 @@ def validate_config(payload: dict) -> tuple[bool, Any]:
if ne is not None:
conf["notifications_enabled"] = bool(ne)
+ # Optional per-app switch for the CT's aggregate updates badge.
+ # Default is False (include). Set to True when the user knowingly
+ # keeps a specific version (e.g. qBittorrent pinned to the version
+ # their private tracker requires) and doesn't want the LXC list
+ # badge blinking about an "available" update that doesn't apply to
+ # them. Independent from `notifications_enabled` on purpose — a
+ # user may still want the outbound notification and just hide the
+ # counter, or the reverse. The App tab itself always shows the
+ # real state (purple update signal, editor version fields).
+ efb = payload.get("exclude_from_badge")
+ if efb is not None:
+ conf["exclude_from_badge"] = bool(efb)
+
return True, conf
@@ -1872,6 +1885,10 @@ def _summarise_app(app: dict) -> dict:
# this app.
"update_command": app.get("update_command") or "",
"hide_no_updater_notice": bool(app.get("hide_no_updater_notice")),
+ # Whether this app should be counted in the CT's aggregate
+ # updates badge (default: yes). See validator for full context.
+ "exclude_from_badge": bool(app.get("exclude_from_badge")),
+ "notifications_enabled": app.get("notifications_enabled", True) is not False,
# Community-scripts slug that the Register-chip flow attaches
# to the app. Surfaced so the Updates tab helper section can
# match this registered app against the CT's helper_slug and
@@ -2366,6 +2383,18 @@ def get_suggestions(vmid) -> dict:
break
meta = _helper_slug_meta(vmid) or {}
slug = meta.get("slug")
+ # Suppress base-OS helper slugs from the suggestion pipeline.
+ # community-scripts publishes bare-OS templates (alpine, debian,
+ # ubuntu, fedora, archlinux, gentoo, opensuse) under the same
+ # helpers_cache the App tab uses to seed detection, so a CT that
+ # only has the OS installed was showing up as "detected app:
+ # Alpine Linux" and inviting the user to register the OS as if
+ # it were an application. These are not trackable apps — treat
+ # the slug as absent for suggestion purposes so the panel goes
+ # straight to the empty state instead.
+ if slug in {"alpine", "archlinux", "archlinux-vm", "debian", "fedora", "gentoo", "opensuse", "ubuntu"}:
+ slug = None
+ meta = {}
# Tracking hint pipeline: catalog + curated hints merged.
# • catalog (community-scripts helpers_cache.json) covers ~430
# apps with name+repo+port+upstream_version, zero curation
diff --git a/AppImage/scripts/lxc_mount_points.py b/AppImage/scripts/lxc_mount_points.py
index c6ec0ebe..f583664a 100644
--- a/AppImage/scripts/lxc_mount_points.py
+++ b/AppImage/scripts/lxc_mount_points.py
@@ -583,10 +583,46 @@ def get_lxc_mount_points_static(vmid: str) -> dict[str, Any]:
"host_source_is_mountpoint": host_src["is_mountpoint"],
})
+ # Cheap hint so the client can render the Mount Points tab
+ # immediately for CTs that ONLY have ad-hoc NFS/CIFS mounts done
+ # from inside the container (nothing in .conf, so `out` is
+ # empty). Without this hint the tab appears only after the
+ # runtime endpoint returns 200-500 ms later, pushing the other
+ # tabs sideways. Reading /proc//mounts is a pure file read
+ # (~1 ms, no subprocess), filter by remote fs family so only
+ # storage counts — plain bind mounts of /dev/* passthrough
+ # devices don't inflate the count.
+ #
+ # IMPORTANT: exclude runtime targets that match a declared mp.
+ # When a host mp source is itself a remote share (e.g. mp0 binds
+ # /mnt/pve/Piblic which is a CIFS mount on the host), the same
+ # mount surfaces in /proc//mounts with an `nfs`/`cifs`
+ # fstype from the CT's perspective. Without the filter the hint
+ # double-counted it, so the badge showed mp+1 when the tab really
+ # only had `mp` cards to render.
+ ad_hoc_hint_count = 0
+ running, host_pid = _ct_status(vmid)
+ if running and host_pid:
+ try:
+ config_targets = {
+ entry.get("target", "")
+ for entry in config_entries
+ if entry.get("target")
+ }
+ for rt in _read_ct_proc_mounts(host_pid):
+ if not _REMOTE_FS_RE.match(rt.get("rt_fstype", "")):
+ continue
+ if rt.get("rt_target") in config_targets:
+ continue
+ ad_hoc_hint_count += 1
+ except Exception:
+ pass
+
return {
"ok": True,
"vmid": vmid,
"mount_points": out,
+ "ad_hoc_hint_count": ad_hoc_hint_count,
}
diff --git a/AppImage/scripts/notification_templates.py b/AppImage/scripts/notification_templates.py
index 543c7ee9..30bd520c 100644
--- a/AppImage/scripts/notification_templates.py
+++ b/AppImage/scripts/notification_templates.py
@@ -2409,7 +2409,20 @@ class AIEnhancer:
if title_match and body_match:
title_content = title_match.group(1).strip()
body_content = body_match.group(1).strip()
-
+
+ # Strip stray `[TITLE]` / `[BODY]` markers the AI may
+ # have echoed back inside the content itself (issue #297
+ # "additional note": PVE events arriving in Telegram
+ # with a literal `[TITLE]` in the title). The parser
+ # regex above splits on the FIRST occurrence, so any
+ # extra marker the model dropped into its title/body
+ # ends up inside the extracted string. Users see the
+ # markers verbatim in Telegram because they are only
+ # supposed to be structural separators, never content.
+ marker_re = re.compile(r'\[\s*(?:TITLE|BODY)\s*\]', re.IGNORECASE)
+ title_content = marker_re.sub('', title_content).strip()
+ body_content = marker_re.sub('', body_content).strip()
+
# Remove any "Original message/text" sections the AI might have added.
# Anchored at start-of-line (`(?:^|\n)\s*`) so legitimate prose
# like "we received the original message earlier" mid-paragraph
diff --git a/scripts/gpu_tpu/nvidia_installer.sh b/scripts/gpu_tpu/nvidia_installer.sh
index e6cc015f..02114978 100644
--- a/scripts/gpu_tpu/nvidia_installer.sh
+++ b/scripts/gpu_tpu/nvidia_installer.sh
@@ -60,21 +60,38 @@ initialize_cache
# ==========================================================
# GPU detection and current status
# ==========================================================
+# Populated by detect_nvidia_gpus. Holds every video-controller PCI
+# Device ID (lowercase, 4-hex) so the version filter can drop branches
+# whose supportedchips.html doesn't list every card on this host.
+NVIDIA_HOST_GPU_IDS=()
+
detect_nvidia_gpus() {
- # Only video controllers (not audio)
+ # Video controllers only — the paired HDA audio functions (10de:xxxx
+ # under class 0403) are not what the display driver ships support for.
local lspci_output
- lspci_output=$(lspci | grep -i "NVIDIA" \
+ lspci_output=$(lspci -nn | grep -i "NVIDIA" \
| grep -Ei "VGA compatible controller|3D controller|Display controller" || true)
if [[ -z "$lspci_output" ]]; then
NVIDIA_GPU_PRESENT=false
DETECTED_GPUS_TEXT="$(translate 'No NVIDIA GPU detected on this system.')"
+ NVIDIA_HOST_GPU_IDS=()
else
NVIDIA_GPU_PRESENT=true
DETECTED_GPUS_TEXT=""
+ NVIDIA_HOST_GPU_IDS=()
local i=1
while IFS= read -r line; do
DETECTED_GPUS_TEXT+=" ${i}. ${line}\n"
+ # Extract [10de:XXXX] — Vendor:Device pair. We keep only the
+ # Device half (4-hex) lowercased, which is what NVIDIA lists in
+ # each version's README/supportedchips.html.
+ local dev_id
+ dev_id=$(echo "$line" | grep -oiE '\[10de:[0-9a-f]{4}\]' | head -1 \
+ | sed -E 's/^\[10de:([0-9a-f]{4})\]$/\1/i' | tr 'A-F' 'a-f')
+ if [[ -n "$dev_id" ]]; then
+ NVIDIA_HOST_GPU_IDS+=("$dev_id")
+ fi
((i++))
done <<< "$lspci_output"
fi
@@ -759,6 +776,196 @@ KEYLASE_PATCH_CACHE="/var/cache/proxmenux/keylase_patch_versions.txt"
KEYLASE_PATCH_TTL_SECONDS=$((7 * 86400))
KEYLASE_PATCH_URL="https://raw.githubusercontent.com/keylase/nvidia-patch/master/patch.sh"
+# NVIDIA branch classification comes from the vendor's own Unix drivers
+# page, not the CDN — the CDN publishes every branch (production, new
+# feature, vulkan-beta, developer) in the same flat directory, whereas
+# the vendor page carries the current heads clearly labelled "Production
+# Branch", "New Feature Branch" and "Legacy GPU version". Extracting the
+# majors from those three lines gives us the set of branches NVIDIA
+# currently endorses for end users, with zero manual maintenance on our
+# side — when NVIDIA promotes a new rama the cache picks it up on the
+# next 24 h refresh. Cache is fail-open: if the fetch is blocked or the
+# page layout changes, we skip the branch filter rather than emptying
+# the picker.
+NVIDIA_BRANCHES_CACHE="/var/cache/proxmenux/nvidia_stable_branches.txt"
+NVIDIA_PRODUCTION_HEAD_CACHE="/var/cache/proxmenux/nvidia_production_head.txt"
+NVIDIA_BRANCH_HEADS_CACHE="/var/cache/proxmenux/nvidia_branch_heads.txt"
+NVIDIA_GPU_SUPPORT_CACHE_PREFIX="/var/cache/proxmenux/nvidia_gpu_support_"
+NVIDIA_BRANCHES_TTL_SECONDS=$((24 * 3600))
+NVIDIA_BRANCHES_URL="https://www.nvidia.com/en-us/drivers/unix/"
+
+refresh_nvidia_branches_cache() {
+ local now ts age
+ now=$(date +%s)
+ if [[ -f "$NVIDIA_BRANCHES_CACHE" ]]; then
+ ts=$(stat -c '%Y' "$NVIDIA_BRANCHES_CACHE" 2>/dev/null || echo 0)
+ age=$(( now - ts ))
+ if (( age < NVIDIA_BRANCHES_TTL_SECONDS )) && [[ -s "$NVIDIA_BRANCHES_CACHE" ]]; then
+ return 0
+ fi
+ fi
+ mkdir -p "$(dirname "$NVIDIA_BRANCHES_CACHE")" 2>/dev/null || return 1
+ local html tmp
+ html=$(curl -fsSL -A "Mozilla/5.0" --max-time 15 "$NVIDIA_BRANCHES_URL" 2>/dev/null) || return 1
+ [[ -z "$html" ]] && return 1
+ local clean tmp_full
+ clean=$(echo "$html" | perl -0777 -pe 's///gs' 2>/dev/null)
+ tmp=$(mktemp)
+ tmp_full=$(mktemp)
+ # Two label shapes on the page:
+ # • Production / New Feature → "… Branch Version:" then full.
+ # • Legacy → "Legacy GPU version (NNN.xx series):"
+ # then full — the word "Version"
+ # lives inside the parenthesised label
+ # so the Production/Feature regex misses
+ # it (separate alternative below).
+ # HTML comments are stripped first so vestigial `` blocks in older ia32 rows don't leak stale majors.
+ echo "$clean" \
+ | grep -oiE '(Production Branch Version|New Feature Branch Version|Legacy GPU version \([0-9]+\.xx series\)):[^<]*()?\s*]*>[0-9]+\.[0-9]+(\.[0-9]+)?' \
+ | grep -oE '>[0-9]+\.[0-9]+(\.[0-9]+)?' \
+ | tr -d '>' \
+ | awk -F. '{ printf "%s|%s\n", $1, $0 }' \
+ | sort -u -t'|' -k1,1 > "$tmp_full"
+ if [[ ! -s "$tmp_full" ]]; then
+ rm -f "$tmp" "$tmp_full"
+ return 1
+ fi
+ # Derive the majors-only file from the same source so both caches
+ # never disagree.
+ cut -d'|' -f1 "$tmp_full" | sort -un > "$tmp"
+ if [[ ! -s "$tmp" ]]; then
+ rm -f "$tmp" "$tmp_full"
+ return 1
+ fi
+ mv "$tmp" "$NVIDIA_BRANCHES_CACHE"
+ mv "$tmp_full" "$NVIDIA_BRANCH_HEADS_CACHE"
+
+ # Extra pass: capture the full Production Branch head so the picker
+ # can default to it instead of the highest numeric available (which
+ # could be a New Feature Branch head — NVIDIA doesn't recommend those
+ # as the general-purpose default). Best-effort.
+ local prod_head
+ prod_head=$(echo "$clean" \
+ | grep -oiE 'Production Branch Version:[^<]*()?\s*]*>[0-9]+\.[0-9]+(\.[0-9]+)?' \
+ | grep -oE '>[0-9]+\.[0-9]+(\.[0-9]+)?' \
+ | tr -d '>' \
+ | head -n1)
+ if [[ -n "$prod_head" ]]; then
+ echo "$prod_head" > "$NVIDIA_PRODUCTION_HEAD_CACHE"
+ else
+ rm -f "$NVIDIA_PRODUCTION_HEAD_CACHE" 2>/dev/null || true
+ fi
+ return 0
+}
+
+# Return the full head version associated with a major from the
+# branch-heads cache (e.g. `get_nvidia_branch_head 595` → 595.91.07).
+# Used to know which release inside a branch to hit for the PCI-ID
+# supported-GPUs list.
+get_nvidia_branch_head() {
+ local major="$1"
+ [[ -f "$NVIDIA_BRANCH_HEADS_CACHE" && -s "$NVIDIA_BRANCH_HEADS_CACHE" ]] || return 1
+ awk -F'|' -v m="$major" '$1 == m { print $2; exit }' "$NVIDIA_BRANCH_HEADS_CACHE"
+}
+
+# Refresh the per-branch supported-GPU cache. Uses the branch head as
+# the "sample release" for the whole branch — NVIDIA rarely drops chip
+# support inside a live branch, so this is a solid proxy that also
+# minimises fetch count (~3 heads total instead of one per release).
+# Written to nvidia_gpu_support_MAJOR.txt with one lowercase hex device
+# id per line. Same 24h TTL as the branches cache.
+refresh_nvidia_gpu_support_for_major() {
+ local major="$1"
+ [[ -z "$major" ]] && return 1
+ local cache="${NVIDIA_GPU_SUPPORT_CACHE_PREFIX}${major}.txt"
+ local now ts age
+ now=$(date +%s)
+ if [[ -f "$cache" ]]; then
+ ts=$(stat -c '%Y' "$cache" 2>/dev/null || echo 0)
+ age=$(( now - ts ))
+ if (( age < NVIDIA_BRANCHES_TTL_SECONDS )) && [[ -s "$cache" ]]; then
+ return 0
+ fi
+ fi
+ local head_ver
+ head_ver=$(get_nvidia_branch_head "$major") || return 1
+ [[ -z "$head_ver" ]] && return 1
+ local url="https://download.nvidia.com/XFree86/Linux-x86_64/${head_ver}/README/supportedchips.html"
+ local html tmp
+ html=$(curl -fsSL -A "Mozilla/5.0" --max-time 20 "$url" 2>/dev/null) || return 1
+ [[ -z "$html" ]] && return 1
+ mkdir -p "$(dirname "$cache")" 2>/dev/null || return 1
+ tmp=$(mktemp)
+ # NVIDIA's supportedchips.html lays out each GPU row as a with
+ # the PCI Device ID in 4-char hex. Anchor on the surrounding tag so
+ # we don't sweep up unrelated 4-hex strings elsewhere in the page.
+ echo "$html" \
+ | grep -oiE ' [0-9A-F]{4} ' \
+ | grep -oiE '[0-9A-F]{4}' \
+ | tr 'A-F' 'a-f' \
+ | sort -u > "$tmp"
+ if [[ -s "$tmp" ]]; then
+ mv "$tmp" "$cache"
+ return 0
+ fi
+ rm -f "$tmp"
+ return 1
+}
+
+# True if every detected NVIDIA GPU on this host has its device id in
+# the branch's supported list. Fail-open when the cache is missing so
+# a network hiccup never locks the picker out.
+is_branch_compatible_with_host_gpus() {
+ local major="$1"
+ local cache="${NVIDIA_GPU_SUPPORT_CACHE_PREFIX}${major}.txt"
+ [[ -f "$cache" && -s "$cache" ]] || return 0
+ [[ ${#NVIDIA_HOST_GPU_IDS[@]} -eq 0 ]] && return 0
+ local id
+ for id in "${NVIDIA_HOST_GPU_IDS[@]}"; do
+ grep -qFx "$id" "$cache" || return 1
+ done
+ return 0
+}
+
+# Reject Vulkan-beta / short-lived / developer branches by counting
+# how many releases NVIDIA actually shipped inside that major on the
+# CDN. Production and long-lived New Feature branches accumulate many
+# release rows (470=17, 535=20, 550=14, 570=12, 580=14 …); Vulkan-beta
+# and developer branches only ever get 1-4 releases before being
+# superseded (590=2, 565=2, 530=2, 555=4 …). Threshold 5 separates the
+# two groups cleanly at time of writing.
+# Fail-open: if the release-count map hasn't been built for whatever
+# reason, the branch passes (kernel + GPU-compat + curated whitelist
+# are still enforced upstream). The map is populated once per
+# `filter_option_c_branch` invocation, so no repeated CDN scraping.
+NVIDIA_BRANCH_MIN_RELEASES=5
+declare -A NVIDIA_BRANCH_RELEASE_COUNT=()
+
+is_branch_release_count_sufficient() {
+ local major="$1"
+ [[ -z "$major" ]] && return 1
+ [[ ${#NVIDIA_BRANCH_RELEASE_COUNT[@]} -eq 0 ]] && return 0
+ local n="${NVIDIA_BRANCH_RELEASE_COUNT[$major]:-0}"
+ (( n >= NVIDIA_BRANCH_MIN_RELEASES ))
+}
+
+get_nvidia_production_head() {
+ [[ -f "$NVIDIA_PRODUCTION_HEAD_CACHE" && -s "$NVIDIA_PRODUCTION_HEAD_CACHE" ]] || return 1
+ local v
+ v=$(head -n1 "$NVIDIA_PRODUCTION_HEAD_CACHE" | tr -d '[:space:]')
+ [[ -z "$v" ]] && return 1
+ printf '%s\n' "$v"
+}
+
+is_nvidia_stable_branch() {
+ local major="$1"
+ [[ -z "$major" ]] && return 1
+ # Fail-open: no cache → don't filter (upstream behaviour preserved).
+ [[ -f "$NVIDIA_BRANCHES_CACHE" && -s "$NVIDIA_BRANCHES_CACHE" ]] || return 0
+ grep -qFx "$major" "$NVIDIA_BRANCHES_CACHE"
+}
+
refresh_keylase_patch_cache() {
local now ts age
now=$(date +%s)
@@ -822,20 +1029,86 @@ filter_option_c_branch() {
return 0
fi
- # Accept the target branch AND any newer branch (major ≥ target).
- # Historical behaviour was an exact-major match, which locked kernel
- # 7.x users to 580.x only. When a 580.x build happens to fail to
- # compile on a very recent kernel + toolchain combo (reproduced on
- # kernel 7.0.14-4-pve — see issue #248), the operator had no
- # in-menu escape. `MIN_DRIVER_VERSION` from get_kernel_compatibility_info
- # still gates the floor, so this only opens the ceiling: newer stable
- # branches like 590 / 595 / 600 that satisfy the min version become
- # selectable, while ancient branches remain filtered out.
+ # Four-way gate for every candidate version:
+ # 1. `major >= target_branch` — kernel floor.
+ # 2. Branch is currently endorsed on NVIDIA's Unix drivers page
+ # (Production / New Feature / Legacy heads) OR was substantial
+ # enough to accumulate ≥ NVIDIA_BRANCH_MIN_RELEASES releases on
+ # the CDN. The endorsement path always passes; the release-count
+ # path lets superseded production branches (580, 570, 550, 535 …
+ # still maintained via bugfix releases) stay selectable while
+ # Vulkan-beta / developer branches with 1-4 releases (590, 565,
+ # 530 …) get dropped.
+ # 3. `is_branch_compatible_with_host_gpus` — every detected NVIDIA
+ # GPU on this host must appear in that branch's supportedchips
+ # list. A host with a Kepler card ends up with 470.x only.
+ # All three fail open when their caches / lookups miss, so the picker
+ # never empties on a network glitch.
+ refresh_nvidia_branches_cache 2>/dev/null || true
+ # Build a majors→count map from the incoming version list. This is
+ # what backs `is_branch_release_count_sufficient` — done once per
+ # call so the tight loop below stays local-arithmetic only.
+ NVIDIA_BRANCH_RELEASE_COUNT=()
+ while IFS= read -r _v; do
+ [[ -z "$_v" ]] && continue
+ local _m="${_v%%.*}"
+ NVIDIA_BRANCH_RELEASE_COUNT[$_m]=$(( ${NVIDIA_BRANCH_RELEASE_COUNT[$_m]:-0} + 1 ))
+ done <<< "$versions_in"
+ # Grab the head (highest version) of every major so we know which
+ # release to sample for supportedchips.html. We use the CDN listing
+ # directly for this — the branch-heads cache only carries the
+ # endorsed heads, not the superseded ones.
+ declare -A _major_head=()
+ while IFS= read -r _v; do
+ [[ -z "$_v" ]] && continue
+ local _m="${_v%%.*}"
+ [[ -z "${_major_head[$_m]:-}" ]] && _major_head[$_m]="$_v"
+ done < <(printf '%s\n' "$versions_in")
+ # Warm the supported-GPU cache for every stable major (whitelist
+ # heads: head already known → normal path; superseded heads: seed the
+ # cache-file's head-version by directly writing a lightweight lookup).
+ # For endorsed majors we can use refresh_nvidia_gpu_support_for_major
+ # as-is (it looks up NVIDIA_BRANCH_HEADS_CACHE). For non-endorsed
+ # majors we need to fetch supportedchips.html against the highest
+ # release we saw in the CDN listing.
+ local _m _head _cache _now _ts _age _html _tmp
+ _now=$(date +%s)
+ for _m in "${!_major_head[@]}"; do
+ _head="${_major_head[$_m]}"
+ _cache="${NVIDIA_GPU_SUPPORT_CACHE_PREFIX}${_m}.txt"
+ if [[ -f "$_cache" ]]; then
+ _ts=$(stat -c '%Y' "$_cache" 2>/dev/null || echo 0)
+ _age=$(( _now - _ts ))
+ if (( _age < NVIDIA_BRANCHES_TTL_SECONDS )) && [[ -s "$_cache" ]]; then
+ continue
+ fi
+ fi
+ _html=$(curl -fsSL -A "Mozilla/5.0" --max-time 20 \
+ "https://download.nvidia.com/XFree86/Linux-x86_64/${_head}/README/supportedchips.html" \
+ 2>/dev/null) || continue
+ [[ -z "$_html" ]] && continue
+ mkdir -p "$(dirname "$_cache")" 2>/dev/null || continue
+ _tmp=$(mktemp)
+ echo "$_html" \
+ | grep -oiE '[0-9A-F]{4} ' \
+ | grep -oiE '[0-9A-F]{4}' \
+ | tr 'A-F' 'a-f' \
+ | sort -u > "$_tmp"
+ if [[ -s "$_tmp" ]]; then
+ mv "$_tmp" "$_cache"
+ else
+ rm -f "$_tmp"
+ fi
+ done
while IFS= read -r ver; do
[[ -z "$ver" ]] && continue
local ver_major="${ver%%.*}"
if (( 10#$ver_major >= 10#$target_branch )); then
- printf '%s\n' "$ver"
+ if is_nvidia_stable_branch "$ver_major" || is_branch_release_count_sufficient "$ver_major"; then
+ if is_branch_compatible_with_host_gpus "$ver_major"; then
+ printf '%s\n' "$ver"
+ fi
+ fi
fi
done <<< "$versions_in"
}
@@ -1359,16 +1632,16 @@ show_version_menu() {
current_list=$(filter_option_c_branch "$current_list" "$CURRENT_DRIVER_VERSION" "$RECOMMENDED_BRANCH")
fi
- if [[ -n "$latest" ]]; then
- local filtered_max_list=""
- while IFS= read -r ver; do
- [[ -z "$ver" ]] && continue
- if version_le "$ver" "$latest"; then
- filtered_max_list+="$ver"$'\n'
- fi
- done <<< "$current_list"
- current_list="$filtered_max_list"
- fi
+ # Historically the picker capped candidates at `latest` (from the
+ # CDN's `latest.txt`) so users never saw versions newer than the
+ # global "latest". But latest.txt lags the Production Branch head
+ # (595.91.07 today vs 595.84 in latest.txt) and also hides the New
+ # Feature Branch head (610.x) that is a legitimate option once
+ # kernel + GPU compat pass. Kernel floor, endorsement whitelist,
+ # release-count heuristic and GPU-compat filter already narrow the
+ # list to safe candidates; the Production head still stands out in
+ # the "Latest available" recommendation, so an artificial ceiling
+ # only masked valid options.
# If the user has the keylase NVENC patch applied, only offer versions
# that the patch supports — picking an unsupported version reinstalls
@@ -1391,16 +1664,50 @@ show_version_menu() {
fi
fi
- # Recompute "latest" as the highest version still in the filtered list
- # so the menu's "Latest available" label matches what we actually offer
- # rather than the global upstream latest (which may have been filtered
- # out by Option C / kernel-compat / patch awareness).
- if [[ -n "$current_list" ]]; then
+ # Pick the default "Recommended" version. Three-tier priority so the
+ # picker stays consistent with what the Monitor's driver-update
+ # notification promised the user:
+ # 1. If a driver is already installed AND its branch is still
+ # offered in the filtered list, recommend the highest release
+ # of that same branch (bugfix upgrade in place). Matches the
+ # Monitor's Hardware card, which surfaces "v580.178.04
+ # available" for a 580.x install — the user hitting Actualizar
+ # then expects to land on 580.178.04, not a cross-branch jump
+ # to Production. Cross-branch is still one row away in the
+ # list.
+ # 2. Fresh install (no current driver) → Production Branch head
+ # from NVIDIA's Unix drivers page, when present in the list.
+ # 3. Fallback → highest numeric in the list (Production may have
+ # been filtered out by kernel-compat / GPU-compat / patch
+ # awareness).
+ latest=""
+ if [[ -n "$CURRENT_DRIVER_VERSION" && -n "$current_list" ]]; then
+ local _cur_branch="${CURRENT_DRIVER_VERSION%%.*}"
+ if [[ -n "$_cur_branch" ]]; then
+ local _same_branch_head
+ _same_branch_head=$(printf '%s\n' "$current_list" \
+ | awk -F. -v b="$_cur_branch" '$1 == b { print; exit }' \
+ | tr -d '[:space:]')
+ if [[ -n "$_same_branch_head" ]]; then
+ latest="$_same_branch_head"
+ fi
+ fi
+ fi
+ if [[ -z "$latest" ]]; then
+ local prod_head=""
+ prod_head=$(get_nvidia_production_head 2>/dev/null) || prod_head=""
+ if [[ -n "$prod_head" && -n "$current_list" ]]; then
+ if printf '%s\n' "$current_list" | grep -qFx "$prod_head"; then
+ latest="$prod_head"
+ fi
+ fi
+ fi
+ if [[ -z "$latest" && -n "$current_list" ]]; then
latest=$(printf '%s\n' "$current_list" | head -n1 | tr -d '[:space:]')
fi
local menu_text="$(translate 'Select the NVIDIA driver version to install:')\n\n"
- menu_text+="$(translate 'Versions shown are compatible with your kernel. Latest available is recommended in most cases.')"
+ menu_text+="$(translate 'Versions shown are compatible with your kernel and your GPU. The recommended version keeps you on your current driver branch, or defaults to the NVIDIA Production Branch head on a fresh install.')"
if $patch_filtered; then
menu_text+="\n\n$(translate 'NVENC patch detected — list narrowed to versions supported by keylase/nvidia-patch.')"
elif [[ -n "$patch_filter_note" ]]; then
@@ -1408,7 +1715,7 @@ show_version_menu() {
fi
local choices=()
- choices+=("latest" "$(translate 'Latest available') (${latest:-unknown})")
+ choices+=("latest" "$(translate 'Recommended') (${latest:-unknown})")
choices+=("" "")
if [[ -n "$current_list" ]]; then
diff --git a/web/app/[locale]/docs/monitor/dashboard/vms-lxcs/app/page.tsx b/web/app/[locale]/docs/monitor/dashboard/vms-lxcs/app/page.tsx
index d4f84f8e..e4591f3b 100644
--- a/web/app/[locale]/docs/monitor/dashboard/vms-lxcs/app/page.tsx
+++ b/web/app/[locale]/docs/monitor/dashboard/vms-lxcs/app/page.tsx
@@ -63,6 +63,7 @@ export default async function AppTabPage({
}
state: { items: string[] }
manage: { items: string[] }
+ options: { items: string[] }
notDetected: { steps: string[] }
} } } }
}
@@ -82,6 +83,7 @@ export default async function AppTabPage({
const step6CorrectItems = v.tracking.step6CorrectItems
const stateItems = v.state.items
const manageItems = v.manage.items
+ const optionsItems = v.options.items
const notDetectedSteps = v.notDetected.steps
// Rich-text tag handlers
@@ -374,6 +376,15 @@ export default async function AppTabPage({
{t.rich("manage.trailing", { strong, em, code })}
+ {t("options.heading")}
+ {t("options.lead")}
+
+ {optionsItems.map((_, idx) => (
+ - {t.rich(`options.items.${idx}`, { strong, em, code })}
+ ))}
+
+ {t.rich("options.trailing", { strong, em, code })}
+
{t("notDetected.heading")}
{t("notDetected.intro")}
diff --git a/web/messages/en/docs/monitor/dashboard/vms-lxcs-app.json b/web/messages/en/docs/monitor/dashboard/vms-lxcs-app.json
index 3465a99d..97ed6d1a 100644
--- a/web/messages/en/docs/monitor/dashboard/vms-lxcs-app.json
+++ b/web/messages/en/docs/monitor/dashboard/vms-lxcs-app.json
@@ -257,6 +257,15 @@
],
"trailing": "Deleting a record does not uninstall or stop the app. It only removes the information ProxMenux uses to display it and monitor its version."
},
+ "options": {
+ "heading": "Optional toggles",
+ "lead": "Two independent switches sit under the version tracking options:",
+ "items": [
+ "Notify me when a new upstream version is available — sends the app_update_available event to the channels enabled in Settings → Notifications.",
+ "Exclude from the LXC updates counter — leaves this app out of the aggregate updates badge shown on the LXC list card."
+ ],
+ "trailing": "Both toggles can be set independently. The App tab still shows the real state of each registered app regardless of these choices."
+ },
"notDetected": {
"heading": "If the app is not detected",
"intro": "Automatic detection is not required to use this feature. If no suggestion appears:",
diff --git a/web/messages/es/docs/monitor/dashboard/vms-lxcs-app.json b/web/messages/es/docs/monitor/dashboard/vms-lxcs-app.json
index 68fd3129..781a2006 100644
--- a/web/messages/es/docs/monitor/dashboard/vms-lxcs-app.json
+++ b/web/messages/es/docs/monitor/dashboard/vms-lxcs-app.json
@@ -257,6 +257,15 @@
],
"trailing": "Eliminar el registro no desinstala ni detiene la aplicación. Solo borra la información que ProxMenux utiliza para mostrarla y supervisar su versión."
},
+ "options": {
+ "heading": "Opciones adicionales",
+ "lead": "Debajo de las opciones de seguimiento de versión hay dos casillas independientes:",
+ "items": [
+ "Notificarme cuando haya una nueva versión disponible — envía el evento app_update_available a los canales activos en Ajustes → Notificaciones.",
+ "Excluir del contador de actualizaciones del LXC — no suma esta aplicación al badge agregado de actualizaciones del card del LXC."
+ ],
+ "trailing": "Ambas casillas se marcan por separado. La pestaña App sigue mostrando el estado real de cada aplicación registrada al margen de esta elección."
+ },
"notDetected": {
"heading": "Si la aplicación no se detecta",
"intro": "La detección automática no es necesaria para utilizar esta función. Si no aparece ninguna sugerencia:",
diff --git a/web/messages/sk/docs/monitor/dashboard/vms-lxcs-app.json b/web/messages/sk/docs/monitor/dashboard/vms-lxcs-app.json
index 557dd324..75675282 100644
--- a/web/messages/sk/docs/monitor/dashboard/vms-lxcs-app.json
+++ b/web/messages/sk/docs/monitor/dashboard/vms-lxcs-app.json
@@ -121,14 +121,38 @@
"colMethod": "Metóda",
"colWhen": "Kedy ju použiť",
"rows": [
- { "method": "Žiadna (iba odkaz)", "when": "Potrebujete len názov a webové odkazy." },
- { "method": "dpkg balík", "when": "Aplikácia je nainštalovaná ako Debian alebo Ubuntu balík." },
- { "method": "apk balík", "when": "Aplikácia je nainštalovaná ako Alpine balík." },
- { "method": "Binárka", "when": "Spustiteľný súbor vracia verziu cez argument ako --version." },
- { "method": "Súbor + regex", "when": "Reťazec verzie je zapísaný v súbore." },
- { "method": "Python distribúcia", "when": "Aplikácia je nainštalovaná ako Python balík." },
- { "method": "Príkaz", "when": "Na získanie verzie treba spustiť konkrétny príkaz." },
- { "method": "Ručne", "when": "Používateľ zadá nainštalovanú verziu ručne." }
+ {
+ "method": "Žiadna (iba odkaz)",
+ "when": "Potrebujete len názov a webové odkazy."
+ },
+ {
+ "method": "dpkg balík",
+ "when": "Aplikácia je nainštalovaná ako Debian alebo Ubuntu balík."
+ },
+ {
+ "method": "apk balík",
+ "when": "Aplikácia je nainštalovaná ako Alpine balík."
+ },
+ {
+ "method": "Binárka",
+ "when": "Spustiteľný súbor vracia verziu cez argument ako --version."
+ },
+ {
+ "method": "Súbor + regex",
+ "when": "Reťazec verzie je zapísaný v súbore."
+ },
+ {
+ "method": "Python distribúcia",
+ "when": "Aplikácia je nainštalovaná ako Python balík."
+ },
+ {
+ "method": "Príkaz",
+ "when": "Na získanie verzie treba spustiť konkrétny príkaz."
+ },
+ {
+ "method": "Ručne",
+ "when": "Používateľ zadá nainštalovanú verziu ručne."
+ }
]
},
"methodsTrailing": "Použite čo najpriamejšiu a najstabilnejšiu metódu. Ak aplikácia pochádza zo systémového balíka, uprednostnite dotaz na balík pred parsovaním výstupu všeobecného príkazu.",
@@ -178,12 +202,30 @@
"colPart": "Časť",
"colMeaning": "Význam",
"rows": [
- { "part": "version", "meaning": "Ukotví hľadanie na tomto slove, aby sa nezhodovalo nesúvisiace číslo." },
- { "part": "[ :=]+", "meaning": "Povolí jednu alebo viac medzier, dvojbodiek alebo znamienok rovnosti." },
- { "part": "v?", "meaning": "Písmeno v sa môže objaviť raz alebo vôbec." },
- { "part": "( and )", "meaning": "Označuje časť, ktorú má ProxMenux ponechať." },
- { "part": "[0-9]+", "meaning": "Zodpovedá jednej alebo viacerým čísliciam." },
- { "part": "\\.", "meaning": "Zodpovedá skutočnej bodke medzi číslami." }
+ {
+ "part": "version",
+ "meaning": "Ukotví hľadanie na tomto slove, aby sa nezhodovalo nesúvisiace číslo."
+ },
+ {
+ "part": "[ :=]+",
+ "meaning": "Povolí jednu alebo viac medzier, dvojbodiek alebo znamienok rovnosti."
+ },
+ {
+ "part": "v?",
+ "meaning": "Písmeno v sa môže objaviť raz alebo vôbec."
+ },
+ {
+ "part": "( and )",
+ "meaning": "Označuje časť, ktorú má ProxMenux ponechať."
+ },
+ {
+ "part": "[0-9]+",
+ "meaning": "Zodpovedá jednej alebo viacerým čísliciam."
+ },
+ {
+ "part": "\\.",
+ "meaning": "Zodpovedá skutočnej bodke medzi číslami."
+ }
]
},
"step2DotNote": "Bodka sa píše ako \\., pretože samotná bodka v regexe znamená „ľubovoľný znak“.",
@@ -194,11 +236,31 @@
"colRegex": "Odporúčaný regex",
"colResult": "Výsledok",
"rows": [
- { "text": "v2.14.3", "regex": "v?([0-9]+\\.[0-9]+\\.[0-9]+)", "result": "2.14.3" },
- { "text": "Version: 2.14", "regex": "Version[ :=]+v?([0-9]+(?:\\.[0-9]+){1,3})", "result": "2.14" },
- { "text": "release-2.14.3.1", "regex": "release-v?([0-9]+(?:\\.[0-9]+){1,3})", "result": "2.14.3.1" },
- { "text": "build 2026.08.10", "regex": "build[ :=]+([0-9]{4}\\.[0-9]{1,2}\\.[0-9]{1,2})", "result": "2026.08.10" },
- { "text": "{\"version\":\"2.14.3\"}", "regex": "\"version\"\\s*:\\s*\"v?([0-9]+(?:\\.[0-9]+){1,3})\"", "result": "2.14.3" }
+ {
+ "text": "v2.14.3",
+ "regex": "v?([0-9]+\\.[0-9]+\\.[0-9]+)",
+ "result": "2.14.3"
+ },
+ {
+ "text": "Version: 2.14",
+ "regex": "Version[ :=]+v?([0-9]+(?:\\.[0-9]+){1,3})",
+ "result": "2.14"
+ },
+ {
+ "text": "release-2.14.3.1",
+ "regex": "release-v?([0-9]+(?:\\.[0-9]+){1,3})",
+ "result": "2.14.3.1"
+ },
+ {
+ "text": "build 2026.08.10",
+ "regex": "build[ :=]+([0-9]{4}\\.[0-9]{1,2}\\.[0-9]{1,2})",
+ "result": "2026.08.10"
+ },
+ {
+ "text": "{\"version\":\"2.14.3\"}",
+ "regex": "\"version\"\\s*:\\s*\"v?([0-9]+(?:\\.[0-9]+){1,3})\"",
+ "result": "2.14.3"
+ }
]
},
"step3Note1": "(?: ... ) zoskupí časť vzoru bez vytvorenia ďalšej výstupnej hodnoty. Hodí sa na prijatie verzií s dvoma, tromi alebo štyrmi blokmi bez komplikovania výsledku.",
@@ -257,6 +319,15 @@
],
"trailing": "Odstránenie záznamu aplikáciu neodinštaluje ani nezastaví. Odstráni iba informácie, ktoré ProxMenux používa na jej zobrazenie a sledovanie verzie."
},
+ "options": {
+ "heading": "",
+ "lead": "",
+ "items": [
+ "",
+ ""
+ ],
+ "trailing": ""
+ },
"notDetected": {
"heading": "Ak aplikácia nebola nájdená",
"intro": "Automatická detekcia nie je nutná na používanie tejto funkcie. Ak sa nezobrazí žiadny návrh:",