fix: name the guest in VM/CT health alerts, refresh AI models, propose the recipe's volume choice

- A health problem of a VM or CT names the guest in its title and in the
  Quiet Hours summary; a recovery shows its category by name in the
  reader's language.
- AI model lists reviewed against each provider: Gemini adds 3.6/3.7/3.8
  Flash, Groq adds gpt-oss-120b, and Groq/OpenRouter models no longer
  served are removed.
- OCI installer proposes the volume choice the recipe declares, so a path
  meant for a host directory is proposed as one; Duplicati /source is
  proposed on a host directory.
This commit is contained in:
MacRimi
2026-10-26 18:47:09 +01:00
parent b6500c147e
commit 7b1736fe3e
21 changed files with 160 additions and 24 deletions
+10 -10
View File
@@ -1,16 +1,16 @@
{
"_description": "Verified AI models for ProxMenux notifications. Only models listed here will be shown to users. Models are tested to work with the chat/completions API format.",
"_updated": "2026-09-02",
"_updated": "2026-10-26",
"_verifier": "Refreshed with tools/ai-models-verifier (private). Re-run before each ProxMenux release to keep the list current. The verifier and ProxMenux share the same reasoning/thinking-model handlers so their verdicts stay aligned with runtime behaviour.",
"groq": {
"models": [
"allam-2-7b",
"qwen/qwen3.8-27b",
"openai/gpt-oss-20b",
"groq/compound-mini"
"openai/gpt-oss-120b"
],
"recommended": "openai/gpt-oss-20b",
"_note": "Verified 2026-09-02 with the Groq API. 4 of 12 tested pass. `llama-3.3-70b-versatile` no longer served by Groq. Recommended kept as `openai/gpt-oss-20b` (english-capable) — `allam-2-7b` is fastest but Arabic-focused; add manually if serving Arabic notifications."
"_note": "Verified 2026-10-26 with the Groq API. 4 of 8 tested pass. `groq/compound-mini` is no longer served and was dropped; `openai/gpt-oss-120b` added. Recommended kept as `openai/gpt-oss-20b` (english-capable) — `allam-2-7b` is fastest but Arabic-focused."
},
"gemini": {
"models": [
@@ -21,7 +21,10 @@
"gemini-flash-lite-latest",
"gemini-2.5-flash",
"gemini-3-flash-preview",
"gemini-3.5-flash"
"gemini-3.5-flash",
"gemini-3.6-flash",
"gemini-3.7-flash",
"gemini-3.8-flash"
],
"recommended": "gemini-2.5-flash-lite",
"_deprecated": [
@@ -31,7 +34,7 @@
"gemini-1.0-pro",
"gemini-pro"
],
"_note": "Verified 2026-09-02. gemini-flash-lite-latest passes consistently but gemini-2.5-flash-lite remains recommended because 'latest' aliases can drift. gemini-3.1-flash-lite is the stable successor to 3-flash-preview. Pro variants continue to reject thinkingBudget=0 and are overkill for notification translation."
"_note": "Verified 2026-10-26. Added gemini-3.6-flash, 3.7-flash and 3.8-flash. gemini-flash-lite-latest passes consistently but gemini-2.5-flash-lite remains recommended because 'latest' aliases can drift. Pro variants continue to reject thinkingBudget=0 and are overkill for notification translation; gemini-4-argon passes but takes ~20s per message."
},
"openai": {
"models": [
@@ -116,13 +119,10 @@
"nvidia/nemotron-3-nano-omni-30b-a3b-reasoning:free",
"nvidia/nemotron-3-super-120b-a12b:free",
"google/gemma-4-26b-a4b-it:free",
"nvidia/nemotron-nano-12b-v2-vl:free",
"nvidia/nemotron-3-nano-30b-a3b:free",
"poolside/laguna-s-2.1:free",
"openai/gpt-oss-20b:free"
"poolside/laguna-s-2.1:free"
],
"recommended": "meta-llama/llama-3.3-70b-instruct",
"_note": "Curated manually from the 249+ models OpenRouter serves — the auto-verifier passes ~200 today but only a subset are practical for notification translation. Includes the 11 previously-curated paid models plus 22 new mainstream additions (gpt-5.1, claude-opus-4.8, sonnet-5, grok-4.3/4.20, kimi-k2, deepseek-v3.2, nova, cohere-a, sonar, ...) and 6 `:free` variants re-added manually — the free tier is intentionally supported per user request even though today's rate limits blocked some of them from passing the test."
"_note": "Curated manually from the 249+ models OpenRouter serves — the auto-verifier passes ~200 today but only a subset are practical for notification translation. Includes the 11 previously-curated paid models plus 22 new mainstream additions (gpt-5.1, claude-opus-4.8, sonnet-5, grok-4.3/4.20, kimi-k2, deepseek-v3.2, nova, cohere-a, sonar, ...) and 6 `:free` variants re-added manually — the free tier is intentionally supported per user request even though today's rate limits blocked some of them from passing the test. Reviewed 2026-10-26: three `:free` variants no longer served by OpenRouter were dropped."
},
"ollama": {
"_note": "Ollama models are local, we don't filter them. User manages their own models.",
@@ -2125,9 +2125,22 @@ def render_template(event_type: str, data: Dict[str, Any],
or ''
)
_ent = str(_ent).strip()
# A VM or CT health issue records the guest as id, vmname and type.
if not _ent and str(variables.get('category', '')) == 'vms' and variables.get('id'):
_ent = f"{variables.get('type') or 'VM/CT'} {variables['id']}"
if variables.get('vmname'):
_ent += f" ({variables['vmname']})"
variables['entity'] = _ent
variables['entity_suffix'] = f' — {_ent}' if _ent else ''
# Health categories reach the templates by their key ("vms"); show the
# name the health summary uses for them, in the reader's language.
_category = str(variables.get('category', '')).strip()
if _category:
_category_label = runtime_message(f'healthDegraded.categories.{_category}', language)
if _category_label:
variables['category'] = _category_label
# `title_or_default` lets a template surface a caller-computed title
# when the caller already knows the entity context (e.g.
# flask_server's health collector), and fall back to a stock string
@@ -0,0 +1,55 @@
"""A health problem of a VM or CT names the guest in its title, and health
categories are shown by their name, not by their internal key."""
import sys
import unittest
from pathlib import Path
from unittest import mock
SCRIPTS_DIR = Path(__file__).resolve().parents[1]
if str(SCRIPTS_DIR) not in sys.path:
sys.path.insert(0, str(SCRIPTS_DIR))
import notification_templates as templates
QMP = {'category': 'vms', 'severity': 'WARNING', 'error_key': 'vm_101',
'reason': 'VM 101 (web): QMP command failed or timed out.',
'id': '101', 'vmname': 'web', 'type': 'VM'}
class VmHealthTitleTests(unittest.TestCase):
def setUp(self):
patcher = mock.patch.object(templates, '_get_hostname', return_value='pve')
patcher.start()
self.addCleanup(patcher.stop)
def test_a_guest_problem_names_the_guest(self):
title = templates.render_template('system_problem', dict(QMP))['title']
self.assertEqual(title, 'pve: System problem detected — VM 101 (web)')
def test_a_container_without_a_name_is_named_by_its_id(self):
data = dict(QMP, id='105', vmname='', type='CT')
self.assertTrue(templates.render_template('system_problem', data)['title'].endswith('— CT 105'))
def test_the_recovery_names_the_category_and_the_guest(self):
data = dict(QMP, severity='OK', original_severity='WARNING', duration='6m', is_recovery=True)
title = templates.render_template('error_resolved', data)['title']
self.assertEqual(title, 'pve: Resolved - VMs and containers — VM 101 (web)')
def test_the_category_follows_the_language_of_the_reader(self):
data = dict(QMP, severity='OK', original_severity='WARNING', duration='6m', is_recovery=True)
title = templates.render_template('error_resolved', data, 'es')['title']
self.assertIn('Máquinas virtuales y contenedores', title)
self.assertNotIn(' vms', title)
def test_a_category_without_a_name_keeps_its_key(self):
data = {'category': 'temperature', 'severity': 'OK', 'reason': 'Back to normal',
'original_severity': 'WARNING', 'duration': '3m', 'is_recovery': True}
self.assertIn('Resolved - temperature', templates.render_template('error_resolved', data)['title'])
def test_other_events_with_an_id_are_not_given_a_guest(self):
data = {'category': 'storage', 'severity': 'WARNING', 'reason': 'x', 'id': '7', 'type': 'VM'}
self.assertNotIn('VM 7', templates.render_template('system_problem', data)['title'])
if __name__ == '__main__':
unittest.main()
+3
View File
@@ -120,6 +120,7 @@ ProxMenux does not build its own containers for these applications: it reads the
- **A host directory that already exists can be opened to the application that shares it**, with access rules (ACL) that leave the owner of the files as it is.
- **A shared host directory shows what the host has mounted inside it**: network shares, disks and ZFS datasets, also the ones mounted while the container runs.
- **A shared host directory is recognised by its filesystem**, so an application keeps being updated after the host restarts or a disk comes back under another device name (reported by Charly Silva).
- **The installer proposes the place each application expects for a path**: a library, a recording folder or the files Duplicati backs up are proposed on a host directory, and the user can still choose a container disk.
### Updating, modifying and recreating
@@ -274,7 +275,9 @@ Real installations compared side by side with the same images under Docker, and
- **Excluded storages stay out of the capacity alert** (reported by @christiannieveslauz), and a storage that is filling up says how full it is.
- **A private bridge with nothing connected is not reported as down.**
- **The field icons of a notification show in French too** ([PR #523](https://github.com/MacRimi/ProxMenux/pull/523), contributed by [@f3rs3n](https://github.com/f3rs3n)), where a non-breaking space comes before the colon of each label.
- **A health problem of a VM or CT names the guest**, such as a QMP command that timed out during a backup, in its notification and in the Quiet Hours summary, and a recovery shows its category by name, such as *VMs and containers*, in the reader's language.
- **The AI rewrite works with the current Claude models** ([PR #530](https://github.com/MacRimi/ProxMenux/pull/530), contributed by [@MaEscalanteHe](https://github.com/MaEscalanteHe)): the Anthropic list moves to the 5.x family with Claude Haiku 5.5 recommended, and a model that declines or answers with no text leaves the original notification to be sent.
- **The AI model lists were reviewed against each provider**: Gemini adds 3.6, 3.7 and 3.8 Flash, Groq adds GPT-OSS 120B, and the Groq and OpenRouter models no longer served were removed.
### 📦 Grouped application update notifications
+1 -1
View File
@@ -117,7 +117,7 @@
"managed-volume",
"host-bind"
],
"default": "managed-volume",
"default": "host-bind",
"managed_volume": {
"backup": true,
"default_size_gb": 8
+1 -1
View File
@@ -166,7 +166,7 @@
"managed-volume",
"host-bind"
],
"default": "managed-volume",
"default": "host-bind",
"managed_volume": {
"backup": true,
"default_size_gb": 8
+1 -1
View File
@@ -117,7 +117,7 @@
"managed-volume",
"host-bind"
],
"default": "managed-volume",
"default": "host-bind",
"managed_volume": {
"backup": true,
"default_size_gb": 8
+50
View File
@@ -58,6 +58,56 @@
"prompt": "Password of the Duplicati web interface",
"source": "linuxserver-compose"
}
],
"volumes": [
{
"id": "volume-0",
"container_path": "/config",
"compose_source_example": "/path/to/duplicati/config",
"read_only": false,
"required": true,
"installation_choice": [
"managed-volume",
"host-bind"
],
"default": "managed-volume",
"managed_volume": {
"backup": true,
"default_size_gb": 4
}
},
{
"id": "volume-1",
"container_path": "/backups",
"compose_source_example": "/path/to/backups",
"read_only": false,
"required": true,
"installation_choice": [
"managed-volume",
"host-bind"
],
"default": "managed-volume",
"managed_volume": {
"backup": true,
"default_size_gb": 8
}
},
{
"id": "volume-2",
"container_path": "/source",
"compose_source_example": "/path/to/source",
"read_only": false,
"required": true,
"installation_choice": [
"managed-volume",
"host-bind"
],
"default": "host-bind",
"managed_volume": {
"backup": true,
"default_size_gb": 8
}
}
]
},
"proxmox": {
@@ -45,7 +45,8 @@
"container_path": "/backups"
},
{
"container_path": "/source"
"container_path": "/source",
"default": "host-bind"
}
],
"restart": "unless-stopped"
+2
View File
@@ -147,6 +147,8 @@ def _recommended_storage(volume: dict[str, Any]) -> str:
default = volume.get("default")
if {"managed-volume", "host-bind"} <= choices:
both = f"{translate('Container volume')} / {translate('Host directory')}"
if default == "host-bind":
both = f"{translate('Host directory')} / {translate('Container volume')}"
return f"{translate('Optional')}: {both}" if default == "skip" else both
if default == "skip":
return translate("Optional, not mounted by default")
+2 -2
View File
@@ -507,8 +507,8 @@ def build_deployment(
offered = list(choices)
chosen = advanced or {"managed-volume", "host-bind"} <= set(choices)
if chosen and len(offered) > 1:
# The answer is the user's: a host directory is never proposed.
proposed = "managed-volume" if item["default"] == "host-bind" else item["default"]
# The answer is the user's; the choice of the oci-compose is proposed.
proposed = item["default"]
volume_mode = ui.choose(f"{translate('Where to store')} {item['container_path']}",
[(choice, labels[choice]) for choice in offered], proposed)
if volume_mode is None:
@@ -104,6 +104,15 @@ class DefaultInstallEssentialsTests(unittest.TestCase):
self.assertEqual({m["source"] for m in plan["mounts"] if m["type"] == "managed-volume"} - {"Public"},
set(), app)
def test_the_files_to_back_up_are_proposed_on_a_host_directory(self, *_):
for app in ("duplicati", "duplicati-official"):
ui, plan = self.build(app)
source = next(m for m in plan["mounts"] if m["container_path"] == "/source")
self.assertEqual(source["type"], "host-bind", app)
self.assertIn("Host path for /source", ui.asked, app)
backups = next(m for m in plan["mounts"] if m["container_path"] == "/backups")
self.assertEqual(backups["type"], "managed-volume", app)
def test_the_members_of_a_stack_ask_nothing_of_their_own(self, *_):
for app in ("linkwarden", "suite-arr"):
ui, _plan = self.build(app)
+3
View File
@@ -119,6 +119,7 @@ ProxMenux no construye sus propios contenedores para estas aplicaciones: lee la
- **Un directorio del host que ya existe se puede abrir a la aplicación que lo comparte**, con reglas de acceso (ACL) que dejan intacto el propietario de los archivos.
- **Un directorio compartido del host muestra lo que el host tiene montado dentro de él**: recursos compartidos de red, discos y datasets ZFS, también los que se montan mientras el contenedor está en marcha.
- **Un directorio compartido del host se reconoce por su sistema de archivos**, así que una aplicación se sigue actualizando después de que el host se reinicie o de que un disco vuelva con otro nombre de dispositivo (reportado por Charly Silva).
- **El instalador propone el sitio que cada aplicación espera para una ruta**: una biblioteca, una carpeta de grabaciones o los archivos de los que Duplicati hace backup se proponen en un directorio del host, y se puede elegir igualmente un disco del contenedor.
### Actualizar, modificar y recrear
@@ -273,7 +274,9 @@ Instalaciones reales comparadas una junto a otra con las mismas imágenes en Doc
- **Los almacenamientos excluidos quedan fuera de la alerta de capacidad** (reportado por @christiannieveslauz), y un almacenamiento que se está llenando indica cuánto está ocupado.
- **Un bridge privado sin nada conectado no se notifica como caído.**
- **Los iconos de los campos de una notificación también se muestran en francés** ([PR #523](https://github.com/MacRimi/ProxMenux/pull/523), aportado por [@f3rs3n](https://github.com/f3rs3n)), donde un espacio de no separación precede a los dos puntos de cada etiqueta.
- **Un problema de salud de una VM o un CT nombra a la máquina**, como un comando QMP que no respondió durante un backup, en su notificación y en el resumen de las horas de silencio, y una recuperación muestra su categoría por su nombre, como *Máquinas virtuales y contenedores*, en el idioma del lector.
- **La reescritura con IA funciona con los modelos actuales de Claude** ([PR #530](https://github.com/MacRimi/ProxMenux/pull/530), aportado por [@MaEscalanteHe](https://github.com/MaEscalanteHe)): la lista de Anthropic pasa a la familia 5.x, con Claude Haiku 5.5 como recomendado, y si un modelo rechaza la petición o responde sin texto se envía la notificación original.
- **Las listas de modelos de IA se han revisado con cada proveedor**: Gemini añade 3.6, 3.7 y 3.8 Flash, Groq añade GPT-OSS 120B y se han eliminado los modelos de Groq y OpenRouter que ya no se sirven.
### 📦 Notificaciones agrupadas de actualizaciones de aplicaciones
@@ -121,7 +121,7 @@
"tagline": "Sehr schnelle, großzügige freie Stufe (30 req/min). Ideal zum Starten.",
"items": [
"API-Schlüssel: <a>console.groq.com/keys</a>",
"Beispiele aus der mitgelieferten Modellliste: <code>allam-2-7b</code>, <code>qwen/qwen3.8-27b</code>, <code>openai/gpt-oss-20b</code>, <code>groq/compound-mini</code>.",
"Beispiele aus der mitgelieferten Modellliste: <code>allam-2-7b</code>, <code>qwen/qwen3.8-27b</code>, <code>openai/gpt-oss-20b</code>, <code>openai/gpt-oss-120b</code>.",
"Mitgelieferte Empfehlung: <strong><code>openai/gpt-oss-20b</code></strong>. Prüfen Sie die Verfügbarkeit für Ihr Konto über die Anbieterauswahl und den Verbindungstest."
]
},
@@ -121,7 +121,7 @@
"tagline": "Very fast, generous free tier (30 req/min). Ideal to start.",
"items": [
"API key: <a>console.groq.com/keys</a>",
"Examples from the bundled model list: <code>allam-2-7b</code>, <code>qwen/qwen3.8-27b</code>, <code>openai/gpt-oss-20b</code>, <code>groq/compound-mini</code>.",
"Examples from the bundled model list: <code>allam-2-7b</code>, <code>qwen/qwen3.8-27b</code>, <code>openai/gpt-oss-20b</code>, <code>openai/gpt-oss-120b</code>.",
"Bundled recommendation: <strong><code>openai/gpt-oss-20b</code></strong>. Use the provider selector and connection test to check availability for your account."
]
},
@@ -121,7 +121,7 @@
"tagline": "Muy rápido, nivel gratuito generoso (30 req/min). Ideal para empezar.",
"items": [
"Clave API: <a>console.groq.com/keys</a>",
"Ejemplos de la lista de modelos incluida: <code>allam-2-7b</code>, <code>qwen/qwen3.8-27b</code>, <code>openai/gpt-oss-20b</code>, <code>groq/compound-mini</code>.",
"Ejemplos de la lista de modelos incluida: <code>allam-2-7b</code>, <code>qwen/qwen3.8-27b</code>, <code>openai/gpt-oss-20b</code>, <code>openai/gpt-oss-120b</code>.",
"Recomendación incluida: <strong><code>openai/gpt-oss-20b</code></strong>. Comprueba su disponibilidad para tu cuenta con el selector del proveedor y la prueba de conexión."
]
},
@@ -121,7 +121,7 @@
"tagline": "Très rapide, généreux niveau libre (30 req/min). Idéal pour commencer.",
"items": [
"Clé API : <a>console.groq.com/keys</a>",
"Exemples de la liste de modèles fournie : <code>allam-2-7b</code>, <code>qwen/qwen3.8-27b</code>, <code>openai/gpt-oss-20b</code>, <code>groq/compound-mini</code>.",
"Exemples de la liste de modèles fournie : <code>allam-2-7b</code>, <code>qwen/qwen3.8-27b</code>, <code>openai/gpt-oss-20b</code>, <code>openai/gpt-oss-120b</code>.",
"Recommandation fournie : <strong><code>openai/gpt-oss-20b</code></strong>. Vérifiez sa disponibilité pour votre compte avec le sélecteur du fournisseur et le test de connexion."
]
},
@@ -121,7 +121,7 @@
"tagline": "Molto veloce, generoso tier libero (30 req/min). Ideale per iniziare.",
"items": [
"Chiave API: <a>console.groq.com/keys</a>",
"Esempi dall’elenco di modelli incluso: <code>allam-2-7b</code>, <code>qwen/qwen3.8-27b</code>, <code>openai/gpt-oss-20b</code>, <code>groq/compound-mini</code>.",
"Esempi dall’elenco di modelli incluso: <code>allam-2-7b</code>, <code>qwen/qwen3.8-27b</code>, <code>openai/gpt-oss-20b</code>, <code>openai/gpt-oss-120b</code>.",
"Raccomandazione inclusa: <strong><code>openai/gpt-oss-20b</code></strong>. Verifica la disponibilità per il tuo account tramite il selettore del provider e il test di connessione."
]
},
@@ -121,7 +121,7 @@
"tagline": "Nível livre muito rápido e generoso (30 req/min). Ideal para começar.",
"items": [
"Chave API: <a>console.groq.com/keys</a>",
"Exemplos da lista de modelos incluída: <code>allam-2-7b</code>, <code>qwen/qwen3.8-27b</code>, <code>openai/gpt-oss-20b</code>, <code>groq/compound-mini</code>.",
"Exemplos da lista de modelos incluída: <code>allam-2-7b</code>, <code>qwen/qwen3.8-27b</code>, <code>openai/gpt-oss-20b</code>, <code>openai/gpt-oss-120b</code>.",
"Recomendação incluída: <strong><code>openai/gpt-oss-20b</code></strong>. Verifique a disponibilidade para a sua conta no seletor do fornecedor e no teste de ligação."
]
},
@@ -121,7 +121,7 @@
"tagline": "Veľmi rýchla, veľkorysá voľná úroveň (30 req/min). Ideálne začať.",
"items": [
"Kľúč API: <a>console.groq.com/keys</a>",
"Príklady z dodaného zoznamu modelov: <code>allam-2-7b</code>, <code>qwen/qwen3.8-27b</code>, <code>openai/gpt-oss-20b</code>, <code>groq/compound-mini</code>.",
"Príklady z dodaného zoznamu modelov: <code>allam-2-7b</code>, <code>qwen/qwen3.8-27b</code>, <code>openai/gpt-oss-20b</code>, <code>openai/gpt-oss-120b</code>.",
"Dodané odporúčanie: <strong><code>openai/gpt-oss-20b</code></strong>. Dostupnosť pre svoj účet overte výberom poskytovateľa a testom spojenia."
]
},
@@ -121,7 +121,7 @@
"tagline": "Mycket snabb, generös fri nivå (30 req/min). Perfekt att börja.",
"items": [
"API-nyckel: <a>console.groq.com/keys</a>",
"Exempel från den medföljande modellistan: <code>allam-2-7b</code>, <code>qwen/qwen3.8-27b</code>, <code>openai/gpt-oss-20b</code>, <code>groq/compound-mini</code>.",
"Exempel från den medföljande modellistan: <code>allam-2-7b</code>, <code>qwen/qwen3.8-27b</code>, <code>openai/gpt-oss-20b</code>, <code>openai/gpt-oss-120b</code>.",
"Medföljande rekommendation: <strong><code>openai/gpt-oss-20b</code></strong>. Kontrollera tillgängligheten för ditt konto med leverantörsväljaren och anslutningstestet."
]
},