overhaul app tracking and update orchestration

- Generate and ship a verified 389-app tracking catalog with 23 runtime overrides, fallback detectors, ports, logos, and Docker Hub tag previews.
- Support modern Proxmox VE Helper-Scripts markers, historical installations, and official or manual app deployments.
- Rework the LXC App and Updates tabs with cached suggestions, explicit discovery, version tracking, web links, custom updaters, and complete i18n.
- Add independent OS, app, Docker Engine, Docker image, bulk, and scheduled update targets.
- Add digest-based Docker inventory, Compose dependency grouping, safe standalone-container recreation with rollback, and package-scoped Docker Engine updates.
- Refresh per-LXC caches after lifecycle and update tasks, then emit idempotent notifications based on the verified final state.
- Harden Coral USB recovery by removing orphaned gasket DKMS registrations and validating that dpkg is healthy before reporting success.
This commit is contained in:
MacRimi
2026-08-23 12:43:03 +02:00
parent 7244201810
commit 0251f77331
27 changed files with 11631 additions and 893 deletions
+45 -2
View File
@@ -22,7 +22,7 @@ import sqlite3
import subprocess
import threading
from queue import Queue
from typing import Optional, Dict, Any, Tuple
from typing import Optional, Dict, Any, Tuple, Callable
from pathlib import Path
@@ -1939,8 +1939,16 @@ class TaskWatcher:
'vzmigrate': ('migration_start', 'INFO'),
}
def __init__(self, event_queue: Queue):
def __init__(
self,
event_queue: Queue,
guest_lifecycle_callback: Optional[Callable[[str, str, str], None]] = None,
):
self._queue = event_queue
# Reuse the exact PVE task transition already responsible for
# VM/CT lifecycle notifications. Consumers such as the modal cache
# can subscribe without introducing a second status poller.
self._guest_lifecycle_callback = guest_lifecycle_callback
self._running = False
self._thread: Optional[threading.Thread] = None
# `_hostname` is exposed as a @property below so every read returns
@@ -2250,6 +2258,31 @@ class TaskWatcher:
# Determine entity type from task type
entity = 'ct' if task_type.startswith('vz') else 'vm'
# A completed PVE lifecycle task is the existing source of truth for
# start/stop/restart notifications. Publish the same transition to
# the optional cache listener before notification-only suppression
# (backup/startup aggregation, disabled channels, cooldowns) so cache
# correctness never depends on whether a message is delivered.
lifecycle_actions = {
'qmstart': ('qemu', 'start'),
'qmstop': ('qemu', 'stop'),
'qmshutdown': ('qemu', 'stop'),
'qmreboot': ('qemu', 'reboot'),
'qmreset': ('qemu', 'reboot'),
'vzstart': ('lxc', 'start'),
'vzstop': ('lxc', 'stop'),
'vzshutdown': ('lxc', 'stop'),
'vzreboot': ('lxc', 'reboot'),
}
lifecycle = lifecycle_actions.get(task_type)
if (lifecycle and self._guest_lifecycle_callback
and not is_error and (status == 'OK' or is_warning)):
try:
self._guest_lifecycle_callback(vmid, lifecycle[0], lifecycle[1])
except Exception as exc:
print(f'[TaskWatcher] guest lifecycle callback failed for '
f'{lifecycle[0]} {vmid}: {exc}', flush=True)
# Backup completion/failure and replication events are handled
# EXCLUSIVELY by the PVE webhook, which delivers richer data (full
@@ -3516,6 +3549,15 @@ class PollingCollector:
try:
import lxc_apps
lxc_apps.refresh_all_apps(force=False)
# Docker images have an independent lifecycle from both the OS
# packages and the Docker engine. Refresh their read-only
# registry digest inventory on the same daily cadence; this never
# pulls or recreates containers.
# This is the single automatic Docker registry comparison. Force
# the rolling pass itself so a user-triggered check shortly after
# yesterday's cycle cannot postpone the next automatic scan by an
# additional day. Normal UI reads remain cache-only for 24 hours.
lxc_apps.refresh_docker_inventories(force=True)
# After the refresh, emit `app_update_available` for every
# sidecar entry currently flagged with a pending upstream
# release. `check_app(force=False)` short-circuits on a
@@ -3526,6 +3568,7 @@ class PollingCollector:
# moment. `notification_manager` dedups by entity_id
# (vmid + app_id + latest_version) so repeated calls only
# deliver one notification per release.
lxc_apps.emit_all_pending_docker_stacks()
lxc_apps.emit_all_pending_updates()
except Exception as e:
print(f"[PollingCollector] lxc_apps refresh failed: {e}")