+ The system doesn\u2019t mount this drive at boot, so apps stored on it won\u2019t start
+ until it is mounted again. LibrePortal can add it to /etc/fstab for you.
+
+
${esc(c.fstab_line)}
+
+
+ Written with nofail, so if the drive is missing the machine still boots
+ normally. Your current /etc/fstab is backed up first.
+
`;
+
+ const close = () => modal.remove();
+ modal.addEventListener('click', (e) => { if (e.target === modal) close(); });
+ modal.querySelector('.setup-modal-close').addEventListener('click', close);
+ document.addEventListener('keydown', function onKey(e) {
+ if (e.key === 'Escape') { close(); document.removeEventListener('keydown', onKey); }
+ });
+
+ // Remember the fstab choice on the wizard, not in the DOM: the modal is
+ // destroyed on close and the choice has to survive to submit().
+ const fstabBox = modal.querySelector('[data-storage-fstab]');
+ if (fstabBox) {
+ fstabBox.addEventListener('change', () => {
+ const path = fstabBox.dataset.storageFstab;
+ this.fstabWanted = this.fstabWanted.filter(p => p !== path);
+ if (fstabBox.checked) {
+ this.fstabWanted.push(path);
+ // Adding it to fstab only makes sense if the drive is actually used.
+ const own = this.container.querySelector(`[data-storage-path="${CSS.escape(path)}"]`);
+ if (own && !own.disabled) own.checked = true;
+ }
+ });
+ }
+
+ this.container.appendChild(modal);
+ }
+
escapeHtml(s) {
return String(s == null ? '' : s).replace(/[&<>"']/g, (ch) => (
{ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[ch]
@@ -864,7 +1004,9 @@ class SetupWizard {
// Paths ticked on the Storage step. Registering one is a privileged
// action, so this is only a request — the host applier runs it through
// the CLI and the root helper, which validate independently.
- storage: this.collectStorage()
+ storage: this.collectStorage(),
+ // Only for drives that are actually being registered.
+ storage_fstab: this.fstabWanted.filter(p => this.collectStorage().includes(p))
};
// Apply the experience choice to the WebUI immediately so the next
diff --git a/docs/roadmap/storage-locations.md b/docs/roadmap/storage-locations.md
index 2a1980f..22afc21 100644
--- a/docs/roadmap/storage-locations.md
+++ b/docs/roadmap/storage-locations.md
@@ -1,6 +1,6 @@
# LibrePortal — Storage Locations (per-app data placement)
-**Status:** Phases 0-2, 4 and 5 **built**; the two WebUI surfaces (setup-wizard step, Disks page) are not. · **Audience:** us, future-self · **Scope:** register more than one filesystem root for live app data, choose one per app, move an app between them, and resolve the right one on restore/migrate · **Origin:** "add different locations to set up LibrePortal on, with control per app" (2026-08-24)
+**Status:** Phases 0-5 **built** (incl. the setup-wizard Storage step); the Disks WebUI page is not. · **Audience:** us, future-self · **Scope:** register more than one filesystem root for live app data, choose one per app, move an app between them, and resolve the right one on restore/migrate · **Origin:** "add different locations to set up LibrePortal on, with control per app" (2026-08-24)
---
@@ -15,7 +15,8 @@ Nextcloud's 4 TB of photos go on the spinning disk. Vaultwarden and the control
## 1. Non-goals
- ❌ Relocating the **system** root (configs/db/logs) per-app. It stays one place, chosen at install. Same for the WebUI's own container dir.
-- ❌ A general volume manager. We don't format, partition, mount, or write `/etc/fstab`. The drive must already be mounted; we validate and use it.
+- ❌ A general volume manager. We don't format, partition or mount anything — the drive must already be mounted; we validate and use it.
+- ⚠️ **One exception, added deliberately:** on explicit request we append a single marked `/etc/fstab` entry so a registered drive comes back after a reboot (§6.3). Telling a non-expert "add this line to fstab yourself" is a wall, and the most likely outcome is a reboot where nothing starts.
- ❌ Striping/tiering/RAID-alikes. One app's data lives on exactly one location. No splitting an app across two.
- ❌ Per-*volume* placement inside an app (`./data` here, `./db` there). Location granularity is the app directory. Revisit only if a real need shows up.
- ❌ Merging the backup-location and storage-location **registries**. They differ in trust, lifecycle and ownership, so they stay separate — but they may freely share a *drive*, which is a supported and expected setup (§6.2).
@@ -209,6 +210,46 @@ Worth noting the compounding case explicitly, because it's the one people don't
**Consequence for naming.** Sharing makes the collision worse, not better: the user now genuinely sees "bigdisk" in two places meaning two things. That's what the **Disks view** (§7.1) answers — one row per device, with *app data* and *backups* as roles on it rather than as competing top-level nouns. It's also where this section's shared-fate badge and per-device space accounting naturally live.
+### 6.3 — Writing `/etc/fstab`, and why that is allowed here
+
+§1 originally ruled this out, and reversing that deserves an argument rather
+than a shrug.
+
+The case for: the persistence warning (§6, check 6) is useless to the audience
+this product is for. "Add `UUID=… /mnt/disk ext4 defaults,nofail 0 2` to
+/etc/fstab" assumes SSH, root, an editor, and knowing what fstab is. Someone who
+registers a drive, doesn't act on the warning, and reboots gets apps that refuse
+to start — the exact failure we were trying to prevent, arrived at by a longer
+road. LibrePortal already writes sysctl drop-ins, `modules-load.d`, systemd
+units, sudoers and firewall rules; fstab is not a different category of file.
+
+The case against is real and specific: **a bad fstab entry can leave a machine
+unbootable**, needing rescue media. That is worse than any other failure mode in
+this product.
+
+What makes it defensible is `nofail`, plus `x-systemd.device-timeout=10s`.
+Together they mean a missing device can never block boot — which is precisely
+the failure the objection is about. Without that pair this would stay a non-goal.
+
+The rules, all enforced in the root helper (`libreportal-storage fstab-add`):
+
+| Rule | Why |
+|---|---|
+| `nofail,x-systemd.device-timeout=10s` always | a missing drive can't block boot or strand the box at a systemd timeout |
+| `UUID=`, never `/dev/sdX` | device names reorder between boots; a stale one mounts the wrong disk or nothing |
+| append inside a marked block, never rewrite | anything the user or another tool manages is untouched |
+| refuse if the target or UUID is already described | we don't get to be the second opinion on someone else's mount |
+| refuse the root filesystem outright | never our business |
+| must currently be a real mount | we describe reality, we don't invent it |
+| timestamped backup to `/etc/fstab.libreportal-*.bak` | recoverable |
+| `findmnt --verify` before install; discard on failure | a file that doesn't parse never reaches `/etc` |
+| opt-in only | nothing calls it unless a person ticked the box |
+
+Verified against a real filesystem: the entry is added and verifies, the
+persistence warning then disappears on the next scan, and a duplicate, the root
+filesystem, a non-mountpoint and a relative path are each refused with the
+reason.
+
## 7. Surfaces — first-run wizard, config panels, CLI
**First-run wizard.** The setup wizard (`core/setup/js/setup-wizard.js`) currently runs `Experience → Identity → Domains → Recommended → Metrics`, and already has the pattern for a step that isn't always shown: Metrics is advanced-only, and `_effectiveTotalSteps()` makes the count dynamic. A **Storage** step slots in **before Recommended** — locations must exist before apps get placed on them — and follows the same conditional rule, with a better trigger:
@@ -401,7 +442,7 @@ Each phase is independently shippable and independently verifiable. Phase 0 carr
| **0** ✅ | `appDir` / `storageRoots` / `pathIsContainerData` / `webuiDir`; ~260 call sites swept. `scripts/dev/lp-storage-test`. | No |
| **1** ✅ | Root-owned registry + `libreportal-storage` + fitness checks + `libreportal storage {list,add,remove,check,scan,apps,disks}`. | CLI only |
| **2** ✅ | `CFG__STORAGE` in 37 templates + the `**READONLY**` marker + resolved path in the comment (§5.2) + per-location config panel emit. | Yes |
-| **3** ⬜ | Setup-wizard **Storage** step (§7) + `--storage-dir=`. **Not built.** The headless path it would drive already works, so this is UI on top of a working feature, not a prerequisite. | Yes |
+| **3** ✅ | Setup-wizard **Storage** step (§7), with a details modal and the opt-in `/etc/fstab` offer (§6.3). `--storage-dir=` for unattended installs is still open. | Yes |
| **4** ✅ | `libreportal app move` — stop, snapshot, copy as root, verify, then delete the source. | Yes |
| **5** ◐ | Manifest `storage` block and the staged restore + path rewrite are done (§9's latent bug is fixed). The "unknown location" **prompt** is not — an unresolvable location currently falls back to this host's default rather than asking. | Yes |
| **6** ◐ | The Disks **data layer** is built and `libreportal storage disks` renders it (including not-attached rows). The WebUI page at `/admin/system/storage` and the periodic `storage check` cron are **not**. | Yes |
diff --git a/scripts/cli/commands/storage/cli_storage_commands.sh b/scripts/cli/commands/storage/cli_storage_commands.sh
index 68a4d97..15a6cd8 100644
--- a/scripts/cli/commands/storage/cli_storage_commands.sh
+++ b/scripts/cli/commands/storage/cli_storage_commands.sh
@@ -102,6 +102,23 @@ cliHandleStorageCommands()
storageDisks
;;
+ fstab-add)
+ # Make a mount permanent. The root helper does the writing and all
+ # the validating; this is only the front door.
+ if [[ -z "$arg1" ]]; then
+ isNotice "Usage: storage fstab-add "
+ return 1
+ fi
+ local line
+ if line=$(runStorage fstab-add "$arg1" 2>&1); then
+ isSuccessful "Added to /etc/fstab: $line"
+ isNotice "A backup of the previous file is in /etc/fstab.libreportal-*.bak"
+ else
+ isError "$line"
+ return 1
+ fi
+ ;;
+
*)
isNotice "Invalid storage action: $action"
cliShowStorageHelp
diff --git a/scripts/cli/commands/storage/cli_storage_header.sh b/scripts/cli/commands/storage/cli_storage_header.sh
index ad7f720..f5ea7ee 100644
--- a/scripts/cli/commands/storage/cli_storage_header.sh
+++ b/scripts/cli/commands/storage/cli_storage_header.sh
@@ -25,6 +25,11 @@ cliShowStorageHelp()
echo "storage apps [id]"
echo " Which apps live on a location."
echo ""
+ echo "storage fstab-add "
+ echo " Make a mounted drive permanent by adding one line to /etc/fstab, so"
+ echo " it comes back after a reboot. Written with nofail, so a missing drive"
+ echo " can never block boot; the previous file is backed up first."
+ echo ""
echo "storage disks"
echo " One row per filesystem, showing what LibrePortal uses it for."
echo ""
diff --git a/scripts/setup/setup_apply.sh b/scripts/setup/setup_apply.sh
index 6d84fc6..d36a8f6 100644
--- a/scripts/setup/setup_apply.sh
+++ b/scripts/setup/setup_apply.sh
@@ -25,6 +25,7 @@ setupApplyConfig()
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 // []')
if [[ -n "$install_name" ]]; then
updateConfigOption "CFG_INSTALL_NAME" "$install_name"
@@ -68,6 +69,17 @@ setupApplyConfig()
[[ -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
diff --git a/scripts/source/files/arrays/function_manifest.sh b/scripts/source/files/arrays/function_manifest.sh
index 2018055..5a29078 100644
--- a/scripts/source/files/arrays/function_manifest.sh
+++ b/scripts/source/files/arrays/function_manifest.sh
@@ -1020,6 +1020,7 @@ declare -gA LP_FN_MAP=(
[storageDisksData]="storage/storage_disks.sh"
[_storageEmit]="storage/storage_checks.sh"
[_storageFreeHuman]="storage/storage_locations.sh"
+ [storageFstabLine]="storage/storage_checks.sh"
[storageList]="storage/storage_locations.sh"
[storageLocationConfig]="storage/storage_locations.sh"
[storageLocationDir]="storage/storage_locations.sh"
@@ -2243,6 +2244,7 @@ declare -gA LP_FN_ROOT=(
[storageDisksData]="scripts"
[_storageEmit]="scripts"
[_storageFreeHuman]="scripts"
+ [storageFstabLine]="scripts"
[storageList]="scripts"
[storageLocationConfig]="scripts"
[storageLocationDir]="scripts"
@@ -3503,6 +3505,7 @@ storageDisks() { unset -f storageDisks; __lpAutoload "${install_scripts_dir}stor
storageDisksData() { unset -f storageDisksData; __lpAutoload "${install_scripts_dir}storage/storage_disks.sh"; storageDisksData "$@"; }
_storageEmit() { unset -f _storageEmit; __lpAutoload "${install_scripts_dir}storage/storage_checks.sh"; _storageEmit "$@"; }
_storageFreeHuman() { unset -f _storageFreeHuman; __lpAutoload "${install_scripts_dir}storage/storage_locations.sh"; _storageFreeHuman "$@"; }
+storageFstabLine() { unset -f storageFstabLine; __lpAutoload "${install_scripts_dir}storage/storage_checks.sh"; storageFstabLine "$@"; }
storageList() { unset -f storageList; __lpAutoload "${install_scripts_dir}storage/storage_locations.sh"; storageList "$@"; }
storageLocationConfig() { unset -f storageLocationConfig; __lpAutoload "${install_scripts_dir}storage/storage_locations.sh"; storageLocationConfig "$@"; }
storageLocationDir() { unset -f storageLocationDir; __lpAutoload "${install_scripts_dir}storage/storage_locations.sh"; storageLocationDir "$@"; }
diff --git a/scripts/storage/storage_checks.sh b/scripts/storage/storage_checks.sh
index f97168a..8708129 100644
--- a/scripts/storage/storage_checks.sh
+++ b/scripts/storage/storage_checks.sh
@@ -114,14 +114,38 @@ _storageCheckPersistence()
return 0
fi
- local src fstype
+ _storageEmit warn persistence "This drive is not listed in /etc/fstab, so the system will not mount it automatically after a reboot. Apps stored here will refuse to start until it is mounted again."
+ # The exact line, as its own record: the UI offers to add it, and a person
+ # reading the CLI can copy it. Kept separate from the prose so neither has
+ # to be parsed out of the other.
+ local line; line=$(storageFstabLine "$probe")
+ [[ -n "$line" ]] && _storageEmit info fstab-line "$line"
+ return 0
+}
+
+# The /etc/fstab line that would make this mount permanent.
+#
+# UUID= rather than /dev/sdX because device names reorder across reboots, and a
+# stale one either mounts the wrong disk or nothing at all.
+#
+# nofail + x-systemd.device-timeout are what make this safe to add at all: a
+# missing device then cannot block boot. Without them an absent drive leaves the
+# machine sitting at a systemd timeout, or worse in emergency mode — a far worse
+# failure than the app-not-starting one we are trying to prevent.
+storageFstabLine()
+{
+ local probe="$1" target src fstype uuid
+ command -v findmnt >/dev/null 2>&1 || return 1
+ target=$(findmnt -no TARGET --target "$probe" 2>/dev/null | tail -1)
+ [[ -z "$target" || "$target" == "/" ]] && return 1
src=$(findmnt -no SOURCE --target "$probe" 2>/dev/null | tail -1)
fstype=$(findmnt -no FSTYPE --target "$probe" 2>/dev/null | tail -1)
- local uuid; uuid=$(findmnt -no UUID --target "$probe" 2>/dev/null | tail -1)
- local line="${src:-} $target ${fstype:-auto} defaults,nofail 0 2"
- [[ -n "$uuid" ]] && line="UUID=$uuid $target ${fstype:-auto} defaults,nofail 0 2"
- _storageEmit warn persistence "Not in /etc/fstab: after a reboot this drive will not be mounted, and apps stored here will not start until it is. To make it permanent, add: $line"
- return 0
+ uuid=$(findmnt -no UUID --target "$probe" 2>/dev/null | tail -1)
+ local dev="${src:-}"
+ [[ -n "$uuid" ]] && dev="UUID=$uuid"
+ [[ -z "$dev" ]] && return 1
+ printf '%s %s %s defaults,nofail,x-systemd.device-timeout=10s 0 2' \
+ "$dev" "$target" "${fstype:-auto}"
}
# 7. Removable / hot-plug. WARNS — an external drive is a supported setup.
diff --git a/scripts/system/libreportal-storage b/scripts/system/libreportal-storage
index 349d538..ab2062c 100755
--- a/scripts/system/libreportal-storage
+++ b/scripts/system/libreportal-storage
@@ -216,6 +216,103 @@ probe() {
return $rc
}
+# Make a mount permanent by adding one line to /etc/fstab.
+#
+# fstab is the most dangerous file LibrePortal touches: a bad entry does not
+# break an app, it can leave the machine unbootable and needing rescue media.
+# Everything below exists to make that impossible:
+#
+# * nofail + x-systemd.device-timeout=10s — a missing device can then never
+# block boot. This single pair is what makes writing fstab defensible at
+# all; without it an unplugged drive strands the box at a systemd timeout
+# or drops it to emergency mode.
+# * UUID=, never /dev/sdX — device names reorder between boots.
+# * append only, inside a marked block. Existing lines are never rewritten,
+# so anything the user or another tool manages is untouched.
+# * refuse when a line for that target or device already exists — we do not
+# get to be the second opinion on a mount someone else configured.
+# * a timestamped backup, and `findmnt --verify` before the new file is put
+# in place. A file that does not verify is discarded, not installed.
+#
+# Opt-in only: nothing calls this unless a person asked for it.
+fstab_add() {
+ local target="${1:-}"
+ [[ -n "$target" && "$target" == /* ]] \
+ || { _err "fstab-add requires an absolute mount point"; return 2; }
+ target="${target%/}"
+ [[ "$target" == *..* ]] && { _err "invalid mount point"; return 2; }
+
+ command -v findmnt >/dev/null 2>&1 || { _err "findmnt unavailable"; return 1; }
+
+ # Refuse the root filesystem before anything else, so the reason given is the
+ # real one rather than a confusing "not a mount point".
+ [[ "$target" == "/" || -z "$target" ]] \
+ && { _err "refusing to touch the root filesystem's fstab entry"; return 1; }
+
+ # Must currently be a real mount — we describe reality, we do not invent it.
+ local now_target
+ now_target=$(findmnt -no TARGET --target "$target" 2>/dev/null | tail -1)
+ if [[ "$now_target" != "$target" ]]; then
+ _err "'$target' is not a mount point right now. Mount it first, then add it."
+ return 1
+ fi
+
+ local uuid fstype
+ uuid=$(findmnt -no UUID --target "$target" 2>/dev/null | tail -1)
+ fstype=$(findmnt -no FSTYPE --target "$target" 2>/dev/null | tail -1)
+ if [[ -z "$uuid" ]]; then
+ _err "'$target' has no filesystem UUID, so no stable fstab entry can be written for it."
+ return 1
+ fi
+ [[ -z "$fstype" ]] && fstype="auto"
+
+ # Already described? Leave it alone — do not add a second opinion.
+ local existing
+ existing=$(grep -vE '^[[:space:]]*#' /etc/fstab 2>/dev/null \
+ | awk -v t="$target" -v u="UUID=$uuid" '$2==t || $1==u {print; exit}')
+ if [[ -n "$existing" ]]; then
+ _err "/etc/fstab already has an entry for this mount: $existing"
+ return 1
+ fi
+
+ local line="UUID=$uuid $target $fstype defaults,nofail,x-systemd.device-timeout=10s 0 2"
+
+ local stamp backup tmp
+ stamp=$(date +%Y%m%d-%H%M%S)
+ backup="/etc/fstab.libreportal-$stamp.bak"
+ cp -a /etc/fstab "$backup" || { _err "could not back up /etc/fstab"; return 1; }
+
+ tmp=$(mktemp) || return 1
+ cat /etc/fstab > "$tmp"
+ # Guarantee the file ends with a newline before appending, or the new entry
+ # would be glued onto whatever the last line was.
+ [[ -s "$tmp" && -n "$(tail -c1 "$tmp")" ]] && printf '\n' >> "$tmp"
+ {
+ printf '\n# Added by LibrePortal (%s) — storage location %s\n' "$stamp" "$target"
+ printf '# nofail: a missing device must never block boot.\n'
+ printf '%s\n' "$line"
+ } >> "$tmp"
+
+ # Verify BEFORE installing. findmnt --verify parses fstab and reports
+ # structural problems; a file that does not pass is thrown away.
+ if ! findmnt --verify --tab-file "$tmp" >/dev/null 2>&1; then
+ local why; why=$(findmnt --verify --tab-file "$tmp" 2>&1 | head -5)
+ rm -f "$tmp"
+ _err "the resulting /etc/fstab did not verify, so it was NOT installed: $why"
+ return 1
+ fi
+
+ install -m 0644 -o root -g root "$tmp" /etc/fstab || { rm -f "$tmp"; return 1; }
+ rm -f "$tmp"
+
+ # Let systemd pick up the new unit now, so the entry is live rather than
+ # only true after the next boot.
+ systemctl daemon-reload >/dev/null 2>&1 || true
+
+ echo "$line"
+ return 0
+}
+
add() {
local raw="" name="" allow_home=0 a
for a in "$@"; do
@@ -384,9 +481,10 @@ action="${1:-}"; shift 2>/dev/null || true
case "$action" in
add) add "$@" ;;
probe) probe "${1:-}" "${2:-}" ;;
+ fstab-add) fstab_add "${1:-}" ;;
remove) remove "${1:-}" ;;
list) list ;;
path) path "${1:-}" ;;
verify) verify "${1:-}" ;;
- *) echo "usage: libreportal-storage {add [--name=NAME] [--allow-home]|probe [--allow-home]|remove |list|path |verify [id]}" >&2; exit 2 ;;
+ *) echo "usage: libreportal-storage {add [--name=NAME] [--allow-home]|probe [--allow-home]|fstab-add |remove |list|path |verify [id]}" >&2; exit 2 ;;
esac
diff --git a/scripts/webui/data/generators/system/webui_storage_candidates.sh b/scripts/webui/data/generators/system/webui_storage_candidates.sh
index 7074393..d2d9bdf 100644
--- a/scripts/webui/data/generators/system/webui_storage_candidates.sh
+++ b/scripts/webui/data/generators/system/webui_storage_candidates.sh
@@ -42,17 +42,26 @@ webuiGenerateStorageCandidates()
# Only offer filesystems LibrePortal isn't already using.
[[ "$role" == "free" ]] || continue
- local sev check msg refusals="" warnings=""
+ # One record per check rather than two joined strings: the card shows a
+ # short summary, the details modal shows the full text, and neither has
+ # to parse anything back out of the other.
+ local sev check msg checks="[" kfirst=1 verdict="ok" fstab_line=""
while IFS=$'\t' read -r sev check msg; do
- case "$sev" in
- refuse) refusals+="${refusals:+; }$msg" ;;
- warn) warnings+="${warnings:+; }$msg" ;;
- esac
+ [[ -z "$sev" ]] && continue
+ if [[ "$check" == "fstab-line" ]]; then
+ fstab_line="$msg"
+ continue
+ fi
+ [[ "$sev" == "warn" && "$verdict" == "ok" ]] && verdict="warn"
+ [[ "$sev" == "refuse" ]] && verdict="refuse"
+ (( kfirst )) || checks+=","
+ kfirst=0
+ checks+="{\"severity\":\"$(_lpJsonEsc "$sev")\",\"id\":\"$(_lpJsonEsc "$check")\",\"message\":\"$(_lpJsonEsc "$msg")\"}"
done < <(storageCheckPath "$target" 2>/dev/null)
+ checks+="]"
- local verdict="ok"
- [[ -n "$warnings" ]] && verdict="warn"
- [[ -n "$refusals" ]] && verdict="refuse"
+ local uuid_val; uuid_val=$(findmnt -no UUID --target "$target" 2>/dev/null | tail -1)
+ local opts_val; opts_val=$(findmnt -no OPTIONS --target "$target" 2>/dev/null | tail -1)
(( cfirst )) || candidates+=","
cfirst=0
@@ -61,10 +70,12 @@ webuiGenerateStorageCandidates()
candidates+=",\"fstype\":\"$(_lpJsonEsc "$fstype")\""
candidates+=",\"size\":\"$(_lpJsonEsc "$size")\""
candidates+=",\"free\":\"$(_lpJsonEsc "$avail")\""
+ candidates+=",\"uuid\":\"$(_lpJsonEsc "$uuid_val")\""
+ candidates+=",\"options\":\"$(_lpJsonEsc "$opts_val")\""
candidates+=",\"removable\":$([[ "$rm_flag" == "1" ]] && echo true || echo false)"
candidates+=",\"verdict\":\"$verdict\""
- candidates+=",\"refusals\":\"$(_lpJsonEsc "$refusals")\""
- candidates+=",\"warnings\":\"$(_lpJsonEsc "$warnings")\"}"
+ candidates+=",\"fstab_line\":\"$(_lpJsonEsc "$fstab_line")\""
+ candidates+=",\"checks\":$checks}"
done < <(storageScanCandidates 2>/dev/null)
candidates+="]"