improve update notifications and add apt-listchanges channel

- add an apt-listchanges notification event that surfaces package-maintainer NEWS forwarded through the PVE mail queue, with its own template, emoji and per-locale label
- emit a single post_install_update event when a tool exposes a newer version, and persist per-tool notified versions on disk so a Monitor restart no longer re-fires the same update announcement
- force AI enrichment to formatting-only on availability notices (update_available / pve_update / proxmenux_update / post_install_update / apt_listchanges / lxc_updates_available / secure_gateway_update_available / nvidia_driver_update_available / coral_driver_update_available / app_update_available / docker_stack_update_available / update_summary) so translations never turn factual inventories into advice
- add the apt_listchanges channel label to Monitor UI strings across the eight locales
This commit is contained in:
MacRimi
2026-08-26 21:00:00 +02:00
parent 0fe32d2842
commit c2a6db38a8
12 changed files with 169 additions and 48 deletions
+71 -7
View File
@@ -92,6 +92,73 @@ def _read_text(path: Path) -> str:
return ""
def _load_notified_versions_from_disk() -> dict[str, set[str]]:
"""Read the versions already announced for each optimization.
Notification history lives beside the existing update snapshot so no
additional runtime file is introduced. Older snapshots simply have no
``notified_versions`` member and therefore start with an empty history.
"""
try:
payload = json.loads(_read_text(_UPDATES_JSON) or "{}")
except json.JSONDecodeError:
return {}
raw = payload.get("notified_versions", {})
if not isinstance(raw, dict):
return {}
normalized: dict[str, set[str]] = {}
for key, versions in raw.items():
if isinstance(versions, str):
versions = [versions]
if not isinstance(versions, list):
continue
clean = {str(version).strip() for version in versions if str(version).strip()}
if clean:
normalized[str(key)] = clean
return normalized
def _write_persisted_snapshot(
scanned_at: float,
updates: list[dict[str, Any]],
notified_versions: dict[str, set[str]],
) -> None:
"""Atomically persist the update snapshot and notification history."""
payload = {
"scanned_at": scanned_at,
"updates": updates,
"notified_versions": {
key: sorted(versions)
for key, versions in sorted(notified_versions.items())
if versions
},
}
_UPDATES_JSON.parent.mkdir(parents=True, exist_ok=True)
temporary = _UPDATES_JSON.with_suffix(_UPDATES_JSON.suffix + ".tmp")
temporary.write_text(json.dumps(payload, indent=2), encoding="utf-8")
temporary.replace(_UPDATES_JSON)
def load_notified_versions() -> dict[str, set[str]]:
"""Return a defensive copy of optimization versions already announced."""
return {
key: set(versions)
for key, versions in _load_notified_versions_from_disk().items()
}
def save_notified_versions(history: dict[str, set[str]]) -> None:
"""Persist notification history without changing the current scan."""
with _cache_lock:
scanned_at = float(_cache.get("scanned_at", 0.0) or 0.0)
updates = list(_cache.get("updates", []))
try:
_write_persisted_snapshot(scanned_at, updates, history)
except OSError:
# Notification de-duplication remains best-effort on read-only hosts.
pass
# ---------------------------------------------------------------------------
# Bash script parser
# ---------------------------------------------------------------------------
@@ -353,13 +420,10 @@ def scan(persist: bool = True) -> dict[str, Any]:
if persist:
try:
_UPDATES_JSON.parent.mkdir(parents=True, exist_ok=True)
_UPDATES_JSON.write_text(
json.dumps(
{"scanned_at": snapshot["scanned_at"], "updates": updates},
indent=2,
),
encoding="utf-8",
_write_persisted_snapshot(
snapshot["scanned_at"],
updates,
_load_notified_versions_from_disk(),
)
except OSError:
# Writing the on-disk cache is best-effort. If /usr/local