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