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/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 "" } 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