#!/bin/bash setupApplyConfig() { local payload_b64="$1" if [[ -z "$payload_b64" ]]; then isError "setupApplyConfig: no payload provided" return 1 fi local payload payload=$(echo "$payload_b64" | base64 -d 2>/dev/null) if [[ -z "$payload" ]]; then isError "setupApplyConfig: failed to decode payload" return 1 fi isHeader "Applying Setup Wizard Configuration" local install_name=$(echo "$payload" | jq -r '.install_name // empty') local timezone=$(echo "$payload" | jq -r '.timezone // empty') local install_level=$(echo "$payload" | jq -r '.install_level // empty') local dev_mode=$(echo "$payload" | jq -r '.dev_mode // empty') local traefik_email=$(echo "$payload" | jq -r '.traefik_email // empty') local domains_json=$(echo "$payload" | jq -c '.domains // []') local storage_json=$(echo "$payload" | jq -c '.storage // []') local storage_fstab_json=$(echo "$payload" | jq -c '.storage_fstab // []') local storage_default=$(echo "$payload" | jq -r '.storage_default // "primary"') local backup_dest=$(echo "$payload" | jq -r '.backup_dest // ""') if [[ -n "$install_name" ]]; then updateConfigOption "CFG_INSTALL_NAME" "$install_name" isSuccessful "Install name set to '$install_name'" fi if [[ -n "$timezone" ]]; then updateConfigOption "CFG_TIMEZONE" "$timezone" isSuccessful "Timezone set to '$timezone'" fi # Experience level — seeds the WebUI's Advanced UI mode on first paint # so a Beginner gets a stripped-down view and an Advanced user sees # everything by default. The WebUI also exposes a per-browser toggle # that overrides this; we just provide the install-time default. if [[ "$install_level" == "beginner" || "$install_level" == "advanced" ]]; then updateConfigOption "CFG_INSTALL_LEVEL" "$install_level" isSuccessful "Experience level set to '$install_level'" fi # Developer mode — opt-in via the wizard's Advanced-card easter egg (10 # taps). Unlocks the **DEV**-marked CFG_* fields across the WebUI. if [[ "$dev_mode" == "true" ]]; then updateConfigOption "CFG_DEV_MODE" "true" isSuccessful "Developer mode enabled" fi # Storage locations ticked on the wizard's Storage step. This is a REQUEST, # not an instruction: storageAdd re-runs the fitness checks and the root # helper re-runs admission, so a path that should not be accepted is not, # no matter what arrived in the payload. local storage_count=$(echo "$storage_json" | jq -r 'length') if [[ "$storage_count" -gt 0 ]]; then local s i=0 while [[ $i -lt $storage_count ]]; do s=$(echo "$storage_json" | jq -r ".[$i]") if [[ -n "$s" && "$s" != "null" ]]; then # Name it after the mount point's basename — short, recognisable, # and the user can rename it later from the location config. local sname="${s##*/}" [[ -z "$sname" ]] && sname="disk$((i+1))" if storageAdd "$s" "$sname" >/dev/null; then isSuccessful "Storage location '$sname' registered at $s" # Only when explicitly asked on the Storage step. The helper # validates independently and writes with nofail, so a # missing drive can never block boot. if echo "$storage_fstab_json" | jq -e --arg p "$s" 'index($p) != null' >/dev/null 2>&1; then local fline if fline=$(runStorage fstab-add "$s" 2>&1); then isSuccessful "Added to /etc/fstab so it mounts at boot: $fline" else isNotice "Could not update /etc/fstab for $s: $fline" fi fi else isNotice "Could not register '$s' as a storage location — continuing without it." fi fi i=$((i+1)) done fi # Where newly installed apps keep their data. Stored as a location NAME, not # a path: app configs ship CFG__STORAGE=default meaning "follow this", # so one choice at setup places every app installed afterwards without # touching 37 configs. Resolved AFTER registration, since the name only # exists once the location has been accepted. if [[ -n "$storage_default" && "$storage_default" != "primary" && "$storage_default" != "null" ]]; then local default_name="" if declare -f storageLocationName >/dev/null 2>&1; then default_name=$(storageLocationName "$storage_default" 2>/dev/null) || default_name="" fi if [[ -n "$default_name" && "$default_name" != "default" ]]; then updateConfigOption "CFG_STORAGE_DEFAULT" "$default_name" isSuccessful "New apps will store their data on '$default_name'" else isNotice "Could not resolve '$storage_default' to a storage location — new apps will use the system disk." fi fi # Backup destination chosen on the Backups step. Configured here rather than # left for the user to find later: the people most likely to need a restore # are the ones who never got round to setting one up. # # Created disabled by locationAdd, so enable it and initialise the repo — # an un-initialised location is a destination that silently backs up # nothing. if [[ -n "$backup_dest" && "$backup_dest" != "null" ]]; then local bpath bname if [[ "$backup_dest" == "primary" ]]; then bpath="${backup_dir%/}" bname="local" else bpath="${backup_dest%/}/libreportal-backups" bname="${backup_dest##*/}" fi local bidx if bidx=$(locationAdd "$bname" local 2>/dev/null | tail -1) && [[ "$bidx" =~ ^[0-9]+$ ]]; then local bcfg; bcfg=$(backupLocationConfig "$bidx") if [[ "$backup_dest" != "primary" ]]; then updateConfigOption "CFG_BACKUP_LOC_${bidx}_PATH_MODE" "custom" "$bcfg" >/dev/null updateConfigOption "CFG_BACKUP_LOC_${bidx}_PATH" "$bpath" "$bcfg" >/dev/null fi updateConfigOption "CFG_BACKUP_LOC_${bidx}_ENABLED" "true" "$bcfg" >/dev/null source "$bcfg" 2>/dev/null if engineInitLocation "$bidx" >/dev/null 2>&1; then isSuccessful "Backups will go to $bpath" # Deliberately NOT echoed here: the repository password is a # secret and task output is logged. It is shown on the Backup # page, which is what the wizard told the user. isNotice "The repository password is on the Backup page — keep a copy somewhere other than this machine." else isNotice "Backup location '$bname' was created but could not be initialised — open the Backup page to finish it." fi else isNotice "Could not create a backup location at $bpath — set one up from the Backup page." fi fi local domains_count=$(echo "$domains_json" | jq -r 'length') if [[ "$domains_count" -gt 0 ]]; then local i=0 while [[ $i -lt $domains_count && $i -lt 9 ]]; do local d=$(echo "$domains_json" | jq -r ".[$i]") updateConfigOption "CFG_DOMAIN_$((i+1))" "$d" isSuccessful "Domain $((i+1)) set to '$d'" ((i++)) done fi if [[ -n "$traefik_email" && "$traefik_email" != "null" ]]; then # CFG_TRAEFIK_EMAIL lives in containers/traefik/traefik.config, not in # the system $configs_dir, so findConfigFileForOption can't auto-locate # it. Point updateConfigOption at the source file directly. Traefik # may not be installed yet at this point — config gets copied from # install_containers_dir into containers_dir during the app-install # task, so we always update the source. local traefik_config_file="$install_containers_dir/traefik/traefik.config" if [[ -f "$traefik_config_file" ]]; then updateConfigOption "CFG_TRAEFIK_EMAIL" "$traefik_email" "$traefik_config_file" isSuccessful "Traefik LetsEncrypt email set to '$traefik_email'" else isNotice "Traefik source config not found at $traefik_config_file; skipping email write." fi fi # App sub-options are no longer handled here. The setup-routes backend # folds payload.appOptions into each install command's config_variables # arg (CFG_REQUIREMENT__=) and dockerInstallApp writes # them into the template config before install runs. sourceScanFiles "libreportal_configs" isSuccessful "Configuration written. Selected apps will install next." } setupApplyFinalize() { # setup_group is passed by the WebUI finalize task so we can read back the # sibling app-install tasks and tell "install ready" apart from "install # ran but an app failed". Empty when finalize is invoked standalone. local setup_group="$1" isNotice "Initializing backup engine..." if declare -f installResticHost >/dev/null 2>&1; then installResticHost else isNotice "installResticHost not loaded; backup repos will init on first backup." fi isNotice "Refreshing WebUI data snapshots so the config page reflects wizard changes..." if declare -f webuiLibrePortalUpdate >/dev/null 2>&1; then webuiLibrePortalUpdate else isNotice "webuiLibrePortalUpdate not loaded; skipping refresh." fi if declare -f webuiGenerateBackupLocations >/dev/null 2>&1; then webuiGenerateBackupLocations webuiGenerateBackupDashboard webuiGenerateBackupSnapshots all webuiGenerateBackupAppStatus fi setupWizardMarkComplete # Roll up the per-app install results for this setup group. Each ticked app # ran as its own `app install` task; the host daemon writes their terminal # status back into the task JSON before it ever reaches this finalize task # (FIFO, one at a time), so by now every sibling is settled. We only LOG the # verdict here — finalize itself still succeeds (it did finalize) and the # failed app's own task row is already red; the WebUI watcher is what gates # the "your install is ready" hand-off on this same group-level result. if [[ -n "$setup_group" ]]; then local tasks_dir="$(webuiDir)/frontend/data/tasks" local total=0 failed=0 failed_names="" f if [[ -d "$tasks_dir" ]]; then for f in "$tasks_dir"/task_*.json; do [[ -f "$f" ]] || continue local grp role grp=$(jq -r '.setupGroup // empty' "$f" 2>/dev/null) role=$(jq -r '.setupRole // empty' "$f" 2>/dev/null) [[ "$grp" == "$setup_group" && "$role" == "app" ]] || continue total=$((total + 1)) local st ec app st=$(jq -r '.status // empty' "$f" 2>/dev/null) ec=$(jq -r '.exit_code // .exitCode // empty' "$f" 2>/dev/null) app=$(jq -r '.app // "app"' "$f" 2>/dev/null) if [[ "$st" == "failed" || "$st" == "cancelled" \ || ( -n "$ec" && "$ec" != "0" && "$ec" != "null" ) ]]; then failed=$((failed + 1)) failed_names+="${failed_names:+, }$app" fi done fi if [[ "$total" -eq 0 ]]; then isNotice "No apps were selected — add apps any time from the App Center." elif [[ "$failed" -eq 0 ]]; then isSuccessful "All $total selected app(s) installed successfully." else isError "$failed of $total app(s) failed to install: ${failed_names}. Setup is marked complete — retry the failed app(s) from the App Center." fi fi isSuccessful "Setup Wizard complete — your install is configured and ready." } setupApply() { setupApplyConfig "$1" || return 1 local payload=$(echo "$1" | base64 -d 2>/dev/null) local apps_json=$(echo "$payload" | jq -c '.apps // []') local apps_count=$(echo "$apps_json" | jq -r 'length') if [[ "$apps_count" -gt 0 ]]; then isHeader "Installing Selected Apps" local i=0 while [[ $i -lt $apps_count ]]; do local app_name=$(echo "$apps_json" | jq -r ".[$i]") isNotice "[$((i+1))/$apps_count] Installing $app_name..." dockerInstallApp "$app_name" ((i++)) done fi setupApplyFinalize } setupGenerateName() { if declare -f generateInstallName >/dev/null 2>&1; then generateInstallName else echo "QuantumOtter" fi } setupCheckDomainPointsHere() { local domain="$1" if [[ -z "$domain" ]]; then echo '{"matches":false,"error":"no domain"}' return 1 fi local server_ip server_ip=$(dig +short +time=3 +tries=1 myip.opendns.com @resolver1.opendns.com 2>/dev/null | head -1) [[ -z "$server_ip" ]] && server_ip=$(hostname -I 2>/dev/null | awk '{print $1}') local domain_ip domain_ip=$(dig +short +time=3 +tries=1 "$domain" A 2>/dev/null | head -1) local matches="false" [[ -n "$server_ip" && "$server_ip" == "$domain_ip" ]] && matches="true" printf '{"matches":%s,"server_ip":"%s","domain_ip":"%s"}\n' "$matches" "$server_ip" "$domain_ip" }