Add missing cancel option to 15 more multi-instance services; add setup.sh --status

Same bug as the previous filebrowser/fmd fix: vaultwarden, immich,
audiobookshelf, homebox, rustdesk, emby, meshcentral, traccar, lyrion,
actualbudget, mealie, joplin, jellyfin, unifi, and ntfy all showed "Manage
that install (update / full reinstall / cancel)" when re-run against an
existing install, but choosing "1) Manage" fell straight through into the
same unconditional fresh-install flow every time regardless of choice —
no way to actually cancel or update in place. Wired all 15 up to
prompt_reinstall_mode, matching the reference pattern in
services/mattermost.sh: update pulls + restarts the existing container
without touching config, cancel leaves the install untouched, fresh falls
through to the existing full-install flow unchanged.

Also add `setup.sh --status`: a plain-text listing of every service with
its install state, using the exact same is_installed() calls the whiptail
checklist's [installed] marker uses. Exists so "is X actually installed"
can be answered by reading terminal output directly, without depending on
a whiptail checklist screen where a narrow/resized terminal can truncate
the "[installed]" suffix off-screen with no visible sign that happened.
This commit is contained in:
Claude
2026-08-10 14:39:34 +00:00
parent 4123662571
commit 11a4e249b6
16 changed files with 355 additions and 1 deletions
+22
View File
@@ -245,6 +245,28 @@ install_actualbudget() {
AB_DIR="$DOCKER_DIR/actualbudget-$_suffix"
CONTAINER="actualbudget-$_suffix"
log_info "New instance: $AB_DIR"
else
# "Manage that install" on THIS instance — the banner above promises
# update/fresh/cancel, so actually offer it instead of falling straight
# through into the same unconditional-overwrite flow as a new install.
if [[ -f "$AB_DIR/docker-compose.yml" ]]; then
local MODE=""
prompt_reinstall_mode MODE
case "$MODE" in
update)
log_info "Refreshing the Actual Budget image only — existing config, port, and Caddy setup are left as-is."
( cd "$AB_DIR" && docker compose pull && docker compose up -d ) \
&& log_success "Actual Budget image refreshed" \
|| log_warning "Refresh failed — check: docker compose -f $AB_DIR/docker-compose.yml logs"
return 0
;;
cancel)
log_info "Leaving the existing install as-is."
return 0
;;
fresh) ;; # fall through to the full install flow below
esac
fi
fi
fi
+22
View File
@@ -248,6 +248,28 @@ install_audiobookshelf() {
CONTAINER="audiobookshelf-$_suffix"
DEFAULT_AUDIOBOOKS="$ACTUAL_HOME/audiobooks-$_suffix"
log_info "New instance: $ABS_DIR"
else
# "Manage that install" on THIS instance — the banner above promises
# update/fresh/cancel, so actually offer it instead of falling straight
# through into the same unconditional-overwrite flow as a new install.
if [[ -f "$ABS_DIR/docker-compose.yml" ]]; then
local MODE=""
prompt_reinstall_mode MODE
case "$MODE" in
update)
log_info "Refreshing the Audiobookshelf image only — existing config, port, and Caddy setup are left as-is."
( cd "$ABS_DIR" && docker compose pull && docker compose up -d ) \
&& log_success "Audiobookshelf image refreshed" \
|| log_warning "Refresh failed — check: docker compose -f $ABS_DIR/docker-compose.yml logs"
return 0
;;
cancel)
log_info "Leaving the existing install as-is."
return 0
;;
fresh) ;; # fall through to the full install flow below
esac
fi
fi
fi
+22
View File
@@ -262,6 +262,28 @@ install_emby() {
CONTAINER="emby-$_suffix"
DEFAULT_MEDIA="$ACTUAL_HOME/media-$_suffix"
log_info "New instance: $EMBY_DIR"
else
# "Manage that install" on THIS instance — the banner above promises
# update/fresh/cancel, so actually offer it instead of falling straight
# through into the same unconditional-overwrite flow as a new install.
if [[ -f "$EMBY_DIR/docker-compose.yml" ]]; then
local MODE=""
prompt_reinstall_mode MODE
case "$MODE" in
update)
log_info "Refreshing the Emby image only — existing config, port, and Caddy setup are left as-is."
( cd "$EMBY_DIR" && docker compose pull && docker compose up -d ) \
&& log_success "Emby image refreshed" \
|| log_warning "Refresh failed — check: docker compose -f $EMBY_DIR/docker-compose.yml logs"
return 0
;;
cancel)
log_info "Leaving the existing install as-is."
return 0
;;
fresh) ;; # fall through to the full install flow below
esac
fi
fi
fi
+22
View File
@@ -243,6 +243,28 @@ install_homebox() {
HB_DIR="$DOCKER_DIR/homebox-$_suffix"
CONTAINER="homebox-$_suffix"
log_info "New instance: $HB_DIR"
else
# "Manage that install" on THIS instance — the banner above promises
# update/fresh/cancel, so actually offer it instead of falling straight
# through into the same unconditional-overwrite flow as a new install.
if [[ -f "$HB_DIR/docker-compose.yml" ]]; then
local MODE=""
prompt_reinstall_mode MODE
case "$MODE" in
update)
log_info "Refreshing the Homebox image only — existing config, port, and Caddy setup are left as-is."
( cd "$HB_DIR" && docker compose pull && docker compose up -d ) \
&& log_success "Homebox image refreshed" \
|| log_warning "Refresh failed — check: docker compose -f $HB_DIR/docker-compose.yml logs"
return 0
;;
cancel)
log_info "Leaving the existing install as-is."
return 0
;;
fresh) ;; # fall through to the full install flow below
esac
fi
fi
fi
+22
View File
@@ -266,6 +266,28 @@ install_immich() {
C_DB="immich_postgres_$_suffix"
DEFAULT_PHOTOS="$ACTUAL_HOME/photos-$_suffix"
log_info "New instance: $IMMICH_DIR"
else
# "Manage that install" on THIS instance — the banner above promises
# update/fresh/cancel, so actually offer it instead of falling straight
# through into the same unconditional-overwrite flow as a new install.
if [[ -f "$IMMICH_DIR/docker-compose.yml" ]]; then
local MODE=""
prompt_reinstall_mode MODE
case "$MODE" in
update)
log_info "Refreshing the Immich image only — existing config, port, and Caddy setup are left as-is."
( cd "$IMMICH_DIR" && docker compose pull && docker compose up -d ) \
&& log_success "Immich image refreshed" \
|| log_warning "Refresh failed — check: docker compose -f $IMMICH_DIR/docker-compose.yml logs"
return 0
;;
cancel)
log_info "Leaving the existing install as-is."
return 0
;;
fresh) ;; # fall through to the full install flow below
esac
fi
fi
fi
+22
View File
@@ -257,6 +257,28 @@ install_jellyfin() {
log_warning "DLNA/discovery (1900/udp, 7359/udp) are fixed, host-wide ports already"
log_warning "claimed by the first instance — this instance skips them (web UI and"
log_warning "app-based streaming are unaffected; DLNA auto-discovery is not)."
else
# "Manage that install" on THIS instance — the banner above promises
# update/fresh/cancel, so actually offer it instead of falling straight
# through into the same unconditional-overwrite flow as a new install.
if [[ -f "$JELLYFIN_DIR/docker-compose.yml" ]]; then
local MODE=""
prompt_reinstall_mode MODE
case "$MODE" in
update)
log_info "Refreshing the Jellyfin image only — existing config, port, and Caddy setup are left as-is."
( cd "$JELLYFIN_DIR" && docker compose pull && docker compose up -d ) \
&& log_success "Jellyfin image refreshed" \
|| log_warning "Refresh failed — check: docker compose -f $JELLYFIN_DIR/docker-compose.yml logs"
return 0
;;
cancel)
log_info "Leaving the existing install as-is."
return 0
;;
fresh) ;; # fall through to the full install flow below
esac
fi
fi
fi
+22
View File
@@ -248,6 +248,28 @@ install_joplin() {
CONTAINER="joplin-$_suffix"
DB_CONTAINER="joplin-db-$_suffix"
log_info "New instance: $JOPLIN_DIR"
else
# "Manage that install" on THIS instance — the banner above promises
# update/fresh/cancel, so actually offer it instead of falling straight
# through into the same unconditional-overwrite flow as a new install.
if [[ -f "$JOPLIN_DIR/docker-compose.yml" ]]; then
local MODE=""
prompt_reinstall_mode MODE
case "$MODE" in
update)
log_info "Refreshing the Joplin Server image only — existing config, port, and Caddy setup are left as-is."
( cd "$JOPLIN_DIR" && docker compose pull && docker compose up -d ) \
&& log_success "Joplin Server image refreshed" \
|| log_warning "Refresh failed — check: docker compose -f $JOPLIN_DIR/docker-compose.yml logs"
return 0
;;
cancel)
log_info "Leaving the existing install as-is."
return 0
;;
fresh) ;; # fall through to the full install flow below
esac
fi
fi
fi
+22
View File
@@ -279,6 +279,28 @@ install_lyrion() {
log_warning "loses zero-config Chromecast/Squeezebox discovery — enter its address"
log_warning "manually in the Squeezer app / Squeezebox firmware instead."
log_info "New instance: $LYRION_DIR"
else
# "Manage that install" on THIS instance — the banner above promises
# update/fresh/cancel, so actually offer it instead of falling straight
# through into the same unconditional-overwrite flow as a new install.
if [[ -f "$LYRION_DIR/docker-compose.yml" ]]; then
local MODE=""
prompt_reinstall_mode MODE
case "$MODE" in
update)
log_info "Refreshing the Lyrion image only — existing config, port, and Caddy setup are left as-is."
( cd "$LYRION_DIR" && docker compose pull && docker compose up -d ) \
&& log_success "Lyrion image refreshed" \
|| log_warning "Refresh failed — check: docker compose -f $LYRION_DIR/docker-compose.yml logs"
return 0
;;
cancel)
log_info "Leaving the existing install as-is."
return 0
;;
fresh) ;; # fall through to the full install flow below
esac
fi
fi
fi
+22
View File
@@ -246,6 +246,28 @@ install_mealie() {
MEALIE_DIR="$DOCKER_DIR/mealie-$_suffix"
CONTAINER="mealie-$_suffix"
log_info "New instance: $MEALIE_DIR"
else
# "Manage that install" on THIS instance — the banner above promises
# update/fresh/cancel, so actually offer it instead of falling straight
# through into the same unconditional-overwrite flow as a new install.
if [[ -f "$MEALIE_DIR/docker-compose.yml" ]]; then
local MODE=""
prompt_reinstall_mode MODE
case "$MODE" in
update)
log_info "Refreshing the Mealie image only — existing config, port, and Caddy setup are left as-is."
( cd "$MEALIE_DIR" && docker compose pull && docker compose up -d ) \
&& log_success "Mealie image refreshed" \
|| log_warning "Refresh failed — check: docker compose -f $MEALIE_DIR/docker-compose.yml logs"
return 0
;;
cancel)
log_info "Leaving the existing install as-is."
return 0
;;
fresh) ;; # fall through to the full install flow below
esac
fi
fi
fi
+22
View File
@@ -257,6 +257,28 @@ install_meshcentral() {
MC_DIR="$DOCKER_DIR/meshcentral-$_suffix"
CONTAINER="meshcentral-$_suffix"
log_info "New instance: $MC_DIR"
else
# "Manage that install" on THIS instance — the banner above promises
# update/fresh/cancel, so actually offer it instead of falling straight
# through into the same unconditional-overwrite flow as a new install.
if [[ -f "$MC_DIR/docker-compose.yml" ]]; then
local MODE=""
prompt_reinstall_mode MODE
case "$MODE" in
update)
log_info "Refreshing the MeshCentral image only — existing config, port, and Caddy setup are left as-is."
( cd "$MC_DIR" && docker compose pull && docker compose up -d ) \
&& log_success "MeshCentral image refreshed" \
|| log_warning "Refresh failed — check: docker compose -f $MC_DIR/docker-compose.yml logs"
return 0
;;
cancel)
log_info "Leaving the existing install as-is."
return 0
;;
fresh) ;; # fall through to the full install flow below
esac
fi
fi
fi
+22
View File
@@ -251,6 +251,28 @@ install_ntfy() {
NTFY_DIR="$DOCKER_DIR/ntfy-$_suffix"
CONTAINER="ntfy-$_suffix"
log_info "New instance: $NTFY_DIR"
else
# "Manage that install" on THIS instance — the banner above promises
# update/fresh/cancel, so actually offer it instead of falling straight
# through into the same unconditional-overwrite flow as a new install.
if [[ -f "$NTFY_DIR/docker-compose.yml" ]]; then
local MODE=""
prompt_reinstall_mode MODE
case "$MODE" in
update)
log_info "Refreshing the ntfy image only — existing config, port, and Caddy setup are left as-is."
( cd "$NTFY_DIR" && docker compose pull && docker compose up -d ) \
&& log_success "ntfy image refreshed" \
|| log_warning "Refresh failed — check: docker compose -f $NTFY_DIR/docker-compose.yml logs"
return 0
;;
cancel)
log_info "Leaving the existing install as-is."
return 0
;;
fresh) ;; # fall through to the full install flow below
esac
fi
fi
fi
+22
View File
@@ -165,6 +165,28 @@ install_rustdesk() {
RD_DIR="$DOCKER_DIR/rustdesk-$_suffix"
CONTAINER="rustdesk-$_suffix"
log_info "New instance: $RD_DIR"
else
# "Manage that install" on THIS instance — the banner above promises
# update/fresh/cancel, so actually offer it instead of falling straight
# through into the same unconditional-overwrite flow as a new install.
if [[ -f "$RD_DIR/docker-compose.yml" ]]; then
local MODE=""
prompt_reinstall_mode MODE
case "$MODE" in
update)
log_info "Refreshing the RustDesk image only — existing config, port, and Caddy setup are left as-is."
( cd "$RD_DIR" && docker compose pull && docker compose up -d ) \
&& log_success "RustDesk image refreshed" \
|| log_warning "Refresh failed — check: docker compose -f $RD_DIR/docker-compose.yml logs"
return 0
;;
cancel)
log_info "Leaving the existing install as-is."
return 0
;;
fresh) ;; # fall through to the full install flow below
esac
fi
fi
fi
+22
View File
@@ -277,6 +277,28 @@ install_traccar() {
PROTO_MAX=$((5150 + _offset))
log_info "New instance: $TRACCAR_DIR (device protocols $PROTO_MIN-$PROTO_MAX)"
else
# "Manage that install" on THIS instance — the banner above promises
# update/fresh/cancel, so actually offer it instead of falling straight
# through into the same unconditional-overwrite flow as a new install.
if [[ -f "$TRACCAR_DIR/docker-compose.yml" ]]; then
local MODE=""
prompt_reinstall_mode MODE
case "$MODE" in
update)
log_info "Refreshing the Traccar image only — existing config, port, and Caddy setup are left as-is."
( cd "$TRACCAR_DIR" && docker compose pull && docker compose up -d ) \
&& log_success "Traccar image refreshed" \
|| log_warning "Refresh failed — check: docker compose -f $TRACCAR_DIR/docker-compose.yml logs"
return 0
;;
cancel)
log_info "Leaving the existing install as-is."
return 0
;;
fresh) ;; # fall through to the full install flow below
esac
fi
fi
fi
+22
View File
@@ -166,6 +166,28 @@ install_unifi() {
DB_CONTAINER="unifi-db-$_suffix"
APP_CONTAINER="unifi-app-$_suffix"
log_info "New instance: $UNIFI_DIR"
else
# "Manage that install" on THIS instance — the banner above promises
# update/fresh/cancel, so actually offer it instead of falling straight
# through into the same unconditional-overwrite flow as a new install.
if [[ -f "$UNIFI_DIR/docker-compose.yml" ]]; then
local MODE=""
prompt_reinstall_mode MODE
case "$MODE" in
update)
log_info "Refreshing the UniFi image only — existing config, port, and Caddy setup are left as-is."
( cd "$UNIFI_DIR" && docker compose pull && docker compose up -d ) \
&& log_success "UniFi image refreshed" \
|| log_warning "Refresh failed — check: docker compose -f $UNIFI_DIR/docker-compose.yml logs"
return 0
;;
cancel)
log_info "Leaving the existing install as-is."
return 0
;;
fresh) ;; # fall through to the full install flow below
esac
fi
fi
fi
+22
View File
@@ -265,6 +265,28 @@ install_vaultwarden() {
VW_DIR="$DOCKER_DIR/vaultwarden-$_suffix"
CONTAINER="vaultwarden-$_suffix"
log_info "New instance: $VW_DIR"
else
# "Manage that install" on THIS instance — the banner above promises
# update/fresh/cancel, so actually offer it instead of falling straight
# through into the same unconditional-overwrite flow as a new install.
if [[ -f "$VW_DIR/docker-compose.yml" ]]; then
local MODE=""
prompt_reinstall_mode MODE
case "$MODE" in
update)
log_info "Refreshing the Vaultwarden image only — existing config, port, and Caddy setup are left as-is."
( cd "$VW_DIR" && docker compose pull && docker compose up -d ) \
&& log_success "Vaultwarden image refreshed" \
|| log_warning "Refresh failed — check: docker compose -f $VW_DIR/docker-compose.yml logs"
return 0
;;
cancel)
log_info "Leaving the existing install as-is."
return 0
;;
fresh) ;; # fall through to the full install flow below
esac
fi
fi
fi
+25 -1
View File
@@ -6,6 +6,7 @@
# category menu you loop through
# sudo ./setup.sh <service> ... install one or more services directly
# ./setup.sh --list list available services (grouped)
# ./setup.sh --status list services with install status (no whiptail)
# ./setup.sh --version print version
#
# Flags:
@@ -33,13 +34,14 @@ declare -A SERVICE_PRIORITY=( [caddy]=1 [crowdsec]=2 [authelia]=3 )
declare -A SERVICE_ALIAS=( [asterisk-digital-ocean]=asterisk )
# ── Parse flags / collect service names ──────────────────────────────────────
DRY_RUN=false; UNATTENDED=false; DO_LIST=false
DRY_RUN=false; UNATTENDED=false; DO_LIST=false; DO_STATUS=false
REQUESTED=()
for arg in "$@"; do
case "$arg" in
--dry-run) DRY_RUN=true ;;
--unattended) UNATTENDED=true ;;
--list|-l) DO_LIST=true ;;
--status) DO_STATUS=true ;;
--version|-V) cat "$HERE/VERSION" 2>/dev/null || echo "unknown"; exit 0 ;;
-h|--help) sed -n '2,18p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'; exit 0 ;;
-*) echo "Unknown flag: $arg" >&2; exit 1 ;;
@@ -130,6 +132,25 @@ list_services() {
echo ""
}
# Plain-text version of the whiptail checklist's [installed] marker — same
# is_installed() calls, no whiptail involved. Exists so "is X actually
# installed" can be answered by reading terminal output directly instead of
# a checklist screen, where a narrow/resized terminal can truncate or wrap
# the "[installed]" suffix off-screen without it being obvious that's what
# happened.
print_status() {
local g name marker
while IFS= read -r g; do
echo ""; echo "── ${g^^} ──"
while IFS= read -r name; do
marker="not installed"
is_installed "$name" && marker="INSTALLED"
printf " %-20s %-12s %s\n" "$name" "$marker" "${SERVICE_DESC[$name]}"
done < <(services_in_group "$g")
done < <(groups_present)
echo ""
}
# ── Site defaults wizard ──────────────────────────────────────────────────────
# Prompts for timezone, base domain, and Caddy network name; saves to .config.
# Run directly: sudo ./setup.sh configure
@@ -206,6 +227,9 @@ run_site_configure() {
# ── --list ───────────────────────────────────────────────────────────────────
if [ "$DO_LIST" = true ]; then list_services; exit 0; fi
# ── --status ─────────────────────────────────────────────────────────────────
if [ "$DO_STATUS" = true ]; then print_status; exit 0; fi
# ── configure: show/update site-wide defaults ────────────────────────────────
if [ "${REQUESTED[*]:-}" = "configure" ]; then
require_root