From c83439f576c7413030551bec2767166751acd493 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 21 Mar 2026 02:05:08 +0000 Subject: [PATCH 1/4] Replace whiptail menus with numbered text toggle selection All interactive prompts now use numbered toggles (type a number to toggle on/off, Enter to confirm) instead of requiring whiptail. Removed whiptail from apt dependencies in ubuntu-post-install.sh. https://claude.ai/code/session_012gDnantBmFTWZGCiKyjazx --- laptop_full_setup.sh | 133 ++++------ ubuntu-post-install.sh | 544 ++++++++++++++++++++--------------------- 2 files changed, 312 insertions(+), 365 deletions(-) diff --git a/laptop_full_setup.sh b/laptop_full_setup.sh index 55e037f..5508450 100755 --- a/laptop_full_setup.sh +++ b/laptop_full_setup.sh @@ -94,38 +94,24 @@ POSTINSTALL_SCRIPT="$SCRIPT_DIR/ubuntu-post-install.sh" HAS_POSTINSTALL=false [[ -f "$POSTINSTALL_SCRIPT" ]] && HAS_POSTINSTALL=true -if command -v whiptail &>/dev/null; then - WHIP_ARGS=() - $HAS_POSTINSTALL && WHIP_ARGS+=( - "postinstall" "Full system setup — Ubuntu apps, security, backups" OFF - ) - WHIP_ARGS+=( "aistack" "AI stack — select services on next screen" ON ) - SELECTED=$(whiptail --title "Local AI Setup" \ - --checklist "SPACE = toggle TAB = move ENTER = confirm" \ - 12 68 "${#WHIP_ARGS[@]}" "${WHIP_ARGS[@]}" 3>&1 1>&2 2>&3) || die "Cancelled." - INSTALL_AI=false - [[ "$SELECTED" == *"postinstall"* ]] && INSTALL_POSTINSTALL=true - [[ "$SELECTED" == *"aistack"* ]] && INSTALL_AI=true -else +echo "" +echo -e " ${BOLD}[1] Select what to run${NC}" +echo " (Type a number to toggle, Enter to confirm)" +SEL_POST=false; SEL_AI=true +while true; do echo "" - echo -e " ${BOLD}[1] Select what to run${NC}" - echo " (Type a number to toggle, Enter to confirm)" - SEL_POST=false; SEL_AI=true - while true; do - echo "" - $HAS_POSTINSTALL && printf " [%s] 1. Full system setup — Ubuntu apps, security, backups\n" "$($SEL_POST && echo '*' || echo ' ')" - printf " [%s] 2. AI stack — select services on next screen\n" "$($SEL_AI && echo '*' || echo ' ')" - echo "" - read -rp " Toggle [number] or Enter to confirm: " T - case "$T" in - 1) $HAS_POSTINSTALL && { $SEL_POST && SEL_POST=false || SEL_POST=true; } ;; - 2) $SEL_AI && SEL_AI=false || SEL_AI=true ;; - "") break ;; - esac - done - INSTALL_POSTINSTALL=$SEL_POST - INSTALL_AI=$SEL_AI -fi + $HAS_POSTINSTALL && printf " [%s] 1. Full system setup — Ubuntu apps, security, backups\n" "$($SEL_POST && echo '*' || echo ' ')" + printf " [%s] 2. AI stack — select services on next screen\n" "$($SEL_AI && echo '*' || echo ' ')" + echo "" + read -rp " Toggle [number] or Enter to confirm: " T + case "$T" in + 1) $HAS_POSTINSTALL && { $SEL_POST && SEL_POST=false || SEL_POST=true; } ;; + 2) $SEL_AI && SEL_AI=false || SEL_AI=true ;; + "") break ;; + esac +done +INSTALL_POSTINSTALL=$SEL_POST +INSTALL_AI=$SEL_AI $INSTALL_AI || $INSTALL_POSTINSTALL \ || die "Nothing selected — run again and select at least one option." @@ -137,62 +123,37 @@ if $INSTALL_AI; then docker ps -a --format '{{.Names}}' 2>/dev/null | grep -q "^$1$" && echo ON || echo OFF } - if command -v whiptail &>/dev/null; then - SELECTED_SVCS=$(whiptail --title "AI Stack — Select Services" \ - --checklist "Ollama always installed (core). SPACE = toggle ENTER = confirm" \ - 20 72 9 \ - "open-webui" "Open WebUI — Chat interface (like ChatGPT, uses Ollama)" "$(existing_svc open-webui)" \ - "rag" "RAG + ChromaDB— Code & document search (needed by MCP)" "$(existing_svc rag-server)" \ - "mcp" "MCP Server — Claude Code tools (bash, files, git, search)" "$(existing_svc mcp-server)" \ - "searxng" "SearXNG — Private local web search engine" "$(existing_svc searxng)" \ - "gitea" "Gitea — Self-hosted Git server" "$(existing_svc gitea)" \ - "invokeai" "InvokeAI — Image generation (Stable Diffusion)" "$(existing_svc invokeai)" \ - "portainer" "Portainer — Docker management web UI" "$(existing_svc portainer)" \ - "kiwix" "Kiwix — Offline Wikipedia, Stack Overflow, Arch Wiki" "$(existing_svc kiwix)" \ - 3>&1 1>&2 2>&3) || die "Cancelled." - - SVC_WEBUI=false; [[ "$SELECTED_SVCS" == *'"open-webui"'* ]] && SVC_WEBUI=true - SVC_RAG=false; [[ "$SELECTED_SVCS" == *'"rag"'* ]] && SVC_RAG=true - SVC_MCP=false; [[ "$SELECTED_SVCS" == *'"mcp"'* ]] && SVC_MCP=true - SVC_SEARXNG=false; [[ "$SELECTED_SVCS" == *'"searxng"'* ]] && SVC_SEARXNG=true - SVC_GITEA=false; [[ "$SELECTED_SVCS" == *'"gitea"'* ]] && SVC_GITEA=true - SVC_INVOKEAI=false; [[ "$SELECTED_SVCS" == *'"invokeai"'* ]] && SVC_INVOKEAI=true - SVC_PORTAINER=false;[[ "$SELECTED_SVCS" == *'"portainer"'* ]] && SVC_PORTAINER=true - SVC_KIWIX=false; [[ "$SELECTED_SVCS" == *'"kiwix"'* ]] && SVC_KIWIX=true - else - # Text toggle fallback + echo "" + echo -e " ${BOLD}[1a] AI Stack — select services to install${NC}" + echo " Ollama always included (required core). Type number to toggle, Enter to confirm." + _W=$SVC_WEBUI; _R=$SVC_RAG; _M=$SVC_MCP; _S=$SVC_SEARXNG + _G=$SVC_GITEA; _I=$SVC_INVOKEAI; _P=$SVC_PORTAINER; _K=$SVC_KIWIX + while true; do echo "" - echo -e " ${BOLD}[1a] AI Stack — select services to install${NC}" - echo " Ollama always included (required core). Type number to toggle, Enter to confirm." - _W=$SVC_WEBUI; _R=$SVC_RAG; _M=$SVC_MCP; _S=$SVC_SEARXNG - _G=$SVC_GITEA; _I=$SVC_INVOKEAI; _P=$SVC_PORTAINER; _K=$SVC_KIWIX - while true; do - echo "" - printf " [%s] 1. Open WebUI — Chat interface\n" "$($_W && echo '*' || echo ' ')" - printf " [%s] 2. RAG+ChromaDB — Code & document search\n" "$($_R && echo '*' || echo ' ')" - printf " [%s] 3. MCP Server — Claude Code tools (needs RAG)\n" "$($_M && echo '*' || echo ' ')" - printf " [%s] 4. SearXNG — Private web search\n" "$($_S && echo '*' || echo ' ')" - printf " [%s] 5. Gitea — Self-hosted Git\n" "$($_G && echo '*' || echo ' ')" - printf " [%s] 6. InvokeAI — Image generation\n" "$($_I && echo '*' || echo ' ')" - printf " [%s] 7. Portainer — Docker management UI\n" "$($_P && echo '*' || echo ' ')" - printf " [%s] 8. Kiwix — Offline Wikipedia, Stack Overflow\n" "$($_K && echo '*' || echo ' ')" - echo "" - read -rp " Toggle [number] or Enter to confirm: " T - case "$T" in - 1) $_W && _W=false || _W=true ;; - 2) $_R && _R=false || _R=true ;; - 3) $_M && _M=false || _M=true ;; - 4) $_S && _S=false || _S=true ;; - 5) $_G && _G=false || _G=true ;; - 6) $_I && _I=false || _I=true ;; - 7) $_P && _P=false || _P=true ;; - 8) $_K && _K=false || _K=true ;; - "") break ;; - esac - done - SVC_WEBUI=$_W; SVC_RAG=$_R; SVC_MCP=$_M; SVC_SEARXNG=$_S - SVC_GITEA=$_G; SVC_INVOKEAI=$_I; SVC_PORTAINER=$_P; SVC_KIWIX=$_K - fi + printf " [%s] 1. Open WebUI — Chat interface\n" "$($_W && echo '*' || echo ' ')" + printf " [%s] 2. RAG+ChromaDB — Code & document search\n" "$($_R && echo '*' || echo ' ')" + printf " [%s] 3. MCP Server — Claude Code tools (needs RAG)\n" "$($_M && echo '*' || echo ' ')" + printf " [%s] 4. SearXNG — Private web search\n" "$($_S && echo '*' || echo ' ')" + printf " [%s] 5. Gitea — Self-hosted Git\n" "$($_G && echo '*' || echo ' ')" + printf " [%s] 6. InvokeAI — Image generation\n" "$($_I && echo '*' || echo ' ')" + printf " [%s] 7. Portainer — Docker management UI\n" "$($_P && echo '*' || echo ' ')" + printf " [%s] 8. Kiwix — Offline Wikipedia, Stack Overflow\n" "$($_K && echo '*' || echo ' ')" + echo "" + read -rp " Toggle [number] or Enter to confirm: " T + case "$T" in + 1) $_W && _W=false || _W=true ;; + 2) $_R && _R=false || _R=true ;; + 3) $_M && _M=false || _M=true ;; + 4) $_S && _S=false || _S=true ;; + 5) $_G && _G=false || _G=true ;; + 6) $_I && _I=false || _I=true ;; + 7) $_P && _P=false || _P=true ;; + 8) $_K && _K=false || _K=true ;; + "") break ;; + esac + done + SVC_WEBUI=$_W; SVC_RAG=$_R; SVC_MCP=$_M; SVC_SEARXNG=$_S + SVC_GITEA=$_G; SVC_INVOKEAI=$_I; SVC_PORTAINER=$_P; SVC_KIWIX=$_K # Enforce dependencies: MCP needs RAG $SVC_MCP && ! $SVC_RAG && { warn "MCP Server requires RAG — enabling RAG+ChromaDB"; SVC_RAG=true; } diff --git a/ubuntu-post-install.sh b/ubuntu-post-install.sh index b260202..2bd1b91 100644 --- a/ubuntu-post-install.sh +++ b/ubuntu-post-install.sh @@ -137,7 +137,6 @@ run_disaster_recovery() { tree \ zip \ unzip \ - whiptail \ 2>/dev/null || echo " ⚠ Some packages may have failed" # Install Kopia for backup management @@ -325,55 +324,31 @@ run_disaster_recovery() { echo "Found ${#SERVICES_FOUND[@]} services in backup." echo "" - # Check if whiptail is available for nice UI - if command -v whiptail &> /dev/null; then - # Build whiptail checklist arguments - CHECKLIST_ARGS=() - for svc in "${SERVICES_FOUND[@]}"; do - CHECKLIST_ARGS+=("$svc" "" "ON") - done - - # Show checklist - returns selected items - SELECTED=$(whiptail --title "Select Services to Restore" \ - --checklist "Use SPACE to select/deselect, ENTER to confirm:" \ - 20 60 12 \ - "${CHECKLIST_ARGS[@]}" \ - 3>&1 1>&2 2>&3) - - # Parse selected services (whiptail returns quoted strings) - SELECTED_SERVICES=() - for item in $SELECTED; do - # Remove quotes - svc=$(echo "$item" | tr -d '"') - SELECTED_SERVICES+=("$svc") - done - else - # Fallback: text-based selection - echo "Select services to restore:" - echo " [A] All services" - echo " [N] None (skip restore)" - echo " [S] Select individually" + # Numbered toggle selection for services to restore + echo "Select services to restore (all selected by default):" + echo " Type a number to toggle, Enter to confirm." + echo "" + declare -a _SEL_FLAGS + for i in "${!SERVICES_FOUND[@]}"; do _SEL_FLAGS[$i]=true; done + while true; do echo "" - read -p "Choice (A/N/S) [A]: " RESTORE_CHOICE - - case "${RESTORE_CHOICE^^}" in - N) - SELECTED_SERVICES=() - ;; - S) - SELECTED_SERVICES=() - for svc in "${SERVICES_FOUND[@]}"; do - read -p " Restore $svc? (y/n) [y]: " RESTORE_SVC - if [ "$RESTORE_SVC" != "n" ] && [ "$RESTORE_SVC" != "N" ]; then - SELECTED_SERVICES+=("$svc") - fi - done - ;; - *) - SELECTED_SERVICES=("${SERVICES_FOUND[@]}") - ;; - esac - fi + for i in "${!SERVICES_FOUND[@]}"; do + printf " [%s] %d. %s\n" "$( ${_SEL_FLAGS[$i]} && echo '*' || echo ' ')" "$((i+1))" "${SERVICES_FOUND[$i]}" + done + echo "" + read -p " Toggle [number] or Enter to confirm: " _T + if [[ -z "$_T" ]]; then + break + elif [[ "$_T" =~ ^[0-9]+$ ]] && [[ "$_T" -ge 1 ]] && [[ "$_T" -le "${#SERVICES_FOUND[@]}" ]]; then + idx=$((_T-1)) + ${_SEL_FLAGS[$idx]} && _SEL_FLAGS[$idx]=false || _SEL_FLAGS[$idx]=true + fi + done + SELECTED_SERVICES=() + for i in "${!SERVICES_FOUND[@]}"; do + ${_SEL_FLAGS[$i]} && SELECTED_SERVICES+=("${SERVICES_FOUND[$i]}") + done + unset _SEL_FLAGS _T # Restore selected services echo "" @@ -635,54 +610,34 @@ run_migration() { SELECTED_CONTAINERS=() - if command -v whiptail &> /dev/null && [ ${#CONTAINERS_FOUND[@]} -gt 3 ]; then - # Use whiptail for many containers - CHECKLIST_ARGS=() - for container in "${CONTAINERS_FOUND[@]}"; do - CHECKLIST_ARGS+=("$container" "" "ON") - done - - SELECTED=$(whiptail --title "Select Containers to Migrate" \ - --checklist "Use SPACE to select/deselect, ENTER to confirm:" \ - 20 60 12 \ - "${CHECKLIST_ARGS[@]}" \ - 3>&1 1>&2 2>&3) - - if [ $? -eq 0 ] && [ -n "$SELECTED" ]; then - # Parse whiptail output - for container in $SELECTED; do - # Remove quotes - container="${container//\"/}" - SELECTED_CONTAINERS+=("$container") - done - fi - else - # Text-based selection - echo "Select containers to migrate:" - echo " [A] All containers" - echo " [S] Select individually" - echo " [N] None (cancel)" + # Numbered toggle selection for containers to migrate (all selected by default) + echo "Select containers to migrate (all selected by default):" + echo " Type a number to toggle, Enter to confirm, or 0 to cancel." + echo "" + declare -a _CONT_FLAGS + for i in "${!CONTAINERS_FOUND[@]}"; do _CONT_FLAGS[$i]=true; done + while true; do echo "" - read -p "Choice (A/S/N) [A]: " MIGRATE_CHOICE - - case "${MIGRATE_CHOICE^^}" in - N) - echo "Migration cancelled." - return 0 - ;; - S) - for container in "${CONTAINERS_FOUND[@]}"; do - read -p " Migrate $container? (y/n) [y]: " MIGRATE_THIS - if [ "$MIGRATE_THIS" != "n" ] && [ "$MIGRATE_THIS" != "N" ]; then - SELECTED_CONTAINERS+=("$container") - fi - done - ;; - *) - SELECTED_CONTAINERS=("${CONTAINERS_FOUND[@]}") - ;; - esac - fi + for i in "${!CONTAINERS_FOUND[@]}"; do + printf " [%s] %d. %s\n" "$( ${_CONT_FLAGS[$i]} && echo '*' || echo ' ')" "$((i+1))" "${CONTAINERS_FOUND[$i]}" + done + echo "" + read -p " Toggle [number], 0=cancel, Enter to confirm: " _T + if [[ -z "$_T" ]]; then + break + elif [[ "$_T" == "0" ]]; then + echo "Migration cancelled." + unset _CONT_FLAGS _T + return 0 + elif [[ "$_T" =~ ^[0-9]+$ ]] && [[ "$_T" -ge 1 ]] && [[ "$_T" -le "${#CONTAINERS_FOUND[@]}" ]]; then + idx=$((_T-1)) + ${_CONT_FLAGS[$idx]} && _CONT_FLAGS[$idx]=false || _CONT_FLAGS[$idx]=true + fi + done + for i in "${!CONTAINERS_FOUND[@]}"; do + ${_CONT_FLAGS[$i]} && SELECTED_CONTAINERS+=("${CONTAINERS_FOUND[$i]}") + done + unset _CONT_FLAGS _T if [ ${#SELECTED_CONTAINERS[@]} -eq 0 ]; then echo "No containers selected." @@ -3793,184 +3748,228 @@ else # SERVICE SELECTION MENU # ============================================================================ - # Use whiptail for service selection if available - if command -v whiptail &> /dev/null; then - # Detect existing services - echo "Detecting existing services..." - declare -A EXISTING_SERVICES - [ -d "$DOCKER_DIR/immich" ] && EXISTING_SERVICES[IMMICH]="ON" - [ -d "$DOCKER_DIR/audiobookshelf" ] && EXISTING_SERVICES[AUDIOBOOKSHELF]="ON" - [ -d "$DOCKER_DIR/emby" ] && EXISTING_SERVICES[EMBY]="ON" - [ -d "$DOCKER_DIR/arm" ] && EXISTING_SERVICES[ARM]="ON" - [ -d "$DOCKER_DIR/filebrowser" ] && EXISTING_SERVICES[FILEBROWSER]="ON" - [ -d "$DOCKER_DIR/magicmirror" ] && EXISTING_SERVICES[MAGICMIRROR]="ON" - [ -d "$DOCKER_DIR/actualbudget" ] && EXISTING_SERVICES[ACTUALBUDGET]="ON" - [ -d "$DOCKER_DIR/keycloak" ] && EXISTING_SERVICES[KEYCLOAK]="ON" - [ -d "$DOCKER_DIR/caddy" ] && EXISTING_SERVICES[CADDY]="ON" - [ -d "$DOCKER_DIR/lms" ] && EXISTING_SERVICES[LYRION]="ON" - [ -d "$DOCKER_DIR/mealie" ] && EXISTING_SERVICES[MEALIE]="ON" - [ -d "$DOCKER_DIR/minecraft" ] && EXISTING_SERVICES[MINECRAFT]="ON" - [ -d "$DOCKER_DIR/jellyfin" ] && EXISTING_SERVICES[JELLYFIN]="ON" - [ -d "$DOCKER_DIR/frigate" ] && EXISTING_SERVICES[FRIGATE]="ON" - [ -d "$DOCKER_DIR/ntfy" ] && EXISTING_SERVICES[NTFY]="ON" - [ -d "$DOCKER_DIR/uptime-kuma" ] && EXISTING_SERVICES[UPTIMEKUMA]="ON" - [ -d "$DOCKER_DIR/wg-easy" ] && EXISTING_SERVICES[WGEASY]="ON" - [ -d "$DOCKER_DIR/traccar" ] && EXISTING_SERVICES[TRACCAR]="ON" - [ -d "$DOCKER_DIR/portainer" ] && EXISTING_SERVICES[PORTAINER]="ON" - [ -d "$DOCKER_DIR/meshcentral" ] && EXISTING_SERVICES[MESHCENTRAL]="ON" - [ -d "$DOCKER_DIR/fmd" ] && EXISTING_SERVICES[FINDMYDEVICE]="ON" - [ -d "$DOCKER_DIR/frigate-notify" ] && EXISTING_SERVICES[FRIGATE_NOTIFY]="ON" - [ -d "$DOCKER_DIR/watchtower" ] && EXISTING_SERVICES[WATCHTOWER]="ON" - command -v fail2ban-client &> /dev/null && EXISTING_SERVICES[FAIL2BAN]="ON" + # Detect existing services + echo "Detecting existing services..." + declare -A EXISTING_SERVICES + [ -d "$DOCKER_DIR/immich" ] && EXISTING_SERVICES[IMMICH]="ON" + [ -d "$DOCKER_DIR/audiobookshelf" ] && EXISTING_SERVICES[AUDIOBOOKSHELF]="ON" + [ -d "$DOCKER_DIR/emby" ] && EXISTING_SERVICES[EMBY]="ON" + [ -d "$DOCKER_DIR/arm" ] && EXISTING_SERVICES[ARM]="ON" + [ -d "$DOCKER_DIR/filebrowser" ] && EXISTING_SERVICES[FILEBROWSER]="ON" + [ -d "$DOCKER_DIR/magicmirror" ] && EXISTING_SERVICES[MAGICMIRROR]="ON" + [ -d "$DOCKER_DIR/actualbudget" ] && EXISTING_SERVICES[ACTUALBUDGET]="ON" + [ -d "$DOCKER_DIR/keycloak" ] && EXISTING_SERVICES[KEYCLOAK]="ON" + [ -d "$DOCKER_DIR/caddy" ] && EXISTING_SERVICES[CADDY]="ON" + [ -d "$DOCKER_DIR/lms" ] && EXISTING_SERVICES[LYRION]="ON" + [ -d "$DOCKER_DIR/mealie" ] && EXISTING_SERVICES[MEALIE]="ON" + [ -d "$DOCKER_DIR/minecraft" ] && EXISTING_SERVICES[MINECRAFT]="ON" + [ -d "$DOCKER_DIR/jellyfin" ] && EXISTING_SERVICES[JELLYFIN]="ON" + [ -d "$DOCKER_DIR/frigate" ] && EXISTING_SERVICES[FRIGATE]="ON" + [ -d "$DOCKER_DIR/ntfy" ] && EXISTING_SERVICES[NTFY]="ON" + [ -d "$DOCKER_DIR/uptime-kuma" ] && EXISTING_SERVICES[UPTIMEKUMA]="ON" + [ -d "$DOCKER_DIR/wg-easy" ] && EXISTING_SERVICES[WGEASY]="ON" + [ -d "$DOCKER_DIR/traccar" ] && EXISTING_SERVICES[TRACCAR]="ON" + [ -d "$DOCKER_DIR/portainer" ] && EXISTING_SERVICES[PORTAINER]="ON" + [ -d "$DOCKER_DIR/meshcentral" ] && EXISTING_SERVICES[MESHCENTRAL]="ON" + [ -d "$DOCKER_DIR/fmd" ] && EXISTING_SERVICES[FINDMYDEVICE]="ON" + [ -d "$DOCKER_DIR/frigate-notify" ] && EXISTING_SERVICES[FRIGATE_NOTIFY]="ON" + [ -d "$DOCKER_DIR/watchtower" ] && EXISTING_SERVICES[WATCHTOWER]="ON" + command -v fail2ban-client &> /dev/null && EXISTING_SERVICES[FAIL2BAN]="ON" - # Ask user what action to perform - ACTION=$(whiptail --title "Service Management" --menu \ - "Choose an action:" 15 60 3 \ - "1" "Install new services" \ - "2" "Uninstall existing services" \ - "3" "Cancel and skip" \ - 3>&1 1>&2 2>&3) + # Action selection + echo "" + echo " Service Management" + echo " ──────────────────" + echo " 1. Install new services" + echo " 2. Uninstall existing services" + echo " 3. Cancel and skip" + echo "" + read -p " Choose action [1]: " ACTION + ACTION="${ACTION:-1}" - case "$ACTION" in - 1) - # INSTALL MODE - MENU_MODE="INSTALL" - # Build checklist - mark existing services as ON - SELECTED_SERVICES=$(whiptail --title "Select Docker Services to Install" \ - --checklist "Use SPACE to select, ENTER to confirm. [*] = already installed" 25 78 17 \ - "IMMICH" "Photo & video backup (like Google Photos)" ${EXISTING_SERVICES[IMMICH]:-OFF} \ - "AUDIOBOOKSHELF" "Audiobook & podcast server" ${EXISTING_SERVICES[AUDIOBOOKSHELF]:-OFF} \ - "EMBY" "Media server for movies, TV, music" ${EXISTING_SERVICES[EMBY]:-OFF} \ - "ARM" "Automatic Ripping Machine for DVDs/Blu-rays" ${EXISTING_SERVICES[ARM]:-OFF} \ - "FILEBROWSER" "Web-based file manager" ${EXISTING_SERVICES[FILEBROWSER]:-OFF} \ - "MAGICMIRROR" "Smart mirror / dashboard display" ${EXISTING_SERVICES[MAGICMIRROR]:-OFF} \ - "ACTUALBUDGET" "Personal finance management with bank sync" ${EXISTING_SERVICES[ACTUALBUDGET]:-OFF} \ - "KEYCLOAK" "Identity & Access Management (SSO)" ${EXISTING_SERVICES[KEYCLOAK]:-OFF} \ - "CADDY" "Reverse proxy with automatic HTTPS" ${EXISTING_SERVICES[CADDY]:-OFF} \ - "FAIL2BAN" "Intrusion prevention system" ${EXISTING_SERVICES[FAIL2BAN]:-OFF} \ - "LYRION" "Music streaming server (LMS)" ${EXISTING_SERVICES[LYRION]:-OFF} \ - "MEALIE" "Recipe manager & meal planner" ${EXISTING_SERVICES[MEALIE]:-OFF} \ - "MINECRAFT" "Minecraft game server" ${EXISTING_SERVICES[MINECRAFT]:-OFF} \ - "JELLYFIN" "Free media server (Emby alternative)" ${EXISTING_SERVICES[JELLYFIN]:-OFF} \ - "FRIGATE" "AI-powered NVR for security cameras" ${EXISTING_SERVICES[FRIGATE]:-OFF} \ - "NTFY" "Push notifications server" ${EXISTING_SERVICES[NTFY]:-OFF} \ - "UPTIMEKUMA" "Service monitoring dashboard" ${EXISTING_SERVICES[UPTIMEKUMA]:-OFF} \ - "WGEASY" "WireGuard VPN with web UI" ${EXISTING_SERVICES[WGEASY]:-OFF} \ - "TRACCAR" "GPS tracking server" ${EXISTING_SERVICES[TRACCAR]:-OFF} \ - "PORTAINER" "Docker management web UI" ${EXISTING_SERVICES[PORTAINER]:-OFF} \ - "MESHCENTRAL" "Remote management server" ${EXISTING_SERVICES[MESHCENTRAL]:-OFF} \ - "FINDMYDEVICE" "Device tracking (like Find My)" ${EXISTING_SERVICES[FINDMYDEVICE]:-OFF} \ - "FRIGATE_NOTIFY" "Push notifications for Frigate" ${EXISTING_SERVICES[FRIGATE_NOTIFY]:-OFF} \ - "WATCHTOWER" "Automatic container updates" ${EXISTING_SERVICES[WATCHTOWER]:-OFF} \ - 3>&1 1>&2 2>&3) - ;; - 2) - # UNINSTALL MODE - MENU_MODE="UNINSTALL" - # Only show services that exist - UNINSTALL_OPTIONS="" - [ -n "${EXISTING_SERVICES[IMMICH]}" ] && UNINSTALL_OPTIONS="$UNINSTALL_OPTIONS IMMICH \"Photo & video backup\" ON" - [ -n "${EXISTING_SERVICES[AUDIOBOOKSHELF]}" ] && UNINSTALL_OPTIONS="$UNINSTALL_OPTIONS AUDIOBOOKSHELF \"Audiobook server\" ON" - [ -n "${EXISTING_SERVICES[EMBY]}" ] && UNINSTALL_OPTIONS="$UNINSTALL_OPTIONS EMBY \"Media server\" ON" - [ -n "${EXISTING_SERVICES[ARM]}" ] && UNINSTALL_OPTIONS="$UNINSTALL_OPTIONS ARM \"Automatic Ripping Machine\" ON" - [ -n "${EXISTING_SERVICES[FILEBROWSER]}" ] && UNINSTALL_OPTIONS="$UNINSTALL_OPTIONS FILEBROWSER \"Web file manager\" ON" - [ -n "${EXISTING_SERVICES[MAGICMIRROR]}" ] && UNINSTALL_OPTIONS="$UNINSTALL_OPTIONS MAGICMIRROR \"Smart mirror\" ON" - [ -n "${EXISTING_SERVICES[ACTUALBUDGET]}" ] && UNINSTALL_OPTIONS="$UNINSTALL_OPTIONS ACTUALBUDGET \"Personal finance\" ON" - [ -n "${EXISTING_SERVICES[KEYCLOAK]}" ] && UNINSTALL_OPTIONS="$UNINSTALL_OPTIONS KEYCLOAK \"Identity management\" ON" - [ -n "${EXISTING_SERVICES[CADDY]}" ] && UNINSTALL_OPTIONS="$UNINSTALL_OPTIONS CADDY \"Reverse proxy\" ON" - [ -n "${EXISTING_SERVICES[FAIL2BAN]}" ] && UNINSTALL_OPTIONS="$UNINSTALL_OPTIONS FAIL2BAN \"Intrusion prevention\" ON" - [ -n "${EXISTING_SERVICES[LYRION]}" ] && UNINSTALL_OPTIONS="$UNINSTALL_OPTIONS LYRION \"Music server\" ON" - [ -n "${EXISTING_SERVICES[MEALIE]}" ] && UNINSTALL_OPTIONS="$UNINSTALL_OPTIONS MEALIE \"Recipe manager\" ON" - [ -n "${EXISTING_SERVICES[MINECRAFT]}" ] && UNINSTALL_OPTIONS="$UNINSTALL_OPTIONS MINECRAFT \"Game server\" ON" - [ -n "${EXISTING_SERVICES[JELLYFIN]}" ] && UNINSTALL_OPTIONS="$UNINSTALL_OPTIONS JELLYFIN \"Media server\" ON" - [ -n "${EXISTING_SERVICES[FRIGATE]}" ] && UNINSTALL_OPTIONS="$UNINSTALL_OPTIONS FRIGATE \"NVR cameras\" ON" - [ -n "${EXISTING_SERVICES[NTFY]}" ] && UNINSTALL_OPTIONS="$UNINSTALL_OPTIONS NTFY \"Push notifications\" ON" - [ -n "${EXISTING_SERVICES[UPTIMEKUMA]}" ] && UNINSTALL_OPTIONS="$UNINSTALL_OPTIONS UPTIMEKUMA \"Service monitoring\" ON" - [ -n "${EXISTING_SERVICES[WGEASY]}" ] && UNINSTALL_OPTIONS="$UNINSTALL_OPTIONS WGEASY \"WireGuard VPN\" ON" - [ -n "${EXISTING_SERVICES[TRACCAR]}" ] && UNINSTALL_OPTIONS="$UNINSTALL_OPTIONS TRACCAR \"GPS tracking\" ON" - [ -n "${EXISTING_SERVICES[PORTAINER]}" ] && UNINSTALL_OPTIONS="$UNINSTALL_OPTIONS PORTAINER \"Docker management\" ON" - [ -n "${EXISTING_SERVICES[MESHCENTRAL]}" ] && UNINSTALL_OPTIONS="$UNINSTALL_OPTIONS MESHCENTRAL \"Remote management\" ON" - [ -n "${EXISTING_SERVICES[FINDMYDEVICE]}" ] && UNINSTALL_OPTIONS="$UNINSTALL_OPTIONS FINDMYDEVICE \"Device tracking\" ON" - [ -n "${EXISTING_SERVICES[FRIGATE_NOTIFY]}" ] && UNINSTALL_OPTIONS="$UNINSTALL_OPTIONS FRIGATE_NOTIFY \"Frigate notifications\" ON" - [ -n "${EXISTING_SERVICES[WATCHTOWER]}" ] && UNINSTALL_OPTIONS="$UNINSTALL_OPTIONS WATCHTOWER \"Container updates\" ON" - - if [ -z "$UNINSTALL_OPTIONS" ]; then - whiptail --title "No Services Found" --msgbox "No services detected to uninstall." 8 50 - SELECTED_SERVICES="" - else - SELECTED_SERVICES=$(eval "whiptail --title 'Select Services to Uninstall' \ - --checklist 'WARNING: Data will be backed up but services will be removed' 25 78 17 \ - $UNINSTALL_OPTIONS \ - 3>&1 1>&2 2>&3") + SELECTED_SERVICES="" + case "$ACTION" in + 1) + MENU_MODE="INSTALL" + # Build ordered list of services; mark existing ones as selected + _SVC_KEYS=(IMMICH AUDIOBOOKSHELF EMBY ARM FILEBROWSER MAGICMIRROR ACTUALBUDGET + KEYCLOAK CADDY FAIL2BAN LYRION MEALIE MINECRAFT JELLYFIN FRIGATE + NTFY UPTIMEKUMA WGEASY TRACCAR PORTAINER MESHCENTRAL FINDMYDEVICE + FRIGATE_NOTIFY WATCHTOWER) + _SVC_LABELS=( + "IMMICH — Photo & video backup (like Google Photos)" + "AUDIOBOOKSHELF— Audiobook & podcast server" + "EMBY — Media server for movies, TV, music" + "ARM — Automatic Ripping Machine for DVDs/Blu-rays" + "FILEBROWSER — Web-based file manager" + "MAGICMIRROR — Smart mirror / dashboard display" + "ACTUALBUDGET — Personal finance management with bank sync" + "KEYCLOAK — Identity & Access Management (SSO)" + "CADDY — Reverse proxy with automatic HTTPS" + "FAIL2BAN — Intrusion prevention system" + "LYRION — Music streaming server (LMS)" + "MEALIE — Recipe manager & meal planner" + "MINECRAFT — Minecraft game server" + "JELLYFIN — Free media server (Emby alternative)" + "FRIGATE — AI-powered NVR for security cameras" + "NTFY — Push notifications server" + "UPTIMEKUMA — Service monitoring dashboard" + "WGEASY — WireGuard VPN with web UI" + "TRACCAR — GPS tracking server" + "PORTAINER — Docker management web UI" + "MESHCENTRAL — Remote management server" + "FINDMYDEVICE — Device tracking (like Find My)" + "FRIGATE_NOTIFY— Push notifications for Frigate" + "WATCHTOWER — Automatic container updates" + ) + declare -a _SVC_FLAGS + for i in "${!_SVC_KEYS[@]}"; do + [ -n "${EXISTING_SERVICES[${_SVC_KEYS[$i]}]:-}" ] && _SVC_FLAGS[$i]=true || _SVC_FLAGS[$i]=false + done + echo "" + echo " Select services to install ([*] = already installed)" + echo " Type number to toggle, Enter to confirm." + while true; do + echo "" + for i in "${!_SVC_KEYS[@]}"; do + _mark="$( ${_SVC_FLAGS[$i]} && echo '*' || echo ' ')" + _exist="$( [ -n "${EXISTING_SERVICES[${_SVC_KEYS[$i]}]:-}" ] && echo ' [installed]' || echo '')" + printf " [%s] %2d. %s%s\n" "$_mark" "$((i+1))" "${_SVC_LABELS[$i]}" "$_exist" + done + echo "" + read -p " Toggle [number] or Enter to confirm: " _T + if [[ -z "$_T" ]]; then + break + elif [[ "$_T" =~ ^[0-9]+$ ]] && [[ "$_T" -ge 1 ]] && [[ "$_T" -le "${#_SVC_KEYS[@]}" ]]; then + idx=$((_T-1)) + ${_SVC_FLAGS[$idx]} && _SVC_FLAGS[$idx]=false || _SVC_FLAGS[$idx]=true fi - ;; - 3|*) - # Cancel + done + for i in "${!_SVC_KEYS[@]}"; do + ${_SVC_FLAGS[$i]} && SELECTED_SERVICES="$SELECTED_SERVICES ${_SVC_KEYS[$i]}" + done + unset _SVC_FLAGS _SVC_KEYS _SVC_LABELS _T _mark _exist + ;; + 2) + MENU_MODE="UNINSTALL" + # Build list from only installed services + _SVC_KEYS=(); _SVC_LABELS=() + _add_if_exists() { [ -n "${EXISTING_SERVICES[$1]:-}" ] && _SVC_KEYS+=("$1") && _SVC_LABELS+=("$2"); } + _add_if_exists IMMICH "IMMICH — Photo & video backup" + _add_if_exists AUDIOBOOKSHELF "AUDIOBOOKSHELF — Audiobook server" + _add_if_exists EMBY "EMBY — Media server" + _add_if_exists ARM "ARM — Automatic Ripping Machine" + _add_if_exists FILEBROWSER "FILEBROWSER — Web file manager" + _add_if_exists MAGICMIRROR "MAGICMIRROR — Smart mirror" + _add_if_exists ACTUALBUDGET "ACTUALBUDGET — Personal finance" + _add_if_exists KEYCLOAK "KEYCLOAK — Identity management" + _add_if_exists CADDY "CADDY — Reverse proxy" + _add_if_exists FAIL2BAN "FAIL2BAN — Intrusion prevention" + _add_if_exists LYRION "LYRION — Music server" + _add_if_exists MEALIE "MEALIE — Recipe manager" + _add_if_exists MINECRAFT "MINECRAFT — Game server" + _add_if_exists JELLYFIN "JELLYFIN — Media server" + _add_if_exists FRIGATE "FRIGATE — NVR cameras" + _add_if_exists NTFY "NTFY — Push notifications" + _add_if_exists UPTIMEKUMA "UPTIMEKUMA — Service monitoring" + _add_if_exists WGEASY "WGEASY — WireGuard VPN" + _add_if_exists TRACCAR "TRACCAR — GPS tracking" + _add_if_exists PORTAINER "PORTAINER — Docker management" + _add_if_exists MESHCENTRAL "MESHCENTRAL — Remote management" + _add_if_exists FINDMYDEVICE "FINDMYDEVICE — Device tracking" + _add_if_exists FRIGATE_NOTIFY "FRIGATE_NOTIFY — Frigate notifications" + _add_if_exists WATCHTOWER "WATCHTOWER — Container updates" + unset -f _add_if_exists + + if [ ${#_SVC_KEYS[@]} -eq 0 ]; then + echo "No installed services detected to uninstall." SELECTED_SERVICES="" MENU_MODE="CANCEL" - ;; - esac - - # Check if user cancelled - if [ $? -ne 0 ]; then - echo "Service selection cancelled. Skipping Docker applications." + else + declare -a _SVC_FLAGS + for i in "${!_SVC_KEYS[@]}"; do _SVC_FLAGS[$i]=true; done + echo "" + echo " WARNING: selected services will be removed (data backed up first)." + echo " Type number to toggle, Enter to confirm." + while true; do + echo "" + for i in "${!_SVC_KEYS[@]}"; do + printf " [%s] %2d. %s\n" "$( ${_SVC_FLAGS[$i]} && echo '*' || echo ' ')" "$((i+1))" "${_SVC_LABELS[$i]}" + done + echo "" + read -p " Toggle [number] or Enter to confirm: " _T + if [[ -z "$_T" ]]; then + break + elif [[ "$_T" =~ ^[0-9]+$ ]] && [[ "$_T" -ge 1 ]] && [[ "$_T" -le "${#_SVC_KEYS[@]}" ]]; then + idx=$((_T-1)) + ${_SVC_FLAGS[$idx]} && _SVC_FLAGS[$idx]=false || _SVC_FLAGS[$idx]=true + fi + done + for i in "${!_SVC_KEYS[@]}"; do + ${_SVC_FLAGS[$i]} && SELECTED_SERVICES="$SELECTED_SERVICES ${_SVC_KEYS[$i]}" + done + unset _SVC_FLAGS _SVC_KEYS _SVC_LABELS _T + fi + ;; + 3|*) SELECTED_SERVICES="" - fi + MENU_MODE="CANCEL" + ;; + esac - # Parse selections (whiptail returns quoted strings) - # Only set to "n" if not already set (preserve any earlier choices) - : ${INSTALL_IMMICH:="n"} - : ${INSTALL_AUDIOBOOKSHELF:="n"} - : ${INSTALL_EMBY:="n"} - : ${INSTALL_ARM:="n"} - : ${INSTALL_FILEBROWSER:="n"} - : ${INSTALL_MAGICMIRROR:="n"} - : ${INSTALL_ACTUALBUDGET:="n"} - : ${INSTALL_KEYCLOAK:="n"} - : ${INSTALL_CADDY:="n"} - : ${INSTALL_FAIL2BAN:="n"} - : ${INSTALL_LMS:="n"} - : ${INSTALL_MEALIE:="n"} - : ${INSTALL_MINECRAFT:="n"} - : ${INSTALL_JELLYFIN:="n"} - : ${INSTALL_FRIGATE:="n"} - : ${INSTALL_NTFY:="n"} - : ${INSTALL_UPTIMEKUMA:="n"} - : ${INSTALL_WGEASY:="n"} - : ${INSTALL_TRACCAR:="n"} - : ${INSTALL_PORTAINER:="n"} - : ${INSTALL_MESHCENTRAL_SERVER:="n"} - : ${INSTALL_FMD:="n"} - : ${INSTALL_FRIGATE_NOTIFY:="n"} - : ${INSTALL_WATCHTOWER:="n"} + # Set installation flags from SELECTED_SERVICES + WHIPTAIL_USED=true + : ${INSTALL_IMMICH:="n"} + : ${INSTALL_AUDIOBOOKSHELF:="n"} + : ${INSTALL_EMBY:="n"} + : ${INSTALL_ARM:="n"} + : ${INSTALL_FILEBROWSER:="n"} + : ${INSTALL_MAGICMIRROR:="n"} + : ${INSTALL_ACTUALBUDGET:="n"} + : ${INSTALL_KEYCLOAK:="n"} + : ${INSTALL_CADDY:="n"} + : ${INSTALL_FAIL2BAN:="n"} + : ${INSTALL_LMS:="n"} + : ${INSTALL_MEALIE:="n"} + : ${INSTALL_MINECRAFT:="n"} + : ${INSTALL_JELLYFIN:="n"} + : ${INSTALL_FRIGATE:="n"} + : ${INSTALL_NTFY:="n"} + : ${INSTALL_UPTIMEKUMA:="n"} + : ${INSTALL_WGEASY:="n"} + : ${INSTALL_TRACCAR:="n"} + : ${INSTALL_PORTAINER:="n"} + : ${INSTALL_MESHCENTRAL_SERVER:="n"} + : ${INSTALL_FMD:="n"} + : ${INSTALL_FRIGATE_NOTIFY:="n"} + : ${INSTALL_WATCHTOWER:="n"} - # Set installation flags based on selections - if echo "$SELECTED_SERVICES" | grep -q "IMMICH"; then INSTALL_IMMICH="y"; fi - if echo "$SELECTED_SERVICES" | grep -q "AUDIOBOOKSHELF"; then INSTALL_AUDIOBOOKSHELF="y"; fi - if echo "$SELECTED_SERVICES" | grep -q "EMBY"; then INSTALL_EMBY="y"; fi - if echo "$SELECTED_SERVICES" | grep -q "ARM"; then INSTALL_ARM="y"; fi - if echo "$SELECTED_SERVICES" | grep -q "FILEBROWSER"; then INSTALL_FILEBROWSER="y"; fi - if echo "$SELECTED_SERVICES" | grep -q "MAGICMIRROR"; then INSTALL_MAGICMIRROR="y"; fi - if echo "$SELECTED_SERVICES" | grep -q "ACTUALBUDGET"; then INSTALL_ACTUALBUDGET="y"; fi - if echo "$SELECTED_SERVICES" | grep -q "KEYCLOAK"; then INSTALL_KEYCLOAK="y"; fi - if echo "$SELECTED_SERVICES" | grep -q "CADDY"; then INSTALL_CADDY="y"; fi - if echo "$SELECTED_SERVICES" | grep -q "FAIL2BAN"; then INSTALL_FAIL2BAN="y"; fi - if echo "$SELECTED_SERVICES" | grep -q "LYRION"; then INSTALL_LMS="y"; fi - if echo "$SELECTED_SERVICES" | grep -q "MEALIE"; then INSTALL_MEALIE="y"; fi - if echo "$SELECTED_SERVICES" | grep -q "MINECRAFT"; then INSTALL_MINECRAFT="y"; fi - if echo "$SELECTED_SERVICES" | grep -q "JELLYFIN"; then INSTALL_JELLYFIN="y"; fi - if echo "$SELECTED_SERVICES" | grep -q "FRIGATE\""; then INSTALL_FRIGATE="y"; fi - if echo "$SELECTED_SERVICES" | grep -q "NTFY"; then INSTALL_NTFY="y"; fi - if echo "$SELECTED_SERVICES" | grep -q "UPTIMEKUMA"; then INSTALL_UPTIMEKUMA="y"; fi - if echo "$SELECTED_SERVICES" | grep -q "WGEASY"; then INSTALL_WGEASY="y"; fi - if echo "$SELECTED_SERVICES" | grep -q "TRACCAR"; then INSTALL_TRACCAR="y"; fi - if echo "$SELECTED_SERVICES" | grep -q "PORTAINER"; then INSTALL_PORTAINER="y"; fi - if echo "$SELECTED_SERVICES" | grep -q "MESHCENTRAL"; then INSTALL_MESHCENTRAL_SERVER="y"; fi - if echo "$SELECTED_SERVICES" | grep -q "FINDMYDEVICE"; then INSTALL_FMD="y"; fi - if echo "$SELECTED_SERVICES" | grep -q "FRIGATE_NOTIFY"; then INSTALL_FRIGATE_NOTIFY="y"; fi - if echo "$SELECTED_SERVICES" | grep -q "WATCHTOWER"; then INSTALL_WATCHTOWER="y"; fi + if echo "$SELECTED_SERVICES" | grep -q "IMMICH"; then INSTALL_IMMICH="y"; fi + if echo "$SELECTED_SERVICES" | grep -q "AUDIOBOOKSHELF"; then INSTALL_AUDIOBOOKSHELF="y"; fi + if echo "$SELECTED_SERVICES" | grep -q "EMBY"; then INSTALL_EMBY="y"; fi + if echo "$SELECTED_SERVICES" | grep -q "ARM"; then INSTALL_ARM="y"; fi + if echo "$SELECTED_SERVICES" | grep -q "FILEBROWSER"; then INSTALL_FILEBROWSER="y"; fi + if echo "$SELECTED_SERVICES" | grep -q "MAGICMIRROR"; then INSTALL_MAGICMIRROR="y"; fi + if echo "$SELECTED_SERVICES" | grep -q "ACTUALBUDGET"; then INSTALL_ACTUALBUDGET="y"; fi + if echo "$SELECTED_SERVICES" | grep -q "KEYCLOAK"; then INSTALL_KEYCLOAK="y"; fi + if echo "$SELECTED_SERVICES" | grep -q "CADDY"; then INSTALL_CADDY="y"; fi + if echo "$SELECTED_SERVICES" | grep -q "FAIL2BAN"; then INSTALL_FAIL2BAN="y"; fi + if echo "$SELECTED_SERVICES" | grep -q "LYRION"; then INSTALL_LMS="y"; fi + if echo "$SELECTED_SERVICES" | grep -q "MEALIE"; then INSTALL_MEALIE="y"; fi + if echo "$SELECTED_SERVICES" | grep -q "MINECRAFT"; then INSTALL_MINECRAFT="y"; fi + if echo "$SELECTED_SERVICES" | grep -q "JELLYFIN"; then INSTALL_JELLYFIN="y"; fi + if echo "$SELECTED_SERVICES" | grep -q " FRIGATE "; then INSTALL_FRIGATE="y"; fi + if echo "$SELECTED_SERVICES" | grep -q "NTFY"; then INSTALL_NTFY="y"; fi + if echo "$SELECTED_SERVICES" | grep -q "UPTIMEKUMA"; then INSTALL_UPTIMEKUMA="y"; fi + if echo "$SELECTED_SERVICES" | grep -q "WGEASY"; then INSTALL_WGEASY="y"; fi + if echo "$SELECTED_SERVICES" | grep -q "TRACCAR"; then INSTALL_TRACCAR="y"; fi + if echo "$SELECTED_SERVICES" | grep -q "PORTAINER"; then INSTALL_PORTAINER="y"; fi + if echo "$SELECTED_SERVICES" | grep -q "MESHCENTRAL"; then INSTALL_MESHCENTRAL_SERVER="y"; fi + if echo "$SELECTED_SERVICES" | grep -q "FINDMYDEVICE"; then INSTALL_FMD="y"; fi + if echo "$SELECTED_SERVICES" | grep -q "FRIGATE_NOTIFY"; then INSTALL_FRIGATE_NOTIFY="y"; fi + if echo "$SELECTED_SERVICES" | grep -q "WATCHTOWER"; then INSTALL_WATCHTOWER="y"; fi - if [ "$MENU_MODE" = "UNINSTALL" ]; then + if [ "$MENU_MODE" = "UNINSTALL" ]; then echo "" echo "Services selected for uninstallation:" echo "$SELECTED_SERVICES" | tr '"' '\n' | grep -v '^$' | sed 's/^/ - /' @@ -4062,25 +4061,12 @@ else fi # After uninstall, exit the Docker apps section - WHIPTAIL_USED=true - elif [ "$MENU_MODE" = "INSTALL" ]; then echo "" echo "Selected services:" - echo "$SELECTED_SERVICES" | tr '"' '\n' | grep -v '^$' | sed 's/^/ - /' + echo "$SELECTED_SERVICES" | tr ' ' '\n' | grep -v '^$' | sed 's/^/ - /' echo "" - - # Mark that whiptail was used - skip all individual prompts - WHIPTAIL_USED=true - else - # Cancel or other mode - WHIPTAIL_USED=false fi - else - # Whiptail not available - will use individual prompts - WHIPTAIL_USED=false - MENU_MODE="INSTALL" - fi # Skip installation section if we just did uninstalls if [ "$MENU_MODE" = "UNINSTALL" ]; then From 342c746b51b7838e61b236c0e462b45c131a328d Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 21 Mar 2026 02:06:55 +0000 Subject: [PATCH 2/4] Revert "Replace whiptail menus with numbered text toggle selection" This reverts commit c83439f576c7413030551bec2767166751acd493. --- laptop_full_setup.sh | 133 ++++++---- ubuntu-post-install.sh | 540 +++++++++++++++++++++-------------------- 2 files changed, 363 insertions(+), 310 deletions(-) diff --git a/laptop_full_setup.sh b/laptop_full_setup.sh index 5508450..55e037f 100755 --- a/laptop_full_setup.sh +++ b/laptop_full_setup.sh @@ -94,24 +94,38 @@ POSTINSTALL_SCRIPT="$SCRIPT_DIR/ubuntu-post-install.sh" HAS_POSTINSTALL=false [[ -f "$POSTINSTALL_SCRIPT" ]] && HAS_POSTINSTALL=true -echo "" -echo -e " ${BOLD}[1] Select what to run${NC}" -echo " (Type a number to toggle, Enter to confirm)" -SEL_POST=false; SEL_AI=true -while true; do +if command -v whiptail &>/dev/null; then + WHIP_ARGS=() + $HAS_POSTINSTALL && WHIP_ARGS+=( + "postinstall" "Full system setup — Ubuntu apps, security, backups" OFF + ) + WHIP_ARGS+=( "aistack" "AI stack — select services on next screen" ON ) + SELECTED=$(whiptail --title "Local AI Setup" \ + --checklist "SPACE = toggle TAB = move ENTER = confirm" \ + 12 68 "${#WHIP_ARGS[@]}" "${WHIP_ARGS[@]}" 3>&1 1>&2 2>&3) || die "Cancelled." + INSTALL_AI=false + [[ "$SELECTED" == *"postinstall"* ]] && INSTALL_POSTINSTALL=true + [[ "$SELECTED" == *"aistack"* ]] && INSTALL_AI=true +else echo "" - $HAS_POSTINSTALL && printf " [%s] 1. Full system setup — Ubuntu apps, security, backups\n" "$($SEL_POST && echo '*' || echo ' ')" - printf " [%s] 2. AI stack — select services on next screen\n" "$($SEL_AI && echo '*' || echo ' ')" - echo "" - read -rp " Toggle [number] or Enter to confirm: " T - case "$T" in - 1) $HAS_POSTINSTALL && { $SEL_POST && SEL_POST=false || SEL_POST=true; } ;; - 2) $SEL_AI && SEL_AI=false || SEL_AI=true ;; - "") break ;; - esac -done -INSTALL_POSTINSTALL=$SEL_POST -INSTALL_AI=$SEL_AI + echo -e " ${BOLD}[1] Select what to run${NC}" + echo " (Type a number to toggle, Enter to confirm)" + SEL_POST=false; SEL_AI=true + while true; do + echo "" + $HAS_POSTINSTALL && printf " [%s] 1. Full system setup — Ubuntu apps, security, backups\n" "$($SEL_POST && echo '*' || echo ' ')" + printf " [%s] 2. AI stack — select services on next screen\n" "$($SEL_AI && echo '*' || echo ' ')" + echo "" + read -rp " Toggle [number] or Enter to confirm: " T + case "$T" in + 1) $HAS_POSTINSTALL && { $SEL_POST && SEL_POST=false || SEL_POST=true; } ;; + 2) $SEL_AI && SEL_AI=false || SEL_AI=true ;; + "") break ;; + esac + done + INSTALL_POSTINSTALL=$SEL_POST + INSTALL_AI=$SEL_AI +fi $INSTALL_AI || $INSTALL_POSTINSTALL \ || die "Nothing selected — run again and select at least one option." @@ -123,37 +137,62 @@ if $INSTALL_AI; then docker ps -a --format '{{.Names}}' 2>/dev/null | grep -q "^$1$" && echo ON || echo OFF } - echo "" - echo -e " ${BOLD}[1a] AI Stack — select services to install${NC}" - echo " Ollama always included (required core). Type number to toggle, Enter to confirm." - _W=$SVC_WEBUI; _R=$SVC_RAG; _M=$SVC_MCP; _S=$SVC_SEARXNG - _G=$SVC_GITEA; _I=$SVC_INVOKEAI; _P=$SVC_PORTAINER; _K=$SVC_KIWIX - while true; do + if command -v whiptail &>/dev/null; then + SELECTED_SVCS=$(whiptail --title "AI Stack — Select Services" \ + --checklist "Ollama always installed (core). SPACE = toggle ENTER = confirm" \ + 20 72 9 \ + "open-webui" "Open WebUI — Chat interface (like ChatGPT, uses Ollama)" "$(existing_svc open-webui)" \ + "rag" "RAG + ChromaDB— Code & document search (needed by MCP)" "$(existing_svc rag-server)" \ + "mcp" "MCP Server — Claude Code tools (bash, files, git, search)" "$(existing_svc mcp-server)" \ + "searxng" "SearXNG — Private local web search engine" "$(existing_svc searxng)" \ + "gitea" "Gitea — Self-hosted Git server" "$(existing_svc gitea)" \ + "invokeai" "InvokeAI — Image generation (Stable Diffusion)" "$(existing_svc invokeai)" \ + "portainer" "Portainer — Docker management web UI" "$(existing_svc portainer)" \ + "kiwix" "Kiwix — Offline Wikipedia, Stack Overflow, Arch Wiki" "$(existing_svc kiwix)" \ + 3>&1 1>&2 2>&3) || die "Cancelled." + + SVC_WEBUI=false; [[ "$SELECTED_SVCS" == *'"open-webui"'* ]] && SVC_WEBUI=true + SVC_RAG=false; [[ "$SELECTED_SVCS" == *'"rag"'* ]] && SVC_RAG=true + SVC_MCP=false; [[ "$SELECTED_SVCS" == *'"mcp"'* ]] && SVC_MCP=true + SVC_SEARXNG=false; [[ "$SELECTED_SVCS" == *'"searxng"'* ]] && SVC_SEARXNG=true + SVC_GITEA=false; [[ "$SELECTED_SVCS" == *'"gitea"'* ]] && SVC_GITEA=true + SVC_INVOKEAI=false; [[ "$SELECTED_SVCS" == *'"invokeai"'* ]] && SVC_INVOKEAI=true + SVC_PORTAINER=false;[[ "$SELECTED_SVCS" == *'"portainer"'* ]] && SVC_PORTAINER=true + SVC_KIWIX=false; [[ "$SELECTED_SVCS" == *'"kiwix"'* ]] && SVC_KIWIX=true + else + # Text toggle fallback echo "" - printf " [%s] 1. Open WebUI — Chat interface\n" "$($_W && echo '*' || echo ' ')" - printf " [%s] 2. RAG+ChromaDB — Code & document search\n" "$($_R && echo '*' || echo ' ')" - printf " [%s] 3. MCP Server — Claude Code tools (needs RAG)\n" "$($_M && echo '*' || echo ' ')" - printf " [%s] 4. SearXNG — Private web search\n" "$($_S && echo '*' || echo ' ')" - printf " [%s] 5. Gitea — Self-hosted Git\n" "$($_G && echo '*' || echo ' ')" - printf " [%s] 6. InvokeAI — Image generation\n" "$($_I && echo '*' || echo ' ')" - printf " [%s] 7. Portainer — Docker management UI\n" "$($_P && echo '*' || echo ' ')" - printf " [%s] 8. Kiwix — Offline Wikipedia, Stack Overflow\n" "$($_K && echo '*' || echo ' ')" - echo "" - read -rp " Toggle [number] or Enter to confirm: " T - case "$T" in - 1) $_W && _W=false || _W=true ;; - 2) $_R && _R=false || _R=true ;; - 3) $_M && _M=false || _M=true ;; - 4) $_S && _S=false || _S=true ;; - 5) $_G && _G=false || _G=true ;; - 6) $_I && _I=false || _I=true ;; - 7) $_P && _P=false || _P=true ;; - 8) $_K && _K=false || _K=true ;; - "") break ;; - esac - done - SVC_WEBUI=$_W; SVC_RAG=$_R; SVC_MCP=$_M; SVC_SEARXNG=$_S - SVC_GITEA=$_G; SVC_INVOKEAI=$_I; SVC_PORTAINER=$_P; SVC_KIWIX=$_K + echo -e " ${BOLD}[1a] AI Stack — select services to install${NC}" + echo " Ollama always included (required core). Type number to toggle, Enter to confirm." + _W=$SVC_WEBUI; _R=$SVC_RAG; _M=$SVC_MCP; _S=$SVC_SEARXNG + _G=$SVC_GITEA; _I=$SVC_INVOKEAI; _P=$SVC_PORTAINER; _K=$SVC_KIWIX + while true; do + echo "" + printf " [%s] 1. Open WebUI — Chat interface\n" "$($_W && echo '*' || echo ' ')" + printf " [%s] 2. RAG+ChromaDB — Code & document search\n" "$($_R && echo '*' || echo ' ')" + printf " [%s] 3. MCP Server — Claude Code tools (needs RAG)\n" "$($_M && echo '*' || echo ' ')" + printf " [%s] 4. SearXNG — Private web search\n" "$($_S && echo '*' || echo ' ')" + printf " [%s] 5. Gitea — Self-hosted Git\n" "$($_G && echo '*' || echo ' ')" + printf " [%s] 6. InvokeAI — Image generation\n" "$($_I && echo '*' || echo ' ')" + printf " [%s] 7. Portainer — Docker management UI\n" "$($_P && echo '*' || echo ' ')" + printf " [%s] 8. Kiwix — Offline Wikipedia, Stack Overflow\n" "$($_K && echo '*' || echo ' ')" + echo "" + read -rp " Toggle [number] or Enter to confirm: " T + case "$T" in + 1) $_W && _W=false || _W=true ;; + 2) $_R && _R=false || _R=true ;; + 3) $_M && _M=false || _M=true ;; + 4) $_S && _S=false || _S=true ;; + 5) $_G && _G=false || _G=true ;; + 6) $_I && _I=false || _I=true ;; + 7) $_P && _P=false || _P=true ;; + 8) $_K && _K=false || _K=true ;; + "") break ;; + esac + done + SVC_WEBUI=$_W; SVC_RAG=$_R; SVC_MCP=$_M; SVC_SEARXNG=$_S + SVC_GITEA=$_G; SVC_INVOKEAI=$_I; SVC_PORTAINER=$_P; SVC_KIWIX=$_K + fi # Enforce dependencies: MCP needs RAG $SVC_MCP && ! $SVC_RAG && { warn "MCP Server requires RAG — enabling RAG+ChromaDB"; SVC_RAG=true; } diff --git a/ubuntu-post-install.sh b/ubuntu-post-install.sh index 2bd1b91..b260202 100644 --- a/ubuntu-post-install.sh +++ b/ubuntu-post-install.sh @@ -137,6 +137,7 @@ run_disaster_recovery() { tree \ zip \ unzip \ + whiptail \ 2>/dev/null || echo " ⚠ Some packages may have failed" # Install Kopia for backup management @@ -324,31 +325,55 @@ run_disaster_recovery() { echo "Found ${#SERVICES_FOUND[@]} services in backup." echo "" - # Numbered toggle selection for services to restore - echo "Select services to restore (all selected by default):" - echo " Type a number to toggle, Enter to confirm." - echo "" - declare -a _SEL_FLAGS - for i in "${!SERVICES_FOUND[@]}"; do _SEL_FLAGS[$i]=true; done - while true; do - echo "" - for i in "${!SERVICES_FOUND[@]}"; do - printf " [%s] %d. %s\n" "$( ${_SEL_FLAGS[$i]} && echo '*' || echo ' ')" "$((i+1))" "${SERVICES_FOUND[$i]}" + # Check if whiptail is available for nice UI + if command -v whiptail &> /dev/null; then + # Build whiptail checklist arguments + CHECKLIST_ARGS=() + for svc in "${SERVICES_FOUND[@]}"; do + CHECKLIST_ARGS+=("$svc" "" "ON") done + + # Show checklist - returns selected items + SELECTED=$(whiptail --title "Select Services to Restore" \ + --checklist "Use SPACE to select/deselect, ENTER to confirm:" \ + 20 60 12 \ + "${CHECKLIST_ARGS[@]}" \ + 3>&1 1>&2 2>&3) + + # Parse selected services (whiptail returns quoted strings) + SELECTED_SERVICES=() + for item in $SELECTED; do + # Remove quotes + svc=$(echo "$item" | tr -d '"') + SELECTED_SERVICES+=("$svc") + done + else + # Fallback: text-based selection + echo "Select services to restore:" + echo " [A] All services" + echo " [N] None (skip restore)" + echo " [S] Select individually" echo "" - read -p " Toggle [number] or Enter to confirm: " _T - if [[ -z "$_T" ]]; then - break - elif [[ "$_T" =~ ^[0-9]+$ ]] && [[ "$_T" -ge 1 ]] && [[ "$_T" -le "${#SERVICES_FOUND[@]}" ]]; then - idx=$((_T-1)) - ${_SEL_FLAGS[$idx]} && _SEL_FLAGS[$idx]=false || _SEL_FLAGS[$idx]=true - fi - done - SELECTED_SERVICES=() - for i in "${!SERVICES_FOUND[@]}"; do - ${_SEL_FLAGS[$i]} && SELECTED_SERVICES+=("${SERVICES_FOUND[$i]}") - done - unset _SEL_FLAGS _T + read -p "Choice (A/N/S) [A]: " RESTORE_CHOICE + + case "${RESTORE_CHOICE^^}" in + N) + SELECTED_SERVICES=() + ;; + S) + SELECTED_SERVICES=() + for svc in "${SERVICES_FOUND[@]}"; do + read -p " Restore $svc? (y/n) [y]: " RESTORE_SVC + if [ "$RESTORE_SVC" != "n" ] && [ "$RESTORE_SVC" != "N" ]; then + SELECTED_SERVICES+=("$svc") + fi + done + ;; + *) + SELECTED_SERVICES=("${SERVICES_FOUND[@]}") + ;; + esac + fi # Restore selected services echo "" @@ -610,34 +635,54 @@ run_migration() { SELECTED_CONTAINERS=() - # Numbered toggle selection for containers to migrate (all selected by default) - echo "Select containers to migrate (all selected by default):" - echo " Type a number to toggle, Enter to confirm, or 0 to cancel." - echo "" - declare -a _CONT_FLAGS - for i in "${!CONTAINERS_FOUND[@]}"; do _CONT_FLAGS[$i]=true; done - while true; do - echo "" - for i in "${!CONTAINERS_FOUND[@]}"; do - printf " [%s] %d. %s\n" "$( ${_CONT_FLAGS[$i]} && echo '*' || echo ' ')" "$((i+1))" "${CONTAINERS_FOUND[$i]}" + if command -v whiptail &> /dev/null && [ ${#CONTAINERS_FOUND[@]} -gt 3 ]; then + # Use whiptail for many containers + CHECKLIST_ARGS=() + for container in "${CONTAINERS_FOUND[@]}"; do + CHECKLIST_ARGS+=("$container" "" "ON") done - echo "" - read -p " Toggle [number], 0=cancel, Enter to confirm: " _T - if [[ -z "$_T" ]]; then - break - elif [[ "$_T" == "0" ]]; then - echo "Migration cancelled." - unset _CONT_FLAGS _T - return 0 - elif [[ "$_T" =~ ^[0-9]+$ ]] && [[ "$_T" -ge 1 ]] && [[ "$_T" -le "${#CONTAINERS_FOUND[@]}" ]]; then - idx=$((_T-1)) - ${_CONT_FLAGS[$idx]} && _CONT_FLAGS[$idx]=false || _CONT_FLAGS[$idx]=true + + SELECTED=$(whiptail --title "Select Containers to Migrate" \ + --checklist "Use SPACE to select/deselect, ENTER to confirm:" \ + 20 60 12 \ + "${CHECKLIST_ARGS[@]}" \ + 3>&1 1>&2 2>&3) + + if [ $? -eq 0 ] && [ -n "$SELECTED" ]; then + # Parse whiptail output + for container in $SELECTED; do + # Remove quotes + container="${container//\"/}" + SELECTED_CONTAINERS+=("$container") + done fi - done - for i in "${!CONTAINERS_FOUND[@]}"; do - ${_CONT_FLAGS[$i]} && SELECTED_CONTAINERS+=("${CONTAINERS_FOUND[$i]}") - done - unset _CONT_FLAGS _T + else + # Text-based selection + echo "Select containers to migrate:" + echo " [A] All containers" + echo " [S] Select individually" + echo " [N] None (cancel)" + echo "" + read -p "Choice (A/S/N) [A]: " MIGRATE_CHOICE + + case "${MIGRATE_CHOICE^^}" in + N) + echo "Migration cancelled." + return 0 + ;; + S) + for container in "${CONTAINERS_FOUND[@]}"; do + read -p " Migrate $container? (y/n) [y]: " MIGRATE_THIS + if [ "$MIGRATE_THIS" != "n" ] && [ "$MIGRATE_THIS" != "N" ]; then + SELECTED_CONTAINERS+=("$container") + fi + done + ;; + *) + SELECTED_CONTAINERS=("${CONTAINERS_FOUND[@]}") + ;; + esac + fi if [ ${#SELECTED_CONTAINERS[@]} -eq 0 ]; then echo "No containers selected." @@ -3748,228 +3793,184 @@ else # SERVICE SELECTION MENU # ============================================================================ - # Detect existing services - echo "Detecting existing services..." - declare -A EXISTING_SERVICES - [ -d "$DOCKER_DIR/immich" ] && EXISTING_SERVICES[IMMICH]="ON" - [ -d "$DOCKER_DIR/audiobookshelf" ] && EXISTING_SERVICES[AUDIOBOOKSHELF]="ON" - [ -d "$DOCKER_DIR/emby" ] && EXISTING_SERVICES[EMBY]="ON" - [ -d "$DOCKER_DIR/arm" ] && EXISTING_SERVICES[ARM]="ON" - [ -d "$DOCKER_DIR/filebrowser" ] && EXISTING_SERVICES[FILEBROWSER]="ON" - [ -d "$DOCKER_DIR/magicmirror" ] && EXISTING_SERVICES[MAGICMIRROR]="ON" - [ -d "$DOCKER_DIR/actualbudget" ] && EXISTING_SERVICES[ACTUALBUDGET]="ON" - [ -d "$DOCKER_DIR/keycloak" ] && EXISTING_SERVICES[KEYCLOAK]="ON" - [ -d "$DOCKER_DIR/caddy" ] && EXISTING_SERVICES[CADDY]="ON" - [ -d "$DOCKER_DIR/lms" ] && EXISTING_SERVICES[LYRION]="ON" - [ -d "$DOCKER_DIR/mealie" ] && EXISTING_SERVICES[MEALIE]="ON" - [ -d "$DOCKER_DIR/minecraft" ] && EXISTING_SERVICES[MINECRAFT]="ON" - [ -d "$DOCKER_DIR/jellyfin" ] && EXISTING_SERVICES[JELLYFIN]="ON" - [ -d "$DOCKER_DIR/frigate" ] && EXISTING_SERVICES[FRIGATE]="ON" - [ -d "$DOCKER_DIR/ntfy" ] && EXISTING_SERVICES[NTFY]="ON" - [ -d "$DOCKER_DIR/uptime-kuma" ] && EXISTING_SERVICES[UPTIMEKUMA]="ON" - [ -d "$DOCKER_DIR/wg-easy" ] && EXISTING_SERVICES[WGEASY]="ON" - [ -d "$DOCKER_DIR/traccar" ] && EXISTING_SERVICES[TRACCAR]="ON" - [ -d "$DOCKER_DIR/portainer" ] && EXISTING_SERVICES[PORTAINER]="ON" - [ -d "$DOCKER_DIR/meshcentral" ] && EXISTING_SERVICES[MESHCENTRAL]="ON" - [ -d "$DOCKER_DIR/fmd" ] && EXISTING_SERVICES[FINDMYDEVICE]="ON" - [ -d "$DOCKER_DIR/frigate-notify" ] && EXISTING_SERVICES[FRIGATE_NOTIFY]="ON" - [ -d "$DOCKER_DIR/watchtower" ] && EXISTING_SERVICES[WATCHTOWER]="ON" - command -v fail2ban-client &> /dev/null && EXISTING_SERVICES[FAIL2BAN]="ON" + # Use whiptail for service selection if available + if command -v whiptail &> /dev/null; then + # Detect existing services + echo "Detecting existing services..." + declare -A EXISTING_SERVICES + [ -d "$DOCKER_DIR/immich" ] && EXISTING_SERVICES[IMMICH]="ON" + [ -d "$DOCKER_DIR/audiobookshelf" ] && EXISTING_SERVICES[AUDIOBOOKSHELF]="ON" + [ -d "$DOCKER_DIR/emby" ] && EXISTING_SERVICES[EMBY]="ON" + [ -d "$DOCKER_DIR/arm" ] && EXISTING_SERVICES[ARM]="ON" + [ -d "$DOCKER_DIR/filebrowser" ] && EXISTING_SERVICES[FILEBROWSER]="ON" + [ -d "$DOCKER_DIR/magicmirror" ] && EXISTING_SERVICES[MAGICMIRROR]="ON" + [ -d "$DOCKER_DIR/actualbudget" ] && EXISTING_SERVICES[ACTUALBUDGET]="ON" + [ -d "$DOCKER_DIR/keycloak" ] && EXISTING_SERVICES[KEYCLOAK]="ON" + [ -d "$DOCKER_DIR/caddy" ] && EXISTING_SERVICES[CADDY]="ON" + [ -d "$DOCKER_DIR/lms" ] && EXISTING_SERVICES[LYRION]="ON" + [ -d "$DOCKER_DIR/mealie" ] && EXISTING_SERVICES[MEALIE]="ON" + [ -d "$DOCKER_DIR/minecraft" ] && EXISTING_SERVICES[MINECRAFT]="ON" + [ -d "$DOCKER_DIR/jellyfin" ] && EXISTING_SERVICES[JELLYFIN]="ON" + [ -d "$DOCKER_DIR/frigate" ] && EXISTING_SERVICES[FRIGATE]="ON" + [ -d "$DOCKER_DIR/ntfy" ] && EXISTING_SERVICES[NTFY]="ON" + [ -d "$DOCKER_DIR/uptime-kuma" ] && EXISTING_SERVICES[UPTIMEKUMA]="ON" + [ -d "$DOCKER_DIR/wg-easy" ] && EXISTING_SERVICES[WGEASY]="ON" + [ -d "$DOCKER_DIR/traccar" ] && EXISTING_SERVICES[TRACCAR]="ON" + [ -d "$DOCKER_DIR/portainer" ] && EXISTING_SERVICES[PORTAINER]="ON" + [ -d "$DOCKER_DIR/meshcentral" ] && EXISTING_SERVICES[MESHCENTRAL]="ON" + [ -d "$DOCKER_DIR/fmd" ] && EXISTING_SERVICES[FINDMYDEVICE]="ON" + [ -d "$DOCKER_DIR/frigate-notify" ] && EXISTING_SERVICES[FRIGATE_NOTIFY]="ON" + [ -d "$DOCKER_DIR/watchtower" ] && EXISTING_SERVICES[WATCHTOWER]="ON" + command -v fail2ban-client &> /dev/null && EXISTING_SERVICES[FAIL2BAN]="ON" - # Action selection - echo "" - echo " Service Management" - echo " ──────────────────" - echo " 1. Install new services" - echo " 2. Uninstall existing services" - echo " 3. Cancel and skip" - echo "" - read -p " Choose action [1]: " ACTION - ACTION="${ACTION:-1}" + # Ask user what action to perform + ACTION=$(whiptail --title "Service Management" --menu \ + "Choose an action:" 15 60 3 \ + "1" "Install new services" \ + "2" "Uninstall existing services" \ + "3" "Cancel and skip" \ + 3>&1 1>&2 2>&3) - SELECTED_SERVICES="" - case "$ACTION" in - 1) - MENU_MODE="INSTALL" - # Build ordered list of services; mark existing ones as selected - _SVC_KEYS=(IMMICH AUDIOBOOKSHELF EMBY ARM FILEBROWSER MAGICMIRROR ACTUALBUDGET - KEYCLOAK CADDY FAIL2BAN LYRION MEALIE MINECRAFT JELLYFIN FRIGATE - NTFY UPTIMEKUMA WGEASY TRACCAR PORTAINER MESHCENTRAL FINDMYDEVICE - FRIGATE_NOTIFY WATCHTOWER) - _SVC_LABELS=( - "IMMICH — Photo & video backup (like Google Photos)" - "AUDIOBOOKSHELF— Audiobook & podcast server" - "EMBY — Media server for movies, TV, music" - "ARM — Automatic Ripping Machine for DVDs/Blu-rays" - "FILEBROWSER — Web-based file manager" - "MAGICMIRROR — Smart mirror / dashboard display" - "ACTUALBUDGET — Personal finance management with bank sync" - "KEYCLOAK — Identity & Access Management (SSO)" - "CADDY — Reverse proxy with automatic HTTPS" - "FAIL2BAN — Intrusion prevention system" - "LYRION — Music streaming server (LMS)" - "MEALIE — Recipe manager & meal planner" - "MINECRAFT — Minecraft game server" - "JELLYFIN — Free media server (Emby alternative)" - "FRIGATE — AI-powered NVR for security cameras" - "NTFY — Push notifications server" - "UPTIMEKUMA — Service monitoring dashboard" - "WGEASY — WireGuard VPN with web UI" - "TRACCAR — GPS tracking server" - "PORTAINER — Docker management web UI" - "MESHCENTRAL — Remote management server" - "FINDMYDEVICE — Device tracking (like Find My)" - "FRIGATE_NOTIFY— Push notifications for Frigate" - "WATCHTOWER — Automatic container updates" - ) - declare -a _SVC_FLAGS - for i in "${!_SVC_KEYS[@]}"; do - [ -n "${EXISTING_SERVICES[${_SVC_KEYS[$i]}]:-}" ] && _SVC_FLAGS[$i]=true || _SVC_FLAGS[$i]=false - done - echo "" - echo " Select services to install ([*] = already installed)" - echo " Type number to toggle, Enter to confirm." - while true; do - echo "" - for i in "${!_SVC_KEYS[@]}"; do - _mark="$( ${_SVC_FLAGS[$i]} && echo '*' || echo ' ')" - _exist="$( [ -n "${EXISTING_SERVICES[${_SVC_KEYS[$i]}]:-}" ] && echo ' [installed]' || echo '')" - printf " [%s] %2d. %s%s\n" "$_mark" "$((i+1))" "${_SVC_LABELS[$i]}" "$_exist" - done - echo "" - read -p " Toggle [number] or Enter to confirm: " _T - if [[ -z "$_T" ]]; then - break - elif [[ "$_T" =~ ^[0-9]+$ ]] && [[ "$_T" -ge 1 ]] && [[ "$_T" -le "${#_SVC_KEYS[@]}" ]]; then - idx=$((_T-1)) - ${_SVC_FLAGS[$idx]} && _SVC_FLAGS[$idx]=false || _SVC_FLAGS[$idx]=true + case "$ACTION" in + 1) + # INSTALL MODE + MENU_MODE="INSTALL" + # Build checklist - mark existing services as ON + SELECTED_SERVICES=$(whiptail --title "Select Docker Services to Install" \ + --checklist "Use SPACE to select, ENTER to confirm. [*] = already installed" 25 78 17 \ + "IMMICH" "Photo & video backup (like Google Photos)" ${EXISTING_SERVICES[IMMICH]:-OFF} \ + "AUDIOBOOKSHELF" "Audiobook & podcast server" ${EXISTING_SERVICES[AUDIOBOOKSHELF]:-OFF} \ + "EMBY" "Media server for movies, TV, music" ${EXISTING_SERVICES[EMBY]:-OFF} \ + "ARM" "Automatic Ripping Machine for DVDs/Blu-rays" ${EXISTING_SERVICES[ARM]:-OFF} \ + "FILEBROWSER" "Web-based file manager" ${EXISTING_SERVICES[FILEBROWSER]:-OFF} \ + "MAGICMIRROR" "Smart mirror / dashboard display" ${EXISTING_SERVICES[MAGICMIRROR]:-OFF} \ + "ACTUALBUDGET" "Personal finance management with bank sync" ${EXISTING_SERVICES[ACTUALBUDGET]:-OFF} \ + "KEYCLOAK" "Identity & Access Management (SSO)" ${EXISTING_SERVICES[KEYCLOAK]:-OFF} \ + "CADDY" "Reverse proxy with automatic HTTPS" ${EXISTING_SERVICES[CADDY]:-OFF} \ + "FAIL2BAN" "Intrusion prevention system" ${EXISTING_SERVICES[FAIL2BAN]:-OFF} \ + "LYRION" "Music streaming server (LMS)" ${EXISTING_SERVICES[LYRION]:-OFF} \ + "MEALIE" "Recipe manager & meal planner" ${EXISTING_SERVICES[MEALIE]:-OFF} \ + "MINECRAFT" "Minecraft game server" ${EXISTING_SERVICES[MINECRAFT]:-OFF} \ + "JELLYFIN" "Free media server (Emby alternative)" ${EXISTING_SERVICES[JELLYFIN]:-OFF} \ + "FRIGATE" "AI-powered NVR for security cameras" ${EXISTING_SERVICES[FRIGATE]:-OFF} \ + "NTFY" "Push notifications server" ${EXISTING_SERVICES[NTFY]:-OFF} \ + "UPTIMEKUMA" "Service monitoring dashboard" ${EXISTING_SERVICES[UPTIMEKUMA]:-OFF} \ + "WGEASY" "WireGuard VPN with web UI" ${EXISTING_SERVICES[WGEASY]:-OFF} \ + "TRACCAR" "GPS tracking server" ${EXISTING_SERVICES[TRACCAR]:-OFF} \ + "PORTAINER" "Docker management web UI" ${EXISTING_SERVICES[PORTAINER]:-OFF} \ + "MESHCENTRAL" "Remote management server" ${EXISTING_SERVICES[MESHCENTRAL]:-OFF} \ + "FINDMYDEVICE" "Device tracking (like Find My)" ${EXISTING_SERVICES[FINDMYDEVICE]:-OFF} \ + "FRIGATE_NOTIFY" "Push notifications for Frigate" ${EXISTING_SERVICES[FRIGATE_NOTIFY]:-OFF} \ + "WATCHTOWER" "Automatic container updates" ${EXISTING_SERVICES[WATCHTOWER]:-OFF} \ + 3>&1 1>&2 2>&3) + ;; + 2) + # UNINSTALL MODE + MENU_MODE="UNINSTALL" + # Only show services that exist + UNINSTALL_OPTIONS="" + [ -n "${EXISTING_SERVICES[IMMICH]}" ] && UNINSTALL_OPTIONS="$UNINSTALL_OPTIONS IMMICH \"Photo & video backup\" ON" + [ -n "${EXISTING_SERVICES[AUDIOBOOKSHELF]}" ] && UNINSTALL_OPTIONS="$UNINSTALL_OPTIONS AUDIOBOOKSHELF \"Audiobook server\" ON" + [ -n "${EXISTING_SERVICES[EMBY]}" ] && UNINSTALL_OPTIONS="$UNINSTALL_OPTIONS EMBY \"Media server\" ON" + [ -n "${EXISTING_SERVICES[ARM]}" ] && UNINSTALL_OPTIONS="$UNINSTALL_OPTIONS ARM \"Automatic Ripping Machine\" ON" + [ -n "${EXISTING_SERVICES[FILEBROWSER]}" ] && UNINSTALL_OPTIONS="$UNINSTALL_OPTIONS FILEBROWSER \"Web file manager\" ON" + [ -n "${EXISTING_SERVICES[MAGICMIRROR]}" ] && UNINSTALL_OPTIONS="$UNINSTALL_OPTIONS MAGICMIRROR \"Smart mirror\" ON" + [ -n "${EXISTING_SERVICES[ACTUALBUDGET]}" ] && UNINSTALL_OPTIONS="$UNINSTALL_OPTIONS ACTUALBUDGET \"Personal finance\" ON" + [ -n "${EXISTING_SERVICES[KEYCLOAK]}" ] && UNINSTALL_OPTIONS="$UNINSTALL_OPTIONS KEYCLOAK \"Identity management\" ON" + [ -n "${EXISTING_SERVICES[CADDY]}" ] && UNINSTALL_OPTIONS="$UNINSTALL_OPTIONS CADDY \"Reverse proxy\" ON" + [ -n "${EXISTING_SERVICES[FAIL2BAN]}" ] && UNINSTALL_OPTIONS="$UNINSTALL_OPTIONS FAIL2BAN \"Intrusion prevention\" ON" + [ -n "${EXISTING_SERVICES[LYRION]}" ] && UNINSTALL_OPTIONS="$UNINSTALL_OPTIONS LYRION \"Music server\" ON" + [ -n "${EXISTING_SERVICES[MEALIE]}" ] && UNINSTALL_OPTIONS="$UNINSTALL_OPTIONS MEALIE \"Recipe manager\" ON" + [ -n "${EXISTING_SERVICES[MINECRAFT]}" ] && UNINSTALL_OPTIONS="$UNINSTALL_OPTIONS MINECRAFT \"Game server\" ON" + [ -n "${EXISTING_SERVICES[JELLYFIN]}" ] && UNINSTALL_OPTIONS="$UNINSTALL_OPTIONS JELLYFIN \"Media server\" ON" + [ -n "${EXISTING_SERVICES[FRIGATE]}" ] && UNINSTALL_OPTIONS="$UNINSTALL_OPTIONS FRIGATE \"NVR cameras\" ON" + [ -n "${EXISTING_SERVICES[NTFY]}" ] && UNINSTALL_OPTIONS="$UNINSTALL_OPTIONS NTFY \"Push notifications\" ON" + [ -n "${EXISTING_SERVICES[UPTIMEKUMA]}" ] && UNINSTALL_OPTIONS="$UNINSTALL_OPTIONS UPTIMEKUMA \"Service monitoring\" ON" + [ -n "${EXISTING_SERVICES[WGEASY]}" ] && UNINSTALL_OPTIONS="$UNINSTALL_OPTIONS WGEASY \"WireGuard VPN\" ON" + [ -n "${EXISTING_SERVICES[TRACCAR]}" ] && UNINSTALL_OPTIONS="$UNINSTALL_OPTIONS TRACCAR \"GPS tracking\" ON" + [ -n "${EXISTING_SERVICES[PORTAINER]}" ] && UNINSTALL_OPTIONS="$UNINSTALL_OPTIONS PORTAINER \"Docker management\" ON" + [ -n "${EXISTING_SERVICES[MESHCENTRAL]}" ] && UNINSTALL_OPTIONS="$UNINSTALL_OPTIONS MESHCENTRAL \"Remote management\" ON" + [ -n "${EXISTING_SERVICES[FINDMYDEVICE]}" ] && UNINSTALL_OPTIONS="$UNINSTALL_OPTIONS FINDMYDEVICE \"Device tracking\" ON" + [ -n "${EXISTING_SERVICES[FRIGATE_NOTIFY]}" ] && UNINSTALL_OPTIONS="$UNINSTALL_OPTIONS FRIGATE_NOTIFY \"Frigate notifications\" ON" + [ -n "${EXISTING_SERVICES[WATCHTOWER]}" ] && UNINSTALL_OPTIONS="$UNINSTALL_OPTIONS WATCHTOWER \"Container updates\" ON" + + if [ -z "$UNINSTALL_OPTIONS" ]; then + whiptail --title "No Services Found" --msgbox "No services detected to uninstall." 8 50 + SELECTED_SERVICES="" + else + SELECTED_SERVICES=$(eval "whiptail --title 'Select Services to Uninstall' \ + --checklist 'WARNING: Data will be backed up but services will be removed' 25 78 17 \ + $UNINSTALL_OPTIONS \ + 3>&1 1>&2 2>&3") fi - done - for i in "${!_SVC_KEYS[@]}"; do - ${_SVC_FLAGS[$i]} && SELECTED_SERVICES="$SELECTED_SERVICES ${_SVC_KEYS[$i]}" - done - unset _SVC_FLAGS _SVC_KEYS _SVC_LABELS _T _mark _exist - ;; - 2) - MENU_MODE="UNINSTALL" - # Build list from only installed services - _SVC_KEYS=(); _SVC_LABELS=() - _add_if_exists() { [ -n "${EXISTING_SERVICES[$1]:-}" ] && _SVC_KEYS+=("$1") && _SVC_LABELS+=("$2"); } - _add_if_exists IMMICH "IMMICH — Photo & video backup" - _add_if_exists AUDIOBOOKSHELF "AUDIOBOOKSHELF — Audiobook server" - _add_if_exists EMBY "EMBY — Media server" - _add_if_exists ARM "ARM — Automatic Ripping Machine" - _add_if_exists FILEBROWSER "FILEBROWSER — Web file manager" - _add_if_exists MAGICMIRROR "MAGICMIRROR — Smart mirror" - _add_if_exists ACTUALBUDGET "ACTUALBUDGET — Personal finance" - _add_if_exists KEYCLOAK "KEYCLOAK — Identity management" - _add_if_exists CADDY "CADDY — Reverse proxy" - _add_if_exists FAIL2BAN "FAIL2BAN — Intrusion prevention" - _add_if_exists LYRION "LYRION — Music server" - _add_if_exists MEALIE "MEALIE — Recipe manager" - _add_if_exists MINECRAFT "MINECRAFT — Game server" - _add_if_exists JELLYFIN "JELLYFIN — Media server" - _add_if_exists FRIGATE "FRIGATE — NVR cameras" - _add_if_exists NTFY "NTFY — Push notifications" - _add_if_exists UPTIMEKUMA "UPTIMEKUMA — Service monitoring" - _add_if_exists WGEASY "WGEASY — WireGuard VPN" - _add_if_exists TRACCAR "TRACCAR — GPS tracking" - _add_if_exists PORTAINER "PORTAINER — Docker management" - _add_if_exists MESHCENTRAL "MESHCENTRAL — Remote management" - _add_if_exists FINDMYDEVICE "FINDMYDEVICE — Device tracking" - _add_if_exists FRIGATE_NOTIFY "FRIGATE_NOTIFY — Frigate notifications" - _add_if_exists WATCHTOWER "WATCHTOWER — Container updates" - unset -f _add_if_exists - - if [ ${#_SVC_KEYS[@]} -eq 0 ]; then - echo "No installed services detected to uninstall." + ;; + 3|*) + # Cancel SELECTED_SERVICES="" MENU_MODE="CANCEL" - else - declare -a _SVC_FLAGS - for i in "${!_SVC_KEYS[@]}"; do _SVC_FLAGS[$i]=true; done - echo "" - echo " WARNING: selected services will be removed (data backed up first)." - echo " Type number to toggle, Enter to confirm." - while true; do - echo "" - for i in "${!_SVC_KEYS[@]}"; do - printf " [%s] %2d. %s\n" "$( ${_SVC_FLAGS[$i]} && echo '*' || echo ' ')" "$((i+1))" "${_SVC_LABELS[$i]}" - done - echo "" - read -p " Toggle [number] or Enter to confirm: " _T - if [[ -z "$_T" ]]; then - break - elif [[ "$_T" =~ ^[0-9]+$ ]] && [[ "$_T" -ge 1 ]] && [[ "$_T" -le "${#_SVC_KEYS[@]}" ]]; then - idx=$((_T-1)) - ${_SVC_FLAGS[$idx]} && _SVC_FLAGS[$idx]=false || _SVC_FLAGS[$idx]=true - fi - done - for i in "${!_SVC_KEYS[@]}"; do - ${_SVC_FLAGS[$i]} && SELECTED_SERVICES="$SELECTED_SERVICES ${_SVC_KEYS[$i]}" - done - unset _SVC_FLAGS _SVC_KEYS _SVC_LABELS _T - fi - ;; - 3|*) + ;; + esac + + # Check if user cancelled + if [ $? -ne 0 ]; then + echo "Service selection cancelled. Skipping Docker applications." SELECTED_SERVICES="" - MENU_MODE="CANCEL" - ;; - esac + fi - # Set installation flags from SELECTED_SERVICES - WHIPTAIL_USED=true - : ${INSTALL_IMMICH:="n"} - : ${INSTALL_AUDIOBOOKSHELF:="n"} - : ${INSTALL_EMBY:="n"} - : ${INSTALL_ARM:="n"} - : ${INSTALL_FILEBROWSER:="n"} - : ${INSTALL_MAGICMIRROR:="n"} - : ${INSTALL_ACTUALBUDGET:="n"} - : ${INSTALL_KEYCLOAK:="n"} - : ${INSTALL_CADDY:="n"} - : ${INSTALL_FAIL2BAN:="n"} - : ${INSTALL_LMS:="n"} - : ${INSTALL_MEALIE:="n"} - : ${INSTALL_MINECRAFT:="n"} - : ${INSTALL_JELLYFIN:="n"} - : ${INSTALL_FRIGATE:="n"} - : ${INSTALL_NTFY:="n"} - : ${INSTALL_UPTIMEKUMA:="n"} - : ${INSTALL_WGEASY:="n"} - : ${INSTALL_TRACCAR:="n"} - : ${INSTALL_PORTAINER:="n"} - : ${INSTALL_MESHCENTRAL_SERVER:="n"} - : ${INSTALL_FMD:="n"} - : ${INSTALL_FRIGATE_NOTIFY:="n"} - : ${INSTALL_WATCHTOWER:="n"} + # Parse selections (whiptail returns quoted strings) + # Only set to "n" if not already set (preserve any earlier choices) + : ${INSTALL_IMMICH:="n"} + : ${INSTALL_AUDIOBOOKSHELF:="n"} + : ${INSTALL_EMBY:="n"} + : ${INSTALL_ARM:="n"} + : ${INSTALL_FILEBROWSER:="n"} + : ${INSTALL_MAGICMIRROR:="n"} + : ${INSTALL_ACTUALBUDGET:="n"} + : ${INSTALL_KEYCLOAK:="n"} + : ${INSTALL_CADDY:="n"} + : ${INSTALL_FAIL2BAN:="n"} + : ${INSTALL_LMS:="n"} + : ${INSTALL_MEALIE:="n"} + : ${INSTALL_MINECRAFT:="n"} + : ${INSTALL_JELLYFIN:="n"} + : ${INSTALL_FRIGATE:="n"} + : ${INSTALL_NTFY:="n"} + : ${INSTALL_UPTIMEKUMA:="n"} + : ${INSTALL_WGEASY:="n"} + : ${INSTALL_TRACCAR:="n"} + : ${INSTALL_PORTAINER:="n"} + : ${INSTALL_MESHCENTRAL_SERVER:="n"} + : ${INSTALL_FMD:="n"} + : ${INSTALL_FRIGATE_NOTIFY:="n"} + : ${INSTALL_WATCHTOWER:="n"} - if echo "$SELECTED_SERVICES" | grep -q "IMMICH"; then INSTALL_IMMICH="y"; fi - if echo "$SELECTED_SERVICES" | grep -q "AUDIOBOOKSHELF"; then INSTALL_AUDIOBOOKSHELF="y"; fi - if echo "$SELECTED_SERVICES" | grep -q "EMBY"; then INSTALL_EMBY="y"; fi - if echo "$SELECTED_SERVICES" | grep -q "ARM"; then INSTALL_ARM="y"; fi - if echo "$SELECTED_SERVICES" | grep -q "FILEBROWSER"; then INSTALL_FILEBROWSER="y"; fi - if echo "$SELECTED_SERVICES" | grep -q "MAGICMIRROR"; then INSTALL_MAGICMIRROR="y"; fi - if echo "$SELECTED_SERVICES" | grep -q "ACTUALBUDGET"; then INSTALL_ACTUALBUDGET="y"; fi - if echo "$SELECTED_SERVICES" | grep -q "KEYCLOAK"; then INSTALL_KEYCLOAK="y"; fi - if echo "$SELECTED_SERVICES" | grep -q "CADDY"; then INSTALL_CADDY="y"; fi - if echo "$SELECTED_SERVICES" | grep -q "FAIL2BAN"; then INSTALL_FAIL2BAN="y"; fi - if echo "$SELECTED_SERVICES" | grep -q "LYRION"; then INSTALL_LMS="y"; fi - if echo "$SELECTED_SERVICES" | grep -q "MEALIE"; then INSTALL_MEALIE="y"; fi - if echo "$SELECTED_SERVICES" | grep -q "MINECRAFT"; then INSTALL_MINECRAFT="y"; fi - if echo "$SELECTED_SERVICES" | grep -q "JELLYFIN"; then INSTALL_JELLYFIN="y"; fi - if echo "$SELECTED_SERVICES" | grep -q " FRIGATE "; then INSTALL_FRIGATE="y"; fi - if echo "$SELECTED_SERVICES" | grep -q "NTFY"; then INSTALL_NTFY="y"; fi - if echo "$SELECTED_SERVICES" | grep -q "UPTIMEKUMA"; then INSTALL_UPTIMEKUMA="y"; fi - if echo "$SELECTED_SERVICES" | grep -q "WGEASY"; then INSTALL_WGEASY="y"; fi - if echo "$SELECTED_SERVICES" | grep -q "TRACCAR"; then INSTALL_TRACCAR="y"; fi - if echo "$SELECTED_SERVICES" | grep -q "PORTAINER"; then INSTALL_PORTAINER="y"; fi - if echo "$SELECTED_SERVICES" | grep -q "MESHCENTRAL"; then INSTALL_MESHCENTRAL_SERVER="y"; fi - if echo "$SELECTED_SERVICES" | grep -q "FINDMYDEVICE"; then INSTALL_FMD="y"; fi - if echo "$SELECTED_SERVICES" | grep -q "FRIGATE_NOTIFY"; then INSTALL_FRIGATE_NOTIFY="y"; fi - if echo "$SELECTED_SERVICES" | grep -q "WATCHTOWER"; then INSTALL_WATCHTOWER="y"; fi + # Set installation flags based on selections + if echo "$SELECTED_SERVICES" | grep -q "IMMICH"; then INSTALL_IMMICH="y"; fi + if echo "$SELECTED_SERVICES" | grep -q "AUDIOBOOKSHELF"; then INSTALL_AUDIOBOOKSHELF="y"; fi + if echo "$SELECTED_SERVICES" | grep -q "EMBY"; then INSTALL_EMBY="y"; fi + if echo "$SELECTED_SERVICES" | grep -q "ARM"; then INSTALL_ARM="y"; fi + if echo "$SELECTED_SERVICES" | grep -q "FILEBROWSER"; then INSTALL_FILEBROWSER="y"; fi + if echo "$SELECTED_SERVICES" | grep -q "MAGICMIRROR"; then INSTALL_MAGICMIRROR="y"; fi + if echo "$SELECTED_SERVICES" | grep -q "ACTUALBUDGET"; then INSTALL_ACTUALBUDGET="y"; fi + if echo "$SELECTED_SERVICES" | grep -q "KEYCLOAK"; then INSTALL_KEYCLOAK="y"; fi + if echo "$SELECTED_SERVICES" | grep -q "CADDY"; then INSTALL_CADDY="y"; fi + if echo "$SELECTED_SERVICES" | grep -q "FAIL2BAN"; then INSTALL_FAIL2BAN="y"; fi + if echo "$SELECTED_SERVICES" | grep -q "LYRION"; then INSTALL_LMS="y"; fi + if echo "$SELECTED_SERVICES" | grep -q "MEALIE"; then INSTALL_MEALIE="y"; fi + if echo "$SELECTED_SERVICES" | grep -q "MINECRAFT"; then INSTALL_MINECRAFT="y"; fi + if echo "$SELECTED_SERVICES" | grep -q "JELLYFIN"; then INSTALL_JELLYFIN="y"; fi + if echo "$SELECTED_SERVICES" | grep -q "FRIGATE\""; then INSTALL_FRIGATE="y"; fi + if echo "$SELECTED_SERVICES" | grep -q "NTFY"; then INSTALL_NTFY="y"; fi + if echo "$SELECTED_SERVICES" | grep -q "UPTIMEKUMA"; then INSTALL_UPTIMEKUMA="y"; fi + if echo "$SELECTED_SERVICES" | grep -q "WGEASY"; then INSTALL_WGEASY="y"; fi + if echo "$SELECTED_SERVICES" | grep -q "TRACCAR"; then INSTALL_TRACCAR="y"; fi + if echo "$SELECTED_SERVICES" | grep -q "PORTAINER"; then INSTALL_PORTAINER="y"; fi + if echo "$SELECTED_SERVICES" | grep -q "MESHCENTRAL"; then INSTALL_MESHCENTRAL_SERVER="y"; fi + if echo "$SELECTED_SERVICES" | grep -q "FINDMYDEVICE"; then INSTALL_FMD="y"; fi + if echo "$SELECTED_SERVICES" | grep -q "FRIGATE_NOTIFY"; then INSTALL_FRIGATE_NOTIFY="y"; fi + if echo "$SELECTED_SERVICES" | grep -q "WATCHTOWER"; then INSTALL_WATCHTOWER="y"; fi - if [ "$MENU_MODE" = "UNINSTALL" ]; then + if [ "$MENU_MODE" = "UNINSTALL" ]; then echo "" echo "Services selected for uninstallation:" echo "$SELECTED_SERVICES" | tr '"' '\n' | grep -v '^$' | sed 's/^/ - /' @@ -4061,12 +4062,25 @@ else fi # After uninstall, exit the Docker apps section + WHIPTAIL_USED=true + elif [ "$MENU_MODE" = "INSTALL" ]; then echo "" echo "Selected services:" - echo "$SELECTED_SERVICES" | tr ' ' '\n' | grep -v '^$' | sed 's/^/ - /' + echo "$SELECTED_SERVICES" | tr '"' '\n' | grep -v '^$' | sed 's/^/ - /' echo "" + + # Mark that whiptail was used - skip all individual prompts + WHIPTAIL_USED=true + else + # Cancel or other mode + WHIPTAIL_USED=false fi + else + # Whiptail not available - will use individual prompts + WHIPTAIL_USED=false + MENU_MODE="INSTALL" + fi # Skip installation section if we just did uninstalls if [ "$MENU_MODE" = "UNINSTALL" ]; then From 19346ba867b15841384bcfef1090c253442506c3 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 21 Mar 2026 02:08:21 +0000 Subject: [PATCH 3/4] Use numbered input for size tier selection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The tier table now shows 1–4 numbers so you type 1/2/3/4 instead of typing "14B" or "32B". Tier names are still accepted as fallback. Recommended tier is shown as a number (e.g. [2] instead of [14B]). https://claude.ai/code/session_012gDnantBmFTWZGCiKyjazx --- laptop_full_setup.sh | 76 +++++++++++++++++++++++++++----------------- 1 file changed, 47 insertions(+), 29 deletions(-) diff --git a/laptop_full_setup.sh b/laptop_full_setup.sh index 55e037f..b06c8c5 100755 --- a/laptop_full_setup.sh +++ b/laptop_full_setup.sh @@ -333,33 +333,6 @@ if $INSTALL_AI; then MODEL_PREF="${MODEL_PREF:-1}" # Q4 approximate sizes in GB: 7B=4, 13-15B=9, 22B=13, 32B=19, 70-72B=41 - echo "" - echo " Size tier — estimated speed on your ${VRAM_GB}GB GPU:" - echo " (No hard limits — Ollama uses all available VRAM automatically)" - echo "" - printf " %-8s %-42s %s\n" "Tier" "Models" "Speed on your system" - printf " %-8s %-42s %s\n" "--------" "------------------------------------------" "--------------------" - - case "$MODEL_PREF" in - 1) # Western - printf " %-8s %-42s %s\n" "7B" "mistral:7b + codellama:7b" "$(speed_label 4)" - printf " %-8s %-42s %s\n" "14B" "phi4:14b + starcoder2:15b" "$(speed_label 9)" - printf " %-8s %-42s %s\n" "22B" "phi4:14b + codestral:22b" "$(speed_label 13)" - printf " %-8s %-42s %s\n" "70B" "llama3.3:70b + codestral:22b" "$(speed_label 41)" - ;; - 2) # Performance - printf " %-8s %-42s %s\n" "7B" "qwen2.5:7b + qwen2.5-coder:7b" "$(speed_label 4)" - printf " %-8s %-42s %s\n" "14B" "qwen2.5:14b + qwen2.5-coder:14b" "$(speed_label 9)" - printf " %-8s %-42s %s\n" "32B" "qwen2.5:14b + qwen2.5-coder:32b" "$(speed_label 19)" - printf " %-8s %-42s %s\n" "72B" "qwen2.5:72b + qwen2.5-coder:32b" "$(speed_label 41)" - ;; - 3) # Mixed - printf " %-8s %-42s %s\n" "7B" "mistral:7b + qwen2.5-coder:7b" "$(speed_label 4)" - printf " %-8s %-42s %s\n" "14B" "phi4:14b + qwen2.5-coder:14b" "$(speed_label 9)" - printf " %-8s %-42s %s\n" "32B" "phi4:14b + qwen2.5-coder:32b" "$(speed_label 19)" - printf " %-8s %-42s %s\n" "70B" "llama3.3:70b + qwen2.5-coder:32b" "$(speed_label 41)" - ;; - esac # VRAM-based recommendation (soft — shown as suggestion only) if [[ "$VRAM_GB" -ge 20 ]]; then REC_TIER="32B" @@ -370,6 +343,44 @@ if $INSTALL_AI; then # performance pref has no 22B tier [[ "$MODEL_PREF" == "2" && "$REC_TIER" == "22B" ]] && REC_TIER="32B" + echo "" + echo " Size tier — estimated speed on your ${VRAM_GB}GB GPU:" + echo " (No hard limits — Ollama uses all available VRAM automatically)" + echo "" + printf " %-4s %-8s %-42s %s\n" "#" "Tier" "Models" "Speed on your system" + printf " %-4s %-8s %-42s %s\n" "----" "--------" "------------------------------------------" "--------------------" + + declare -a _TIER_NAMES + case "$MODEL_PREF" in + 1) # Western + _TIER_NAMES=(7B 14B 22B 70B) + printf " %-4s %-8s %-42s %s\n" "1)" "7B" "mistral:7b + codellama:7b" "$(speed_label 4)" + printf " %-4s %-8s %-42s %s\n" "2)" "14B" "phi4:14b + starcoder2:15b" "$(speed_label 9)" + printf " %-4s %-8s %-42s %s\n" "3)" "22B" "phi4:14b + codestral:22b" "$(speed_label 13)" + printf " %-4s %-8s %-42s %s\n" "4)" "70B" "llama3.3:70b + codestral:22b" "$(speed_label 41)" + ;; + 2) # Performance + _TIER_NAMES=(7B 14B 32B 72B) + printf " %-4s %-8s %-42s %s\n" "1)" "7B" "qwen2.5:7b + qwen2.5-coder:7b" "$(speed_label 4)" + printf " %-4s %-8s %-42s %s\n" "2)" "14B" "qwen2.5:14b + qwen2.5-coder:14b" "$(speed_label 9)" + printf " %-4s %-8s %-42s %s\n" "3)" "32B" "qwen2.5:14b + qwen2.5-coder:32b" "$(speed_label 19)" + printf " %-4s %-8s %-42s %s\n" "4)" "72B" "qwen2.5:72b + qwen2.5-coder:32b" "$(speed_label 41)" + ;; + 3) # Mixed + _TIER_NAMES=(7B 14B 32B 70B) + printf " %-4s %-8s %-42s %s\n" "1)" "7B" "mistral:7b + qwen2.5-coder:7b" "$(speed_label 4)" + printf " %-4s %-8s %-42s %s\n" "2)" "14B" "phi4:14b + qwen2.5-coder:14b" "$(speed_label 9)" + printf " %-4s %-8s %-42s %s\n" "3)" "32B" "phi4:14b + qwen2.5-coder:32b" "$(speed_label 19)" + printf " %-4s %-8s %-42s %s\n" "4)" "70B" "llama3.3:70b + qwen2.5-coder:32b" "$(speed_label 41)" + ;; + esac + + # Figure out recommended number from REC_TIER + REC_NUM=2 + for i in "${!_TIER_NAMES[@]}"; do + [[ "${_TIER_NAMES[$i]}" == "$REC_TIER" ]] && REC_NUM=$((i+1)) + done + echo "" if [[ "$MODEL_PREF" == "4" ]]; then @@ -382,8 +393,15 @@ if $INSTALL_AI; then read -rp " Code model [$CODE_MODEL]: " _in; CODE_MODEL="${_in:-$CODE_MODEL}" read -rp " Reasoning model (Enter to skip): " REASON_MODEL else - read -rp " Choose tier [$REC_TIER]: " TIER_PICK - TIER_PICK="${TIER_PICK:-$REC_TIER}" + read -rp " Choose tier [$REC_NUM]: " _TIER_INPUT + _TIER_INPUT="${_TIER_INPUT:-$REC_NUM}" + # Accept either a number (1-4) or the tier name directly + if [[ "$_TIER_INPUT" =~ ^[1-4]$ ]]; then + TIER_PICK="${_TIER_NAMES[$((_TIER_INPUT-1))]}" + else + TIER_PICK="$_TIER_INPUT" + fi + unset _TIER_NAMES _TIER_INPUT REC_NUM case "${MODEL_PREF}:${TIER_PICK}" in # Western From 01ab7b8cc9abc8f61a777ec5e06e0f8c9fe9e870 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 21 Mar 2026 02:11:39 +0000 Subject: [PATCH 4/4] Number drive mount points for Ollama and Kiwix storage selection Both [2/6] and [3/6] prompts now show detected mount points as a numbered list. Type 0 for default/Docker volume, or 1/2/3 to pick a drive directly. Typing a path manually still works as fallback. https://claude.ai/code/session_012gDnantBmFTWZGCiKyjazx --- laptop_full_setup.sh | 73 ++++++++++++++++++++++++++++---------------- 1 file changed, 47 insertions(+), 26 deletions(-) diff --git a/laptop_full_setup.sh b/laptop_full_setup.sh index b06c8c5..212fc64 100755 --- a/laptop_full_setup.sh +++ b/laptop_full_setup.sh @@ -225,25 +225,35 @@ if $INSTALL_AI; then echo -e " ${BOLD}[2/6] Where should Ollama models be stored?${NC}" echo " (Models are large — 5-50GB each. A fast SSD or large HDD is ideal.)" echo "" - echo " Available mount points with >20GB free:" - df -h --output=target,avail,fstype 2>/dev/null \ - | awk 'NR>1 && $2~/[0-9]/ { - val=$2; unit=substr(val,length(val)); - num=substr(val,1,length(val)-1)+0; - if ((unit=="G" && num>=20) || unit=="T") print " " $0 - }' | head -10 + mapfile -t _MPTS < <( + df -h --output=target,avail,fstype 2>/dev/null \ + | awk 'NR>1 && $2~/[0-9]/ { + val=$2; unit=substr(val,length(val)); + num=substr(val,1,length(val)-1)+0; + if ((unit=="G" && num>=20) || unit=="T") print $0 + }' | head -10 + ) + echo " 0) Docker volume (default — /var/lib/docker/volumes/)" + for _i in "${!_MPTS[@]}"; do + printf " %d) %s\n" "$((_i+1))" "${_MPTS[$_i]}" + done echo "" - echo " 1) Docker volume (default — stored in /var/lib/docker/volumes/)" - echo " 2) Custom path (e.g. /mnt/ssd/ollama or /data/ollama)" - echo "" - read -rp " Choice [1]: " STORAGE_CHOICE - STORAGE_CHOICE="${STORAGE_CHOICE:-1}" - if [[ "$STORAGE_CHOICE" == "2" ]]; then - read -rp " Enter full path for Ollama models: " OLLAMA_HOST_PATH - OLLAMA_HOST_PATH="${OLLAMA_HOST_PATH%/}" # strip trailing slash + read -rp " Choice [0]: " STORAGE_CHOICE + STORAGE_CHOICE="${STORAGE_CHOICE:-0}" + if [[ "$STORAGE_CHOICE" == "0" ]]; then + OLLAMA_STORAGE="volume" + elif [[ "$STORAGE_CHOICE" =~ ^[0-9]+$ ]] && (( STORAGE_CHOICE >= 1 && STORAGE_CHOICE <= ${#_MPTS[@]} )); then + _MP=$(awk '{print $1}' <<< "${_MPTS[$(( STORAGE_CHOICE-1 ))]}") + OLLAMA_HOST_PATH="$_MP/ollama-models" + OLLAMA_STORAGE="bind" + ok "Ollama models → $OLLAMA_HOST_PATH" + else + # Fallback: treat input as a literal path + OLLAMA_HOST_PATH="${STORAGE_CHOICE%/}" [[ -z "$OLLAMA_HOST_PATH" ]] && die "No path entered." OLLAMA_STORAGE="bind" fi + unset _MPTS _MP _i fi # ── Q3: Storage for Kiwix ZIMs ──────────────────────────────────────────────── @@ -254,19 +264,30 @@ if $SVC_KIWIX; then echo -e " ${BOLD}[3/6] Where should Kiwix ZIM files be stored?${NC}" echo " (ZIMs are large — Wikipedia alone is ~46GB. Total collection ~130GB.)" echo "" - echo " Available mount points with >50GB free:" - df -h --output=target,avail,fstype 2>/dev/null \ - | awk 'NR>1 && $2~/[0-9]/ { - val=$2; unit=substr(val,length(val)); - num=substr(val,1,length(val)-1)+0; - if ((unit=="G" && num>=50) || unit=="T") print " " $0 - }' | head -10 + mapfile -t _MPTS < <( + df -h --output=target,avail,fstype 2>/dev/null \ + | awk 'NR>1 && $2~/[0-9]/ { + val=$2; unit=substr(val,length(val)); + num=substr(val,1,length(val)-1)+0; + if ((unit=="G" && num>=50) || unit=="T") print $0 + }' | head -10 + ) + echo " 0) Default: $KIWIX_DIR" + for _i in "${!_MPTS[@]}"; do + printf " %d) %s\n" "$((_i+1))" "${_MPTS[$_i]}" + done echo "" - echo " Default: $KIWIX_DIR" - read -rp " Press Enter to use default, or type a different path: " KIWIX_INPUT - if [[ -n "$KIWIX_INPUT" ]]; then - KIWIX_DIR="${KIWIX_INPUT%/}" + read -rp " Choice [0]: " KIWIX_CHOICE + KIWIX_CHOICE="${KIWIX_CHOICE:-0}" + if [[ "$KIWIX_CHOICE" != "0" ]] && [[ "$KIWIX_CHOICE" =~ ^[0-9]+$ ]] && (( KIWIX_CHOICE >= 1 && KIWIX_CHOICE <= ${#_MPTS[@]} )); then + _MP=$(awk '{print $1}' <<< "${_MPTS[$(( KIWIX_CHOICE-1 ))]}") + KIWIX_DIR="$_MP/kiwix" + ok "Kiwix ZIMs → $KIWIX_DIR" + elif [[ "$KIWIX_CHOICE" != "0" ]] && [[ -n "$KIWIX_CHOICE" ]]; then + # Fallback: treat as a literal path + KIWIX_DIR="${KIWIX_CHOICE%/}" fi + unset _MPTS _MP _i _KIWIX_CHOICE # ── Q4: Download ZIMs now? ───────────────────────────────────────────────── echo ""