librelad dd68c04fec fix(updater): stop showing ghost rows for uninstalled apps
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 <noreply@anthropic.com>
2026-08-21 00:50:11 +01:00

146 lines
5.9 KiB
Bash
Executable File

#!/bin/bash
dockerUninstallApp()
{
local app_name="$1"
# Optional: delete the app's docker images too. Default false so a
# plain `libreportal app uninstall <app>` keeps cached images for a
# quick reinstall. The WebUI's Uninstall modal exposes this as the
# "Also delete docker image" checkbox.
local delete_images="${2:-false}"
local delete_tasks="${3:-false}"
local stored_app_name=$app_name
if [[ "$stored_app_name" == "" ]]; then
isError "No app_name provided, unable to continue..."
else
isHeader "Uninstalling $stored_app_name"
initializeAppVariables $stored_app_name;
((menu_number++))
echo ""
echo "---- $menu_number. Removing app where docker compose is installed"
echo ""
dockerComposeDownRemove $stored_app_name;
# App-specific uninstall hook. Host-installed apps (and any app needing
# teardown beyond the generic docker/DB cleanup) define an uninstall<App>
# function — it runs the app-specific bits (apt purge, systemd units,
# cscli state, ...) while the shared compose/data/DB/WebUI teardown below
# stays generic. The CLI and menu both route uninstalls through here, so
# this is the single point every uninstall passes through.
local app_name_ucfirst="$(tr '[:lower:]' '[:upper:]' <<< ${stored_app_name:0:1})${stored_app_name:1}"
local uninstallFuncName="uninstall${app_name_ucfirst}"
if declare -f "$uninstallFuncName" >/dev/null 2>&1; then
((menu_number++))
echo ""
echo "---- $menu_number. Running $stored_app_name-specific uninstall steps"
echo ""
"$uninstallFuncName"
fi
((menu_number++))
echo ""
if [[ "$delete_images" == "true" ]]; then
echo "---- $menu_number. Removing Docker images for the app"
echo ""
dockerRemoveAppImages $stored_app_name;
else
echo "---- $menu_number. Keeping Docker images"
echo ""
isNotice "Docker images for '$stored_app_name' left in place. A reinstall will reuse them."
fi
((menu_number++))
echo ""
echo "---- $menu_number. Deleting all app data from docker folder"
echo ""
dockerDeleteData $stored_app_name;
((menu_number++))
echo ""
echo "---- $menu_number. Removing unused Docker networks."
echo ""
dockerPruneAppNetworks $stored_app_name;
((menu_number++))
echo ""
echo "---- $menu_number. Removing app from the database"
echo ""
ipRemoveFromDatabase $stored_app_name;
portsRemoveFromDatabase $stored_app_name;
databaseUninstallApp $stored_app_name;
((menu_number++))
echo ""
echo "---- $menu_number. Updating the WebUI config file."
echo ""
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.
local _np_fn
for _np_fn in $(compgen -A function 2>/dev/null | grep '^appNetworkRegisterPorts_'); do
"$_np_fn"
done
if [[ "$delete_tasks" == "true" ]]; then
((menu_number++))
echo ""
echo "---- $menu_number. Removing related task history"
echo ""
local _tasks_dir="${containers_dir}libreportal/frontend/data/tasks"
local _removed=0
if [[ -d "$_tasks_dir" ]]; then
for _tf in "$_tasks_dir"/task_*.json; do
[[ ! -f "$_tf" ]] && continue
# Skip in-flight tasks — that includes the uninstall task
# we're currently inside, plus anything queued or running.
if runFileOp grep -qE "\"status\"[[:space:]]*:[[:space:]]*\"(running|queued|pending)\"" "$_tf" 2>/dev/null; then
continue
fi
if runFileOp grep -q "\"app\"[[:space:]]*:[[:space:]]*\"${stored_app_name}\"" "$_tf" 2>/dev/null; then
local _id=$(basename "$_tf" .json)
runFileOp rm -f "$_tf" "$_tasks_dir/${_id}.log" "$_tasks_dir/${_id}.cancel" 2>/dev/null
_removed=$((_removed + 1))
fi
done
fi
isSuccessful "Removed $_removed task record(s) for $stored_app_name (in-flight task left for the WebUI to clean up after completion)."
fi
((menu_number++))
echo ""
isSuccessful "$stored_app_name has been removed from your system!"
echo ""
menu_number=0
cd
fi
}