diff --git a/docs/roadmap/storage-locations.md b/docs/roadmap/storage-locations.md index ae26de4..e2eed17 100644 --- a/docs/roadmap/storage-locations.md +++ b/docs/roadmap/storage-locations.md @@ -176,7 +176,7 @@ The reason we can afford it: **the dangerous moment is start-up, not registratio So the obligation moves from *prevent* to *inform*, and "loud" has to mean **durable**, not a toast that scrolls away: - an explicit warning on the wizard/CLI card at registration, stating the consequence in plain words — *"apps stored here will not start until this drive is mounted"* — plus the exact `fstab` line (or `systemd.mount` unit) that would make it permanent, ready to copy -- a **standing badge** on the location in the WebUI Storage page and in `storage list`, for as long as the condition holds. Six months later, when an app won't start, the reason should be visible without archaeology +- a **standing badge** on the device's row in the Disks view (§7.1) and in `storage list`, for as long as the condition holds. Six months later, when an app won't start, the reason should be visible without archaeology - the resulting start-up refusal must name the cause: *"nextcloud is on location `bigdisk`, which is not mounted"* — never a generic failure - `storage check` re-reports it on its periodic run, so a drive that *was* in `fstab` and silently stopped being one gets caught @@ -207,7 +207,7 @@ Worth noting the compounding case explicitly, because it's the one people don't - the picker on either side can **offer known drives** as suggestions, with the shared-fate note inline, instead of making the user type a path twice - the storage marker (§3.1) is a strictly better mount test than the backup subsystem's `findmnt` probe — it catches a mountpoint shadowed by a stale directory, which `findmnt` reports as mounted. A backup location on a drive that also hosts a storage location can borrow it. -**Consequence for §13.1 (naming).** Sharing makes the collision worse, not better: the user now genuinely sees "bigdisk" in two places meaning two things. The natural resolution is to stop treating the two registries as the top-level concept in the UI and introduce a **Disks** view — one row per device, showing which roles LibrePortal has on it (*app data*, *backups*, or both), free space for the device as a whole, and mount state. The registries stay separate underneath; the user just stops having to hold that distinction to understand their own hardware. Recommended, but a bigger UI call than this doc should make alone. +**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. ## 7. Surfaces — first-run wizard, config panels, CLI @@ -234,7 +234,38 @@ For unattended installs the wizard isn't there, so keep an `init.sh --storage-di - **Per-location config** — `configs/storage/locations//location.config` holding `CFG_STORAGE_LOC__*` (`NAME`, `ENABLED`, `NOTES`, `REQUIRE_MOUNT`…). `webui_generate_configs.sh` already has a bespoke loop for exactly this shape for backup locations (`configs/backup/locations/*/location.config`, which the flat-file scan can't reach at that depth) — the storage loop is a near-copy, and the WebUI *Storage* page renders from config metadata like every other page. Note the **path is not among these fields**: it's registry data, root-owned, and changing it is `storage add`/`remove`, not a config edit. - **Per-app field** — the `CFG__STORAGE` dropdown described in §5, on the app's existing config page. -**CLI.** `libreportal storage {list,add,remove,check,scan}` mirroring `libreportal backup location …` — `scan` being the candidate detection the wizard uses, so a headless user gets the same view. +**CLI.** `libreportal storage {list,add,remove,check,scan}` mirroring `libreportal backup location …` — `scan` being the candidate detection the wizard uses, so a headless user gets the same view. Plus `libreportal storage disks`, the CLI rendering of §7.1. + +### 7.1 — The Disks view + +**One row per filesystem, showing what LibrePortal does with it.** This is the answer to the naming collision (§13.1) and to shared drives (§6.2): the user stops holding "storage location" and "backup location" as two competing top-level nouns, and instead sees their actual hardware with *roles* on it. + +**It extends a page that already exists.** `/admin/system/storage` is currently the "Docker disk breakdown" (`system-page.js:9`), and `webui_system_metrics.sh:80` already builds a `disks` array from `df -PB1` with tmpfs/devtmpfs/squashfs/overlay/aufs excluded. The Disks view is that array enriched and joined, not a new page bolted on. The dashboard's existing gauge stays as a summary that links here — which is also how §10.5 gets fixed properly rather than by adding a second misleading number. + +**Three sources, unioned:** + +| Source | Contributes | +|---|---| +| `lsblk -J -e7 -o NAME,UUID,PARTUUID,MODEL,TRAN,ROTA,RM,HOTPLUG,FSTYPE,SIZE,FSAVAIL,MOUNTPOINT` | hardware identity — model, USB vs NVMe, spinning vs solid, removable. `-e7` drops the loop devices (~15 snap mounts on a desktop box) | +| `df` — already generated | live capacity per mounted filesystem | +| both registries | which roles LibrePortal has claimed, and on what | + +**The union matters more than the enrichment.** A registered drive that is currently unplugged does not appear in `lsblk` at all — and that is precisely the moment the user opens this page. So rows come from *the registry first*, attached hardware second: + +> **A registered location whose device is missing must still render**, marked *not attached*, with the apps or backup repos that are stranded on it named. A row vanishing when the disk is pulled is the one failure this page cannot have. + +**Identity is the `fs_uuid`**, not `/dev/sdb1` — device names reorder across reboots and would scramble the table. Fall back to `PARTUUID`, then the mountpoint, and say so in the row when identity is weak. + +**Per row:** model and transport, size, filesystem, mountpoint, device-wide free space, mount state, and the **roles** — *System*, *App data (N apps)*, *Backups (N locations)*, or *Unused*. Then the standing badges from §6/§6.1/§6.2: transient (not in `fstab`), removable, shared fate, low space, unsupported filesystem. This is the natural home for all of them, because every one is a property of the device rather than of either registry. + +**Actions on the row** are what make it a view rather than a report — register as app storage, add a backup location here, move apps here, unregister, re-run checks. That collapses "type the same path into two different pages" into one place, and the picker suggestions from §6.2 become unnecessary because the disk is already the thing you're looking at. + +**Generator:** `scripts/webui/data/generators/system/webui_disks.sh` → `data/system/disks.json`, following the existing generator convention. Unused candidates are included (that's the §7 wizard's `storage scan`, same implementation) so the page answers "what else could I use?" as well as "what am I using?". + +**Two honest limitations**, worth writing down rather than discovering later: + +- **ZFS datasets report distinct `st_dev` values but share one pool**, so grouping by device over-reports available space — each dataset appears to have the pool's free space to itself. Btrfs subvolumes share a device and are fine. Detect ZFS and group by pool, or state the caveat in the row; don't silently under-count. +- **This stays read-only about the system.** No formatting, partitioning, mounting, or `fstab` writing (§1). The page can show the exact line to add and let the user copy it; it does not add it. ## 8. Moving an installed app @@ -308,7 +339,7 @@ The WebUI's own dir stays pinned permanently and *structurally* (`webuiDir`), no **10.4 — Filesystem capability.** Superseded by the fitness checks (§6), which is where this risk got its answer: checks 1–5 turn "hope the filesystem is suitable" into a refusal with a stated reason, and check 3 (a real chown probe) is what catches the cases type-sniffing alone misses. -**10.5 — `du`/`df` reporting.** The dashboard's disk gauge reads `/` only. With apps spread over disks it needs one gauge per location, or the number is actively misleading. +**10.5 — `du`/`df` reporting.** The dashboard's disk gauge reads `/` only, which becomes actively misleading once apps are spread across disks. Answered by the Disks view (§7.1): the gauge stays as a summary that links there, rather than being duplicated into a second number that's wrong in a different way. **10.6 — Uninstall/teardown.** `init.sh uninstall` prints and removes a fixed set of paths. It must enumerate the registry, and default to **leaving** external locations alone (with an explicit `--remove-storage` to wipe them) — an external drive is not ours to erase. @@ -335,11 +366,11 @@ Each phase is independently shippable and independently verifiable. Phase 0 carr | **3** | Setup-wizard **Storage** step (§7) + `--storage-dir=` for unattended installs. Depends on 1 and 2; deliberately after them, so first-run drives a path that already works headlessly. | Yes | | **4** | `libreportal app move`. | Yes | | **5** | Manifest `storage` block, staged restore + path rewrite (fixes §9's latent bug), the "unknown location" prompt in migrate. | Yes | -| **6** | WebUI **Storage** page mirroring *Backup → Locations*: add/remove, mount state, free space, apps per location, drag-to-move. Periodic `storage check` + dashboard degradation warnings. | Yes | +| **6** | **Disks view** (§7.1) — extend `/admin/system/storage` into one row per filesystem with roles, badges and actions; `libreportal storage disks`. Periodic `storage check` + dashboard degradation warnings. Replaces the separate *Storage* page this phase used to propose: with disks as the frame, a second locations page would reintroduce exactly the split §13.1 resolved. | Yes | ## 13. Open questions -1. **Naming.** "Storage location" vs "backup location" in the same UI — is that confusing enough to want a different word (*volume*? *disk*? *pool*?) for one of them? +1. ~~**Naming.** "Storage location" vs "backup location" in the same UI~~ — **resolved (2026-08-24):** build the Disks view (§7.1). The device becomes the organising concept and the two registries become *roles* on it, so the user never has to hold the distinction to understand their own hardware. Registries stay separate underneath. 2. ~~**Can a storage location double as a backup location?**~~ — **resolved (2026-08-24):** yes, sharing a drive is supported. Registries stay separate, sibling directories only (never nested), with a durable shared-fate warning and per-device space accounting. See §6.2. 3. **Does the `libreportal` app itself ever get to move?** Currently pinned. If the primary root fills up, that's a reinstall — acceptable, or worth solving? 4. **Per-instance vs per-type placement.** Instances inherit the model for free, but should `instance create` offer a location up front (likely yes — "work Nextcloud on the big disk, family on the SSD" is a good demo)?