diff --git a/containers/libreportal/frontend/core/setup/css/setup-wizard.css b/containers/libreportal/frontend/core/setup/css/setup-wizard.css
index 8604f06..3c82c30 100755
--- a/containers/libreportal/frontend/core/setup/css/setup-wizard.css
+++ b/containers/libreportal/frontend/core/setup/css/setup-wizard.css
@@ -295,11 +295,7 @@ body.setup-wizard-open {
.setup-field input[type=text],
.setup-field input[type=email],
-/* The storage default's select is outside .setup-field (it is a row, not a
- labelled field), so it would otherwise render as a raw native dropdown —
- white box, wrong font — against the glass panel. Share the styling. */
-.setup-field select,
-.setup-storage-default select {
+.setup-field select {
width: 100%;
background: rgba(var(--text-rgb), 0.06);
border: 1px solid rgba(var(--text-rgb), 0.12);
@@ -316,8 +312,7 @@ body.setup-wizard-open {
.setup-field input[type=text]:focus,
.setup-field input[type=email]:focus,
-.setup-field select:focus,
-.setup-storage-default select:focus {
+.setup-field select:focus {
outline: none;
background: rgba(var(--text-rgb), 0.10);
border-color: rgba(var(--accent-rgb), 0.55);
@@ -1390,4 +1385,9 @@ body.setup-wizard-open .eo-modal { z-index: 10000; }
margin-top: 12px;
}
.setup-storage-default-label { font-size: 0.92em; opacity: 0.9; }
-.setup-storage-default select { flex: 1; min-width: 180px; }
+/* custom-select.js replaces the native popup (the OS draws that one and
+ ignores our CSS) with a themed button + list. It enhances any
+ select.form-control, so the class is what makes this match the wizard's
+ other dropdowns rather than showing stock white chrome. */
+.setup-storage-default select,
+.setup-storage-default .custom-select { flex: 1; min-width: 180px; }
diff --git a/containers/libreportal/frontend/core/setup/js/setup-wizard.js b/containers/libreportal/frontend/core/setup/js/setup-wizard.js
index 3e6dafe..bbc84af 100755
--- a/containers/libreportal/frontend/core/setup/js/setup-wizard.js
+++ b/containers/libreportal/frontend/core/setup/js/setup-wizard.js
@@ -593,7 +593,7 @@ class SetupWizard {
box.style.display = '';
box.innerHTML = `
New apps store their data on
-
+
${opts.map(o => `${this.escapeHtml(o.label)} `).join('')}
`;
diff --git a/docs/guide/install-and-use.md b/docs/guide/install-and-use.md
index 4330d39..e1292f6 100644
--- a/docs/guide/install-and-use.md
+++ b/docs/guide/install-and-use.md
@@ -26,23 +26,29 @@ login), installs LibrePortal, and prints the WebUI address + a generated passwor
### Put data where you want it (separate disks, external drives)
-**The installer just asks.** If it finds a second drive, it offers it for app
-data before installing anything:
+**The installer just asks.** If it finds another drive, it offers it before
+installing anything — one list, two independent questions:
```
-Where should app data live?
+Where should LibrePortal keep things?
1) This disk (default) 911.9G 808.4G free
2) /mnt/bigdisk 3.6T 3.6T free
- LibrePortal itself stays on this disk either way — only app data moves.
+LibrePortal itself — settings, database, logs. Around 20 MB, and it stays small.
+Choose [1]:
+App data — everything your apps store. This is the one that grows.
Choose [1]:
```
+They're separate on purpose. On a desktop you usually only move the second. On
+a small board with an 8 GB eMMC and a USB SSD you move both.
+
It picks a subdirectory on the drive you choose, never the mount point itself,
-and skips the question entirely when there is nothing else to choose, when you
-passed `--containers-dir`, or when running unattended.
+and skips a question when the matching flag was already passed — or the whole
+thing when there is nothing else to choose, no terminal, or you're running
+unattended.
The three roots below are still there for scripted installs:
diff --git a/init.sh b/init.sh
index 6c2cea2..4689a47 100755
--- a/init.sh
+++ b/init.sh
@@ -188,32 +188,38 @@ libreportalDerivePaths() {
}
libreportalDerivePaths
-# Interactive disk picker for app data.
+# Interactive disk picker, in the shape an OS installer asks it.
#
-# The --containers-dir flag has existed for a while, but someone running the
-# curl|bash installer never learns it exists — so a box with a 4 TB disk sitting
-# next to a small system SSD quietly put everything on the SSD. This asks, once,
-# in the shape an OS installer asks it.
+# --system-dir/--containers-dir have existed for a while and nobody running the
+# curl|bash installer ever learned they existed, so a box with a 4 TB disk beside
+# a small system SSD quietly put everything on the SSD.
+#
+# Two roots, asked independently, because they are genuinely different choices:
+# the control plane is ~20 MB and never grows, while app data is what becomes
+# terabytes. On a normal desktop only the second matters; on an SBC with a small
+# eMMC and a USB SSD you want both moved. Backups stay on --backups-dir, which
+# is a scripted-install concern rather than a first-run one.
#
# Deliberately narrow:
-# * app data only. The control plane is ~20 MB and never grows; the thing
-# worth placing is the data. Offering three roots would be three questions
-# to answer badly.
-# * skipped entirely when unattended, when --containers-dir was passed, when
-# there is no TTY, or when there is nothing else to choose. A prompt with
-# one answer is not a question.
+# * one disk list, two short questions. Three questions nobody can answer at
+# that moment is worse than a flag they never see.
# * a SUBDIRECTORY on the chosen disk, never its mount point — that keeps the
# "root only ever takes an empty directory" rule intact and leaves anything
# already on the disk alone.
+# * candidates exclude anything on the same filesystem as /, since placing
+# data there gains nothing.
#
-# Self-contained (findmnt only), like the rest of init.sh: scripts/ is not
+# Skipped when unattended, with no TTY, when the matching flag was already
+# passed, or when the scan finds nothing else — a prompt with one possible
+# answer is not a question. Self-contained (findmnt only): scripts/ is not
# necessarily loadable this early.
-initPickContainersDir()
+initPickRoots()
{
[[ "$init_unattended_mode" == true ]] && return 0
- [[ -n "${LP_CONTAINERS_DIR_EXPLICIT:-}" ]] && return 0
[[ -t 0 && -t 1 ]] || return 0
command -v findmnt >/dev/null 2>&1 || return 0
+ # Nothing left to ask?
+ [[ -n "${LP_SYSTEM_DIR_EXPLICIT:-}" && -n "${LP_CONTAINERS_DIR_EXPLICIT:-}" ]] && return 0
local sys_dev; sys_dev=$(stat -c '%d' -- / 2>/dev/null)
local -a paths=() labels=()
@@ -239,7 +245,7 @@ initPickContainersDir()
squashfs|overlay|overlay2|aufs|tmpfs|devtmpfs|ramfs|iso9660|udf|vfat|exfat|ntfs|ntfs3|msdos|fuseblk|"") continue ;;
esac
dev=$(stat -c '%d' -- "$target" 2>/dev/null)
- [[ -n "$dev" && "$dev" == "$sys_dev" ]] && continue # same disk as / — no gain
+ [[ -n "$dev" && "$dev" == "$sys_dev" ]] && continue
paths+=("$target")
labels+=("$(printf '%-24s %-8s %s free' "$target" "${size:-?}" "${avail:-?}")")
done < <(findmnt -Pno TARGET,SOURCE,FSTYPE,SIZE,AVAIL 2>/dev/null)
@@ -251,7 +257,7 @@ initPickContainersDir()
root_avail=$(findmnt -no AVAIL --target / 2>/dev/null | tail -1)
echo ""
- isHeader "Where should app data live?"
+ isHeader "Where should LibrePortal keep things?"
echo ""
printf ' %s %-24s %-8s %s free\n' "1)" "This disk (default)" "${root_size:-?}" "${root_avail:-?}"
local i
@@ -259,22 +265,43 @@ initPickContainersDir()
printf ' %s %s\n' "$((i + 2)))" "${labels[$i]}"
done
echo ""
- echo " LibrePortal itself stays on this disk either way — only app data moves."
- echo ""
- local choice=""
- isQuestion "Choose [1]:"
- read -r choice
- echo ""
- [[ -z "$choice" || "$choice" == "1" ]] && return 0
- if ! [[ "$choice" =~ ^[0-9]+$ ]] || (( choice < 2 || choice > ${#paths[@]} + 1 )); then
- isNotice "Not a listed option — using this disk."
- return 0
+ # Ask for one root. Echoes the chosen mount point on STDOUT, or nothing for
+ # "this disk". Everything the human reads goes to stderr — stdout is the
+ # return channel here, and prompting on it makes the prompt part of the
+ # answer.
+ _initAskDisk() {
+ local prompt="$1" choice=""
+ echo "$prompt" >&2
+ isQuestion "Choose [1]:" >&2
+ read -r choice
+ echo "" >&2
+ [[ -z "$choice" || "$choice" == "1" ]] && return 0
+ if ! [[ "$choice" =~ ^[0-9]+$ ]] || (( choice < 2 || choice > ${#paths[@]} + 1 )); then
+ isNotice "Not a listed option — using this disk." >&2
+ return 0
+ fi
+ printf '%s' "${paths[$((choice - 2))]}"
+ }
+
+ local chosen
+ if [[ -z "${LP_SYSTEM_DIR_EXPLICIT:-}" ]]; then
+ chosen=$(_initAskDisk "LibrePortal itself — settings, database, logs. Around 20 MB, and it stays small.")
+ if [[ -n "$chosen" ]]; then
+ LP_SYSTEM_DIR="${chosen%/}/libreportal-system"
+ isSuccessful "LibrePortal will live in $LP_SYSTEM_DIR"
+ fi
fi
- local chosen="${paths[$((choice - 2))]}"
- LP_CONTAINERS_DIR="${chosen%/}/libreportal-containers"
- isSuccessful "App data will live in $LP_CONTAINERS_DIR"
+ if [[ -z "${LP_CONTAINERS_DIR_EXPLICIT:-}" ]]; then
+ chosen=$(_initAskDisk "App data — everything your apps store. This is the one that grows.")
+ if [[ -n "$chosen" ]]; then
+ LP_CONTAINERS_DIR="${chosen%/}/libreportal-containers"
+ isSuccessful "App data will live in $LP_CONTAINERS_DIR"
+ fi
+ fi
+
+ unset -f _initAskDisk
return 0
}
@@ -365,7 +392,7 @@ for ((i=1; i<=$#; i++)); do
# Relocatable roots (=form keeps the single-token shift logic). Validated
# by libreportalValidatePaths before any folder is created. Can also be set
# via the LP_*_DIR environment.
- --system-dir=*) LP_SYSTEM_DIR="${!i#*=}"; ((init_shift_count++)) ;;
+ --system-dir=*) LP_SYSTEM_DIR="${!i#*=}"; LP_SYSTEM_DIR_EXPLICIT=1; ((init_shift_count++)) ;;
--containers-dir=*) LP_CONTAINERS_DIR="${!i#*=}"; LP_CONTAINERS_DIR_EXPLICIT=1; ((init_shift_count++)) ;;
--backups-dir=*) LP_BACKUPS_DIR="${!i#*=}"; ((init_shift_count++)) ;;
--manager-user=*) LP_MANAGER_USER="${!i#*=}"; ((init_shift_count++)) ;;
@@ -2000,10 +2027,10 @@ if [[ $EUID -ne 0 ]]; then
exit 1
else
if [[ "$param1" == "init" ]]; then
- # Ask where app data goes before anything is validated or created.
- # No-op when unattended, when --containers-dir was passed, or when
- # there is only one possible answer.
- initPickContainersDir
+ # Ask where things go before anything is validated or created.
+ # No-op when unattended, when the flags were passed, or when there
+ # is only one possible answer.
+ initPickRoots
libreportalDerivePaths
# Validate the chosen install roots before creating/baking anything.