Phase 3 of docs/roadmap/storage-locations.md. The step appears only when the candidate scan finds a filesystem LibrePortal isn't already using, so the single-disk case — which is most boxes — is completely unchanged. It sits before Recommended because a location has to exist before an app can be placed on it. Supporting two conditional steps meant the wizard could no longer treat 'position in the DOM' and 'step index' as the same number: Metrics was advanced-only and got away with 'length minus one', but a step hidden in the MIDDLE leaves a gap. Navigation, progress, validation and submit now all run off _visibleSteps(), and section matching is by data-step rather than DOM position. Unusable candidates render greyed WITH the reason rather than being filtered out — 'why isn't my drive listed?' is a support burden, and 'exFAT can't store file ownership' is actionable. The step is skipped only when nothing usable was found at all. What the wizard sends is a request, not an instruction: setupApplyConfig feeds each path through storageAdd, so the fitness checks and the root helper's admission rules both re-run regardless of what arrived in the payload. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
237 lines
9.4 KiB
Bash
237 lines
9.4 KiB
Bash
#!/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 // []')
|
|
|
|
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"
|
|
else
|
|
isNotice "Could not register '$s' as a storage location — continuing without it."
|
|
fi
|
|
fi
|
|
i=$((i+1))
|
|
done
|
|
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_<APP>_<OPT>=<bool>) and dockerInstallApp writes
|
|
# them into the template config before install<App> 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"
|
|
}
|