docs(roadmap): the Disks view — one row per filesystem

Resolves open question 1. The device becomes the organising concept and
the two registries become roles on it, so a user with one drive holding
both app data and snapshots stops seeing "bigdisk" twice meaning two
different things.

It extends a page that already exists rather than adding one:
/admin/system/storage is currently the Docker disk breakdown, and
webui_system_metrics.sh already builds a disks array from df. The view is
that array enriched with lsblk -J -e7 (model, transport, rotational,
removable — with -e7 dropping the ~15 snap loop devices a desktop box
carries) and joined against both registries.

The union is the part that matters more than the enrichment: a registered
drive that is currently unplugged does not appear in lsblk at all, and
that is exactly when someone opens this page. So rows come from the
registry first and attached hardware second — a missing device still
renders, marked not attached, naming the apps stranded on it. Rows key on
fs_uuid, since /dev/sdb1 reorders across reboots and would scramble the
table.

This also gives every standing badge from §6/§6.1/§6.2 a natural home,
because each is a property of the device rather than of either registry,
and it turns "type the same path into two different pages" into actions
on the row you are already looking at.

Two limitations written down rather than discovered later: ZFS datasets
report distinct st_dev but share a pool, so grouping by device
over-reports free space (btrfs subvolumes are fine); and the page stays
read-only about the system — it can show the fstab line to add, never
add it.

Phase 6 now builds this instead of a separate Storage page, which with
disks as the frame would have reintroduced the split this resolves.
Also folds in §10.5 — the dashboard gauge becomes a summary linking here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
librelad 2026-08-24 03:54:22 +01:00
parent 5491455290
commit 48c78270a9

View File

@ -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/<id>/location.config` holding `CFG_STORAGE_LOC_<id>_*` (`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_<APP>_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 15 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)?