diff --git a/containers/libreportal/frontend/components/updater/js/updater-page.js b/containers/libreportal/frontend/components/updater/js/updater-page.js index fdb229a..f386e2d 100644 --- a/containers/libreportal/frontend/components/updater/js/updater-page.js +++ b/containers/libreportal/frontend/components/updater/js/updater-page.js @@ -161,6 +161,23 @@ class UpdaterPage { for (const a of this.cves.apps) cveByApp[a.name] = a.cves || []; } let base = (this.updates && Array.isArray(this.updates.apps)) ? this.updates.apps : null; + // updates.json is a scan-time snapshot and nothing rewrites it at + // uninstall, so for up to a scan cycle it can still carry an app that no + // longer exists — Matrix kept a ghost row (and an "installed"-looking + // presence) for half an hour after being removed. window.apps IS refreshed + // by the uninstall flow, so where it can answer, an app it does not list + // as installed is dropped from the merge. Only applied when window.apps + // has content: this page must keep degrading gracefully when the installed + // list has not loaded, per the generator's own contract. + if (base && Array.isArray(window.apps) && window.apps.length) { + const installedSlugs = new Set( + window.apps + .filter(a => a && a.installed) + .map(a => ((a.command || '').split(' ').pop() || '').toLowerCase()) + .filter(Boolean) + ); + base = base.filter(a => installedSlugs.has(String(a.name).toLowerCase())); + } if (!base) { const installed = (window.apps || []).filter(a => a && (a.status === 1 || a.installed || a.is_installed)); base = installed.map(a => ({ diff --git a/scripts/docker/app/uninstall/uninstall_app.sh b/scripts/docker/app/uninstall/uninstall_app.sh index 57f38a5..4138932 100755 --- a/scripts/docker/app/uninstall/uninstall_app.sh +++ b/scripts/docker/app/uninstall/uninstall_app.sh @@ -82,6 +82,25 @@ dockerUninstallApp() webuiContainerSetup $stored_app_name uninstall; + # Drop the app's rows from the updater's generated data right now. + # updates.json / cves.json are scan-time snapshots on a 30-minute + # cadence, so without this the Updates tab kept showing a ghost row — + # an uninstalled app still listed as "up to date" — until the next + # scan happened to run. Surgical delete rather than a rescan: a full + # updater scan re-runs CVE checks against every image and has no place + # inside an uninstall. + local _upd_gen="${containers_dir}libreportal/frontend/data/updater/generated" + local _upd_f + for _upd_f in updates.json cves.json; do + if [[ -f "$_upd_gen/$_upd_f" ]] && command -v jq >/dev/null 2>&1; then + local _upd_tmp; _upd_tmp="$(mktemp)" + if jq --arg n "$stored_app_name" '.apps = [(.apps // [])[] | select(.name != $n)]' "$_upd_gen/$_upd_f" > "$_upd_tmp" 2>/dev/null && [ -s "$_upd_tmp" ]; then + runFileWrite "$_upd_gen/$_upd_f" < "$_upd_tmp" + fi + rm -f "$_upd_tmp" + fi + done + # A removed app may have been routed through a network gateway (e.g. # gluetun); let each provider refresh its forwarded-port registration. # Each hook self-skips when its provider isn't installed.