mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-09-24 23:56:42 +00:00
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:
@@ -1775,6 +1775,7 @@
|
|||||||
"service_fail_batch": "Mehrere Dienstausfälle",
|
"service_fail_batch": "Mehrere Dienstausfälle",
|
||||||
"cron_output": "Cron-Job-Ausgabe",
|
"cron_output": "Cron-Job-Ausgabe",
|
||||||
"system_mail": "Smartd / Systemmail",
|
"system_mail": "Smartd / Systemmail",
|
||||||
|
"apt_listchanges": "Pakethinweise von apt-listchanges",
|
||||||
"webhook_test": "Webhook-Test",
|
"webhook_test": "Webhook-Test",
|
||||||
"update_available": "Verfügbare Updates (Legacy)",
|
"update_available": "Verfügbare Updates (Legacy)",
|
||||||
"unknown_persistent": "Scheck nicht verfügbar",
|
"unknown_persistent": "Scheck nicht verfügbar",
|
||||||
|
|||||||
@@ -1774,6 +1774,7 @@
|
|||||||
"service_fail_batch": "Multiple service failures",
|
"service_fail_batch": "Multiple service failures",
|
||||||
"cron_output": "Cron job output",
|
"cron_output": "Cron job output",
|
||||||
"system_mail": "Smartd / system mail",
|
"system_mail": "Smartd / system mail",
|
||||||
|
"apt_listchanges": "apt-listchanges package notices",
|
||||||
"webhook_test": "Webhook test",
|
"webhook_test": "Webhook test",
|
||||||
"update_available": "Updates available (legacy)",
|
"update_available": "Updates available (legacy)",
|
||||||
"unknown_persistent": "Check unavailable",
|
"unknown_persistent": "Check unavailable",
|
||||||
|
|||||||
@@ -1775,6 +1775,7 @@
|
|||||||
"service_fail_batch": "Múltiples fallas de servicio",
|
"service_fail_batch": "Múltiples fallas de servicio",
|
||||||
"cron_output": "Salida del trabajo cron",
|
"cron_output": "Salida del trabajo cron",
|
||||||
"system_mail": "Smartd / correo del sistema",
|
"system_mail": "Smartd / correo del sistema",
|
||||||
|
"apt_listchanges": "Avisos de paquetes de apt-listchanges",
|
||||||
"webhook_test": "prueba de webhook",
|
"webhook_test": "prueba de webhook",
|
||||||
"update_available": "Actualizaciones disponibles (heredadas)",
|
"update_available": "Actualizaciones disponibles (heredadas)",
|
||||||
"unknown_persistent": "Verificar no disponible",
|
"unknown_persistent": "Verificar no disponible",
|
||||||
|
|||||||
@@ -1775,6 +1775,7 @@
|
|||||||
"service_fail_batch": "Plusieurs échecs de service",
|
"service_fail_batch": "Plusieurs échecs de service",
|
||||||
"cron_output": "Sortie de la tâche Cron",
|
"cron_output": "Sortie de la tâche Cron",
|
||||||
"system_mail": "Smartd / messagerie système",
|
"system_mail": "Smartd / messagerie système",
|
||||||
|
"apt_listchanges": "Avis de paquets apt-listchanges",
|
||||||
"webhook_test": "Test de webhook",
|
"webhook_test": "Test de webhook",
|
||||||
"update_available": "Mises à jour disponibles (héritées)",
|
"update_available": "Mises à jour disponibles (héritées)",
|
||||||
"unknown_persistent": "Chèque indisponible",
|
"unknown_persistent": "Chèque indisponible",
|
||||||
|
|||||||
@@ -1775,6 +1775,7 @@
|
|||||||
"service_fail_batch": "Diversi errori di servizio",
|
"service_fail_batch": "Diversi errori di servizio",
|
||||||
"cron_output": "Output del lavoro Cron",
|
"cron_output": "Output del lavoro Cron",
|
||||||
"system_mail": "Smartd/posta di sistema",
|
"system_mail": "Smartd/posta di sistema",
|
||||||
|
"apt_listchanges": "Avvisi sui pacchetti apt-listchanges",
|
||||||
"webhook_test": "Prova del webhook",
|
"webhook_test": "Prova del webhook",
|
||||||
"update_available": "Aggiornamenti disponibili (legacy)",
|
"update_available": "Aggiornamenti disponibili (legacy)",
|
||||||
"unknown_persistent": "Controlla non disponibile",
|
"unknown_persistent": "Controlla non disponibile",
|
||||||
|
|||||||
@@ -1775,6 +1775,7 @@
|
|||||||
"service_fail_batch": "Várias falhas de serviço",
|
"service_fail_batch": "Várias falhas de serviço",
|
||||||
"cron_output": "Saída do cron job",
|
"cron_output": "Saída do cron job",
|
||||||
"system_mail": "Smartd / correio do sistema",
|
"system_mail": "Smartd / correio do sistema",
|
||||||
|
"apt_listchanges": "Avisos de pacotes do apt-listchanges",
|
||||||
"webhook_test": "Teste de webhook",
|
"webhook_test": "Teste de webhook",
|
||||||
"update_available": "Atualizações disponíveis (legado)",
|
"update_available": "Atualizações disponíveis (legado)",
|
||||||
"unknown_persistent": "Verifique indisponível",
|
"unknown_persistent": "Verifique indisponível",
|
||||||
|
|||||||
@@ -1774,6 +1774,7 @@
|
|||||||
"service_fail_batch": "Zlyhanie viacerých služieb",
|
"service_fail_batch": "Zlyhanie viacerých služieb",
|
||||||
"cron_output": "Výstup úlohy cron",
|
"cron_output": "Výstup úlohy cron",
|
||||||
"system_mail": "Správy Smartd a systému",
|
"system_mail": "Správy Smartd a systému",
|
||||||
|
"apt_listchanges": "Oznámenia o balíkoch apt-listchanges",
|
||||||
"webhook_test": "Test webhooku",
|
"webhook_test": "Test webhooku",
|
||||||
"update_available": "Dostupné aktualizácie (starší typ)",
|
"update_available": "Dostupné aktualizácie (starší typ)",
|
||||||
"unknown_persistent": "Kontrola nie je dostupná",
|
"unknown_persistent": "Kontrola nie je dostupná",
|
||||||
|
|||||||
@@ -1775,6 +1775,7 @@
|
|||||||
"service_fail_batch": "Flera tjänstefel",
|
"service_fail_batch": "Flera tjänstefel",
|
||||||
"cron_output": "Cron-jobbutdata",
|
"cron_output": "Cron-jobbutdata",
|
||||||
"system_mail": "Smartd / systemmail",
|
"system_mail": "Smartd / systemmail",
|
||||||
|
"apt_listchanges": "Paketmeddelanden från apt-listchanges",
|
||||||
"webhook_test": "Webhook test",
|
"webhook_test": "Webhook test",
|
||||||
"update_available": "Uppdateringar tillgängliga (legacy)",
|
"update_available": "Uppdateringar tillgängliga (legacy)",
|
||||||
"unknown_persistent": "Kontrollera inte tillgänglig",
|
"unknown_persistent": "Kontrollera inte tillgänglig",
|
||||||
|
|||||||
@@ -196,6 +196,20 @@ def _hostname() -> str:
|
|||||||
return resolved
|
return resolved
|
||||||
|
|
||||||
|
|
||||||
|
def _new_post_install_update_versions(
|
||||||
|
updates: list[dict[str, Any]],
|
||||||
|
notified_versions: dict[str, set[str]],
|
||||||
|
) -> dict[str, str]:
|
||||||
|
"""Return only optimization versions that have never been announced."""
|
||||||
|
available: dict[str, str] = {}
|
||||||
|
for update in updates:
|
||||||
|
key = str(update.get('key', '') or '').strip()
|
||||||
|
version = str(update.get('available_version', '') or '').strip()
|
||||||
|
if key and version and version not in notified_versions.get(key, set()):
|
||||||
|
available[key] = version
|
||||||
|
return available
|
||||||
|
|
||||||
|
|
||||||
def capture_journal_context(keywords: list, lines: int = 30,
|
def capture_journal_context(keywords: list, lines: int = 30,
|
||||||
since: str = "5 minutes ago") -> str:
|
since: str = "5 minutes ago") -> str:
|
||||||
"""Capture relevant journal lines for AI context enrichment.
|
"""Capture relevant journal lines for AI context enrichment.
|
||||||
@@ -2567,11 +2581,9 @@ class PollingCollector:
|
|||||||
self._last_ai_model_check = 0
|
self._last_ai_model_check = 0
|
||||||
# Sprint 12D: post-install function updates check, on the same
|
# Sprint 12D: post-install function updates check, on the same
|
||||||
# 24h cooldown as the Proxmox/ProxMenux update checks. Notify
|
# 24h cooldown as the Proxmox/ProxMenux update checks. Notify
|
||||||
# once per *changed set* of update keys — repeating the same
|
# once for each genuinely new available version. The persistent
|
||||||
# notification every 24h forever would be noisy, so we de-dupe
|
# history is stored in updates_available.json beside the scan.
|
||||||
# against the previously-notified set.
|
|
||||||
self._last_post_install_check = 0
|
self._last_post_install_check = 0
|
||||||
self._notified_post_install_keys: set[str] = set()
|
|
||||||
# Sprint 14.7: fingerprint (item_id → latest_version) of the
|
# Sprint 14.7: fingerprint (item_id → latest_version) of the
|
||||||
# last managed-installs update notification, across all types
|
# last managed-installs update notification, across all types
|
||||||
# in the registry. A new notification fires when the
|
# in the registry. A new notification fires when the
|
||||||
@@ -3503,11 +3515,9 @@ class PollingCollector:
|
|||||||
Sprint 12A's detector runs at AppImage startup and writes
|
Sprint 12A's detector runs at AppImage startup and writes
|
||||||
``updates_available.json``. This check refreshes the snapshot
|
``updates_available.json``. This check refreshes the snapshot
|
||||||
every 24h (matching the other update channels), and emits a
|
every 24h (matching the other update channels), and emits a
|
||||||
single ``post_install_update`` event the first time the *set* of
|
single ``post_install_update`` event when a tool exposes an available
|
||||||
available updates changes. Repeating the same notification every
|
version that has never been announced. Applying one item merely
|
||||||
24h forever would be noisy, so we de-dupe against the previously
|
shrinks the pending set and must not produce a second notification.
|
||||||
notified set of tool keys: only when a new tool joins the list
|
|
||||||
(or an existing one disappears) does a fresh notification fire.
|
|
||||||
"""
|
"""
|
||||||
now = time.time()
|
now = time.time()
|
||||||
if now - self._last_post_install_check < self.UPDATE_CHECK_INTERVAL:
|
if now - self._last_post_install_check < self.UPDATE_CHECK_INTERVAL:
|
||||||
@@ -3523,16 +3533,12 @@ class PollingCollector:
|
|||||||
return
|
return
|
||||||
|
|
||||||
if not updates:
|
if not updates:
|
||||||
# All caught up. Reset so a future bump triggers a fresh
|
|
||||||
# notification instead of being suppressed by stale state.
|
|
||||||
self._notified_post_install_keys = set()
|
|
||||||
return
|
return
|
||||||
|
|
||||||
new_keys = {u.get('key', '') for u in updates if u.get('key')}
|
notified_versions = post_install_versions.load_notified_versions()
|
||||||
if new_keys == self._notified_post_install_keys:
|
new_versions = _new_post_install_update_versions(updates, notified_versions)
|
||||||
return # already notified about this exact set
|
if not new_versions:
|
||||||
|
return
|
||||||
self._notified_post_install_keys = new_keys
|
|
||||||
|
|
||||||
# Pre-format the bullet list here so the template can drop it
|
# Pre-format the bullet list here so the template can drop it
|
||||||
# straight in with `{tool_list}` (the renderer is plain
|
# straight in with `{tool_list}` (the renderer is plain
|
||||||
@@ -3567,6 +3573,9 @@ class PollingCollector:
|
|||||||
'post_install_update', 'INFO', data,
|
'post_install_update', 'INFO', data,
|
||||||
source='polling', entity='node', entity_id='',
|
source='polling', entity='node', entity_id='',
|
||||||
))
|
))
|
||||||
|
for key, version in new_versions.items():
|
||||||
|
notified_versions.setdefault(key, set()).add(version)
|
||||||
|
post_install_versions.save_notified_versions(notified_versions)
|
||||||
|
|
||||||
# ── Managed-installs update check (Sprint 14.7) ─────────────────
|
# ── Managed-installs update check (Sprint 14.7) ─────────────────
|
||||||
|
|
||||||
@@ -4178,26 +4187,33 @@ class ProxmoxHookWatcher:
|
|||||||
# smartd and other system mail contains verbose boilerplate.
|
# smartd and other system mail contains verbose boilerplate.
|
||||||
# Extract just the actionable warning/error lines.
|
# Extract just the actionable warning/error lines.
|
||||||
if pve_type == 'system-mail' and message:
|
if pve_type == 'system-mail' and message:
|
||||||
clean_lines = []
|
# apt-listchanges is package-maintainer NEWS, not diagnostic
|
||||||
for line in message.split('\n'):
|
# boilerplate. Preserve it verbatim (including paragraphs and
|
||||||
stripped = line.strip()
|
# signatures) so ProxMenux only attributes the source and never
|
||||||
# Skip boilerplate lines
|
# silently edits or truncates the upstream notice.
|
||||||
if not stripped:
|
if event_type == 'apt_listchanges':
|
||||||
continue
|
data['reason'] = message.strip()
|
||||||
if stripped.startswith('This message was generated'):
|
else:
|
||||||
continue
|
clean_lines = []
|
||||||
if stripped.startswith('For details see'):
|
for line in message.split('\n'):
|
||||||
continue
|
stripped = line.strip()
|
||||||
if stripped.startswith('You can also use'):
|
# Skip boilerplate lines
|
||||||
continue
|
if not stripped:
|
||||||
if stripped.startswith('The original message'):
|
continue
|
||||||
continue
|
if stripped.startswith('This message was generated'):
|
||||||
if stripped.startswith('Another message will'):
|
continue
|
||||||
continue
|
if stripped.startswith('For details see'):
|
||||||
if stripped.startswith('host name:') or stripped.startswith('DNS domain:'):
|
continue
|
||||||
continue
|
if stripped.startswith('You can also use'):
|
||||||
clean_lines.append(stripped)
|
continue
|
||||||
data['reason'] = '\n'.join(clean_lines).strip() if clean_lines else message.strip()[:500]
|
if stripped.startswith('The original message'):
|
||||||
|
continue
|
||||||
|
if stripped.startswith('Another message will'):
|
||||||
|
continue
|
||||||
|
if stripped.startswith('host name:') or stripped.startswith('DNS domain:'):
|
||||||
|
continue
|
||||||
|
clean_lines.append(stripped)
|
||||||
|
data['reason'] = '\n'.join(clean_lines).strip() if clean_lines else message.strip()[:500]
|
||||||
|
|
||||||
# Extract VMID and VM name from message for vzdump events
|
# Extract VMID and VM name from message for vzdump events
|
||||||
if pve_type == 'vzdump' and message:
|
if pve_type == 'vzdump' and message:
|
||||||
@@ -4319,6 +4335,14 @@ class ProxmoxHookWatcher:
|
|||||||
msg_lower = (message or '').lower()
|
msg_lower = (message or '').lower()
|
||||||
title_lower_sm = (title or '').lower()
|
title_lower_sm = (title or '').lower()
|
||||||
|
|
||||||
|
# apt-listchanges forwards legitimate upstream package NEWS through
|
||||||
|
# PVE's generic system-mail bucket. Keep it, but classify it as an
|
||||||
|
# update notice of its own so the template can identify the source
|
||||||
|
# clearly instead of making package-maintainer prose look like a
|
||||||
|
# recommendation written by ProxMenux.
|
||||||
|
if 'apt-listchanges' in title_lower_sm or 'apt-listchanges' in msg_lower[:500]:
|
||||||
|
return 'apt_listchanges', 'node', ''
|
||||||
|
|
||||||
# ── Record disk observation regardless of noise filter ──
|
# ── Record disk observation regardless of noise filter ──
|
||||||
# Even "noise" events are recorded as observations so the user
|
# Even "noise" events are recorded as observations so the user
|
||||||
# can see them in the Storage UI. We just don't send notifications.
|
# can see them in the Storage UI. We just don't send notifications.
|
||||||
|
|||||||
@@ -1350,6 +1350,20 @@ class NotificationManager:
|
|||||||
# If AI is enabled AND rich_format is on, AI will include emojis directly
|
# If AI is enabled AND rich_format is on, AI will include emojis directly
|
||||||
# Pass channel_type so AI knows whether to append original (email only)
|
# Pass channel_type so AI knows whether to append original (email only)
|
||||||
channel_ai_config = {**ai_config, 'channel_type': ch_name}
|
channel_ai_config = {**ai_config, 'channel_type': ch_name}
|
||||||
|
# Availability notices are factual inventories, not advice.
|
||||||
|
# Even when the user enables experimental AI suggestions for
|
||||||
|
# diagnostic alerts, update announcements must only translate
|
||||||
|
# and format the versions/actions already supplied by the
|
||||||
|
# deterministic template.
|
||||||
|
if event_type in {
|
||||||
|
'update_available', 'update_summary', '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',
|
||||||
|
}:
|
||||||
|
channel_ai_config['ai_allow_suggestions'] = False
|
||||||
|
|
||||||
# Isolate the AI/enrich block in its own try so a failure
|
# Isolate the AI/enrich block in its own try so a failure
|
||||||
# here (raised from enrich_context_for_ai or any other
|
# here (raised from enrich_context_for_ai or any other
|
||||||
@@ -1979,7 +1993,6 @@ class NotificationManager:
|
|||||||
# cooldown resumes after that first post-restart send.
|
# cooldown resumes after that first post-restart send.
|
||||||
'update_summary',
|
'update_summary',
|
||||||
'proxmenux_update',
|
'proxmenux_update',
|
||||||
'post_install_update',
|
|
||||||
'pve_update',
|
'pve_update',
|
||||||
'update_available',
|
'update_available',
|
||||||
'nvidia_driver_update_available',
|
'nvidia_driver_update_available',
|
||||||
|
|||||||
@@ -1033,6 +1033,17 @@ TEMPLATES = {
|
|||||||
# /etc/aliases or removing MAILTO from the cron job. Audit Tier 6
|
# /etc/aliases or removing MAILTO from the cron job. Audit Tier 6
|
||||||
# — `system_mail` toggle no visible en UI / reportado por usuario.
|
# — `system_mail` toggle no visible en UI / reportado por usuario.
|
||||||
},
|
},
|
||||||
|
'apt_listchanges': {
|
||||||
|
'title': '{hostname}: {pve_title}',
|
||||||
|
'body': (
|
||||||
|
'Upstream package information forwarded by Proxmox VE through '
|
||||||
|
'apt-listchanges. The following text comes from the package '
|
||||||
|
'maintainer and is not a ProxMenux recommendation.\n\n{reason}'
|
||||||
|
),
|
||||||
|
'label': 'apt-listchanges package notices',
|
||||||
|
'group': 'updates',
|
||||||
|
'default_enabled': True,
|
||||||
|
},
|
||||||
'webhook_test': {
|
'webhook_test': {
|
||||||
'title': '{hostname}: Webhook test received',
|
'title': '{hostname}: Webhook test received',
|
||||||
'body': 'PVE webhook connectivity test successful.\n{reason}',
|
'body': 'PVE webhook connectivity test successful.\n{reason}',
|
||||||
@@ -1272,9 +1283,9 @@ TEMPLATES = {
|
|||||||
'post_install_update': {
|
'post_install_update': {
|
||||||
'title': '{hostname}: {count} ProxMenux optimization update(s) available',
|
'title': '{hostname}: {count} ProxMenux optimization update(s) available',
|
||||||
'body': (
|
'body': (
|
||||||
'{count} optimization update(s) detected on this host.\n\n'
|
'{count} ProxMenux optimization update(s) available on this host.\n\n'
|
||||||
'🛠️ Tools:\n{tool_list}\n\n'
|
'🛠️ Available versions:\n{tool_list}\n\n'
|
||||||
'💡 How to apply:\n'
|
'💡 Apply from:\n'
|
||||||
' • ProxMenux Monitor → Settings → ProxMenux Optimizations\n'
|
' • ProxMenux Monitor → Settings → ProxMenux Optimizations\n'
|
||||||
' • Or run the post-install menu (option 2) → "Apply available updates"'
|
' • Or run the post-install menu (option 2) → "Apply available updates"'
|
||||||
),
|
),
|
||||||
@@ -1733,6 +1744,7 @@ CATEGORY_EMOJI = {
|
|||||||
EVENT_EMOJI = {
|
EVENT_EMOJI = {
|
||||||
# VM / CT
|
# VM / CT
|
||||||
'lxc_updates_available': '\U0001F4E6', # \uD83D\uDCE6 package \u2014 pending CT updates
|
'lxc_updates_available': '\U0001F4E6', # \uD83D\uDCE6 package \u2014 pending CT updates
|
||||||
|
'apt_listchanges': '\U0001F4E6', # package-maintainer NEWS via PVE mail
|
||||||
'lxc_update_applied': '\u2705', # \u2705 check \u2014 update applied
|
'lxc_update_applied': '\u2705', # \u2705 check \u2014 update applied
|
||||||
'app_update_available': '\U0001F195', # \ud83c\udd95 NEW \u2014 upstream app release
|
'app_update_available': '\U0001F195', # \ud83c\udd95 NEW \u2014 upstream app release
|
||||||
'docker_stack_update_available': '\U0001F433',
|
'docker_stack_update_available': '\U0001F433',
|
||||||
|
|||||||
@@ -92,6 +92,73 @@ def _read_text(path: Path) -> str:
|
|||||||
return ""
|
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
|
# Bash script parser
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -353,13 +420,10 @@ def scan(persist: bool = True) -> dict[str, Any]:
|
|||||||
|
|
||||||
if persist:
|
if persist:
|
||||||
try:
|
try:
|
||||||
_UPDATES_JSON.parent.mkdir(parents=True, exist_ok=True)
|
_write_persisted_snapshot(
|
||||||
_UPDATES_JSON.write_text(
|
snapshot["scanned_at"],
|
||||||
json.dumps(
|
updates,
|
||||||
{"scanned_at": snapshot["scanned_at"], "updates": updates},
|
_load_notified_versions_from_disk(),
|
||||||
indent=2,
|
|
||||||
),
|
|
||||||
encoding="utf-8",
|
|
||||||
)
|
)
|
||||||
except OSError:
|
except OSError:
|
||||||
# Writing the on-disk cache is best-effort. If /usr/local
|
# Writing the on-disk cache is best-effort. If /usr/local
|
||||||
|
|||||||
Reference in New Issue
Block a user