From 412366257149eef156c1e298231bef8be775ff37 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 10 Aug 2026 14:25:34 +0000 Subject: [PATCH 1/2] Fix fmd's broken Docker image and add missing cancel option to two installers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fmd.sh pointed at nulide/findmydevice, which no longer exists on Docker Hub — the project has moved twice (nulide/findmydevice -> gitlab.com/Nulide/findmydeviceserver -> gitlab.com/fmd-foss/fmd-server) and was rewritten from Node.js to Go+React along the way, confirmed against the current upstream repo and its GitLab container registry. This means the service never actually started for anyone who installed it before this fix ("pull access denied for nulide/findmydevice, repository does not exist"). Switch to registry.gitlab.com/fmd-foss/fmd-server:0 (GitLab's own registry has no "latest" tag; ":0" tracks the current major release the same way this repo's other services use a floating tag). The old FMD_ADMIN_PASSWORD model is gone from the app too — replaced with FMD_REGISTRATIONTOKEN (self-registration gated by a token instead of one shared admin login), and the database path moved from /fmd/data to /var/lib/fmd-server/db. Also: filebrowser.sh and fmd.sh both 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 — no way to actually cancel or update in place, contradicting both the banner text and the documented prompt_reinstall_mode contract (CLAUDE.md's "Update vs. fresh reinstall on rerun"). Wired both up to prompt_reinstall_mode, matching the reference pattern in services/mattermost.sh. The same gap exists in 15 other multi-instance services (vaultwarden, immich, audiobookshelf, homebox, rustdesk, emby, meshcentral, traccar, lyrion, actualbudget, mealie, joplin, jellyfin, unifi, ntfy) — not fixed here, flagged for a follow-up pass. --- services/filebrowser.sh | 22 +++++++++++++ services/fmd.sh | 70 +++++++++++++++++++++++++++++++---------- 2 files changed, 76 insertions(+), 16 deletions(-) diff --git a/services/filebrowser.sh b/services/filebrowser.sh index 5bbcebb..2362c74 100644 --- a/services/filebrowser.sh +++ b/services/filebrowser.sh @@ -242,6 +242,28 @@ install_filebrowser() { FB_DIR="$DOCKER_DIR/filebrowser-$_suffix" CONTAINER="filebrowser-$_suffix" log_info "New instance: $FB_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 "$FB_DIR/docker-compose.yml" ]]; then + local MODE="" + prompt_reinstall_mode MODE + case "$MODE" in + update) + log_info "Refreshing the FileBrowser image only — files-directory mount and Caddy config are left as-is." + ( cd "$FB_DIR" && docker compose pull && docker compose up -d ) \ + && log_success "FileBrowser image refreshed" \ + || log_warning "Refresh failed — check: docker compose -f $FB_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 diff --git a/services/fmd.sh b/services/fmd.sh index 454b9ac..78465e8 100644 --- a/services/fmd.sh +++ b/services/fmd.sh @@ -8,7 +8,18 @@ # # Ported from ubuntu-post-install-24.04-crowdsec.sh (# ---- FINDMYDEVICE ----). # Own ~/docker/fmd/ with a standalone docker-compose.yml + .env. -# Mobile app: "FindMyDevice" on F-Droid — not the Play Store version. +# Mobile app: "FMD" (by Nulide) on F-Droid — not the Google Find My Device app. +# +# Image: registry.gitlab.com/fmd-foss/fmd-server (GitLab's own registry, no +# Docker Hub image exists). The project has moved twice — originally +# nulide/findmydevice on Docker Hub, then gitlab.com/Nulide/findmydeviceserver, +# now gitlab.com/fmd-foss/fmd-server — and was rewritten from Node.js to +# Go+React along the way. Confirmed live: the old nulide/findmydevice image +# no longer exists at all ("pull access denied"), so this service never +# actually started on any box that installed it before this fix. The old +# single shared FMD_ADMIN_PASSWORD model is gone too — the current server +# uses per-user self-registration gated by an optional FMD_REGISTRATIONTOKEN, +# and stores its database at /var/lib/fmd-server/db, not /fmd/data. # ── Standalone bootstrap ────────────────────────────────────────────────────── # Detected when the script is executed directly rather than sourced by setup.sh. @@ -224,7 +235,7 @@ install_fmd() { echo "[DRY-RUN] FindMyDevice would:" echo " - Offer to add a new, separate instance if one already exists" echo " - Create \$DOCKER_DIR/fmd(-) with docker-compose.yml + .env (data/)" - echo " - Generate a random admin password" + echo " - Generate a random registration token" echo " - Auto-scan for a free host port if this is an additional instance" echo " - Offer a Caddy reverse proxy and to start the container" return 0 @@ -256,6 +267,28 @@ install_fmd() { FMD_DIR="$DOCKER_DIR/fmd-$_suffix" CONTAINER="fmd-$_suffix" log_info "New instance: $FMD_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 "$FMD_DIR/docker-compose.yml" ]]; then + local MODE="" + prompt_reinstall_mode MODE + case "$MODE" in + update) + log_info "Refreshing the FindMyDevice image only — token, port, and Caddy config are left as-is." + ( cd "$FMD_DIR" && docker compose pull && docker compose up -d ) \ + && log_success "FindMyDevice image refreshed" \ + || log_warning "Refresh failed — check: docker compose -f $FMD_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 @@ -269,8 +302,8 @@ install_fmd() { ensure_docker_dir_ownership "$FMD_DIR" cd "$FMD_DIR" || return 1 - local FMD_PASS - FMD_PASS=$(openssl rand -base64 16 | tr -dc 'a-zA-Z0-9' | head -c 16) + local FMD_TOKEN + FMD_TOKEN=$(openssl rand -base64 16 | tr -dc 'a-zA-Z0-9' | head -c 16) # Mirrors configure_caddy_for_service's own mode resolution (lib/common.sh): # explicit CADDY_MODE from the site config wins, then a local ~/docker/caddy, @@ -300,21 +333,21 @@ name: $CONTAINER services: fmd: - image: nulide/findmydevice + image: registry.gitlab.com/fmd-foss/fmd-server:0 container_name: $CONTAINER hostname: $CONTAINER restart: unless-stopped environment: - - FMD_ADMIN_PASSWORD=\${FMD_ADMIN_PASSWORD} + - FMD_REGISTRATIONTOKEN=\${FMD_REGISTRATIONTOKEN} volumes: - - ./data:/fmd/data + - ./data:/var/lib/fmd-server/db ports: - "${WEB_PORT}:8080" ${_CADDY_NET_BLOCK}${_CADDY_NET_SECTION} FMD_COMPOSE cat > .env << FMD_ENV -FMD_ADMIN_PASSWORD=$FMD_PASS +FMD_REGISTRATIONTOKEN=$FMD_TOKEN CADDY_NET=$SITE_CADDY_NET FMD_ENV @@ -334,8 +367,11 @@ This is a separate, fully isolated instance (own server, own password, own port) — not shared devices with another FindMyDevice instance.") - Web UI: http://localhost:${WEB_PORT} -- Admin password: stored in \`.env\` (\`FMD_ADMIN_PASSWORD\`) -- App data: \`data/\` +- Registration token: stored in \`.env\` (\`FMD_REGISTRATIONTOKEN\`) — the FMD + app must supply this when creating an account on this server, so a + stranger who finds the URL can't just self-register. Leave the \`.env\` + value blank and restart if you'd rather allow open registration. +- App data: \`data/\` (mounted at the container's own db path) ## Manage \`\`\`bash @@ -347,9 +383,11 @@ docker compose pull && docker compose up -d # update \`\`\` ## Mobile app -Install **FindMyDevice** from **F-Droid** (not the Play Store version): -1. Open the app → Settings → Server URL → \`http://YOUR-SERVER-IP:${WEB_PORT}\` -2. Enter your admin password from \`.env\` +Install **FMD** from **F-Droid** (search "Find My Device", by Nulide — not +the Google "Find My Device" app): +1. Open the app → create/register an account → Server URL → + \`http://YOUR-SERVER-IP:${WEB_PORT}\` +2. Enter the registration token from \`.env\` when prompted 3. Grant location and accessibility permissions MD @@ -360,9 +398,9 @@ MD fi echo "" - echo " Access at: http://localhost:${WEB_PORT}" - echo " Password: $FMD_PASS (saved in .env)" - echo " Mobile app: FindMyDevice on F-Droid" + echo " Access at: http://localhost:${WEB_PORT}" + echo " Registration token: $FMD_TOKEN (saved in .env)" + echo " Mobile app: FMD (by Nulide) on F-Droid" echo "" } From 11a4e249b614986d7922d1a1345b00565c6cfe88 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 10 Aug 2026 14:39:34 +0000 Subject: [PATCH 2/2] Add missing cancel option to 15 more multi-instance services; add setup.sh --status MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- services/actualbudget.sh | 22 ++++++++++++++++++++++ services/audiobookshelf.sh | 22 ++++++++++++++++++++++ services/emby.sh | 22 ++++++++++++++++++++++ services/homebox.sh | 22 ++++++++++++++++++++++ services/immich.sh | 22 ++++++++++++++++++++++ services/jellyfin.sh | 22 ++++++++++++++++++++++ services/joplin.sh | 22 ++++++++++++++++++++++ services/lyrion.sh | 22 ++++++++++++++++++++++ services/mealie.sh | 22 ++++++++++++++++++++++ services/meshcentral.sh | 22 ++++++++++++++++++++++ services/ntfy.sh | 22 ++++++++++++++++++++++ services/rustdesk.sh | 22 ++++++++++++++++++++++ services/traccar.sh | 22 ++++++++++++++++++++++ services/unifi.sh | 22 ++++++++++++++++++++++ services/vaultwarden.sh | 22 ++++++++++++++++++++++ setup.sh | 26 +++++++++++++++++++++++++- 16 files changed, 355 insertions(+), 1 deletion(-) diff --git a/services/actualbudget.sh b/services/actualbudget.sh index 4bb68ef..ae429b4 100644 --- a/services/actualbudget.sh +++ b/services/actualbudget.sh @@ -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 diff --git a/services/audiobookshelf.sh b/services/audiobookshelf.sh index bcc295c..863bd4f 100644 --- a/services/audiobookshelf.sh +++ b/services/audiobookshelf.sh @@ -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 diff --git a/services/emby.sh b/services/emby.sh index 3dd3c09..acb0206 100644 --- a/services/emby.sh +++ b/services/emby.sh @@ -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 diff --git a/services/homebox.sh b/services/homebox.sh index a1c116f..8c71238 100644 --- a/services/homebox.sh +++ b/services/homebox.sh @@ -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 diff --git a/services/immich.sh b/services/immich.sh index 7197439..a4eaff3 100644 --- a/services/immich.sh +++ b/services/immich.sh @@ -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 diff --git a/services/jellyfin.sh b/services/jellyfin.sh index 53187b4..7cea4f7 100644 --- a/services/jellyfin.sh +++ b/services/jellyfin.sh @@ -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 diff --git a/services/joplin.sh b/services/joplin.sh index 7fa279c..df63b11 100644 --- a/services/joplin.sh +++ b/services/joplin.sh @@ -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 diff --git a/services/lyrion.sh b/services/lyrion.sh index f44e576..5b41823 100644 --- a/services/lyrion.sh +++ b/services/lyrion.sh @@ -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 diff --git a/services/mealie.sh b/services/mealie.sh index e343054..e96acd8 100644 --- a/services/mealie.sh +++ b/services/mealie.sh @@ -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 diff --git a/services/meshcentral.sh b/services/meshcentral.sh index 21b079d..945f1f3 100644 --- a/services/meshcentral.sh +++ b/services/meshcentral.sh @@ -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 diff --git a/services/ntfy.sh b/services/ntfy.sh index 62abde7..8a95ba8 100644 --- a/services/ntfy.sh +++ b/services/ntfy.sh @@ -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 diff --git a/services/rustdesk.sh b/services/rustdesk.sh index f5f4d14..6915e46 100644 --- a/services/rustdesk.sh +++ b/services/rustdesk.sh @@ -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 diff --git a/services/traccar.sh b/services/traccar.sh index e41be28..853cf96 100644 --- a/services/traccar.sh +++ b/services/traccar.sh @@ -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 diff --git a/services/unifi.sh b/services/unifi.sh index 6dc7bc9..15a41ca 100644 --- a/services/unifi.sh +++ b/services/unifi.sh @@ -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 diff --git a/services/vaultwarden.sh b/services/vaultwarden.sh index c7f3e8a..7509eee 100644 --- a/services/vaultwarden.sh +++ b/services/vaultwarden.sh @@ -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 diff --git a/setup.sh b/setup.sh index 58d9fba..f162a1a 100755 --- a/setup.sh +++ b/setup.sh @@ -6,6 +6,7 @@ # category menu you loop through # sudo ./setup.sh ... 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