From dd68c04fece2a471a3d966be5043f4e3ea65eb10 Mon Sep 17 00:00:00 2001 From: librelad Date: Fri, 21 Aug 2026 00:50:11 +0100 Subject: [PATCH] fix(updater): stop showing ghost rows for uninstalled apps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Matrix was uninstalled and the Updates tab kept listing it as up to date. Not an instance problem — updates.json and cves.json are scan-time snapshots on a 30-minute cadence, and nothing rewrote them at uninstall, so any removed app haunted every updater surface until the next scan happened to run. The backend was never wrong: the DB, the apps data and the app's own page all said uninstalled within seconds. Fixed at both ends. Uninstall now deletes the app's rows from both generated files, surgically — a full rescan re-runs CVE checks against every image and has no place inside an uninstall. And the updater's merge drops any row whose app window.apps does not list as installed, which covers every other way the snapshot can go stale (a crashed uninstall, a hand-edited file, the next bug). The filter only applies when the installed list has actually loaded, preserving the page's degrade-gracefully contract when it has not. The stale Matrix rows on this install were purged the same surgical way; the tab now shows 14 rows with the merge still intact. Co-Authored-By: Claude Opus 5 --- .../components/updater/js/updater-page.js | 17 +++++++++++++++++ scripts/docker/app/uninstall/uninstall_app.sh | 19 +++++++++++++++++++ 2 files changed, 36 insertions(+) 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.