docs(roadmap): drop STORAGE_PINNED, record the name-vs-path decision

Pinning an app to the primary location isn't a fact about its storage,
it's a statement about whether the field is editable — so it belongs in
the comment, where the renderer's other display metadata already lives.
webui_generate_configs.sh already parses **ADVANCED** out of a field's
comment; **READONLY** parses the same way and needs no new key, no
engine-side list, and no special case in appDir or the migrate path. It
also has uses beyond this feature: crowdsec.config:72 warns that editing
a field does nothing, next to an editable input.

Also records why CFG_<APP>_STORAGE holds a location name rather than a
path, since it's the kind of decision that gets relitigated: mount paths
move (relabel, /media/<user>/…, a different session) and a name means
re-registering once instead of rewriting every app config; and migrate's
central question — "this host has no such location, where should it go?"
— is a registry lookup, not a string comparison. The config stores intent,
the manifest stores fact. Unlike backup locations there's no case for a
custom-path override: root only touches registered roots, so a path
outside the registry is unusable rather than merely discouraged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
librelad 2026-08-24 03:14:04 +01:00
parent 8ef04ef5ad
commit 786d8ebfe8

View File

@ -116,6 +116,17 @@ CFG_BOOKSTACK_STORAGE=default
- `configBackfillMissingKeys` carries the new key into already-installed apps on the next update, so nothing needs a reinstall. - `configBackfillMissingKeys` carries the new key into already-installed apps on the next update, so nothing needs a reinstall.
- **New compose tag** `#LIBREPORTAL|APP_DIR_TAG|APP_DIR_DATA`, filled in `dockerConfigSetupFileWithData` alongside the existing `CONTAINERS_DIR_TAG`, for the rare template that genuinely needs its own absolute host path. Most apps need nothing — relative volumes already do the right thing. - **New compose tag** `#LIBREPORTAL|APP_DIR_TAG|APP_DIR_DATA`, filled in `dockerConfigSetupFileWithData` alongside the existing `CONTAINERS_DIR_TAG`, for the rare template that genuinely needs its own absolute host path. Most apps need nothing — relative volumes already do the right thing.
### 5.1 — Why the value is a name and not `CFG_<APP>_STORAGE_PATH`
A path reads better in isolation — `grep STORAGE *.config` would tell you where everything lives without consulting anything. It loses on the two things that actually happen:
- **Mount paths move.** External drives arrive at `/media/<user>/<label>` or `/run/media/…`, and those change on relabel, on a different desktop session, on a distro that mounts differently. With a name, re-register the location's path once and every app on it follows. With a path, every app config has to be rewritten — N edits, each able to fail halfway, and a half-finished pass leaves some apps agreeing with `appDir` and some not.
- **Migrate is a lookup, not a string match.** This feature's motivating case is "the restored config names a location this host doesn't have — ask the user." A name makes that a clean miss against the registry. A path makes it a comparison against a string that never meant anything on this host, and it can't distinguish "different disk layout" from "typo".
Debuggability doesn't require the path be *stored*, only *retrievable*: `libreportal storage list`, `libreportal app info <app>`, and the resolved directory shown beside the field on the config page (above) all print it. The split that keeps this honest — **the config stores the name because it is intent; the manifest (§7) stores name *and* path *and* `fs_uuid` because it is a record of fact.**
Note this deliberately diverges from the backup-location template, which offers `PATH_MODE=auto|custom` + an explicit `PATH`. There, restic runs as an ordinary user and can write anywhere it has permission, so a custom path is genuinely useful. Here, an app directory needs ownership that only root can establish, and root only touches registered roots (§3) — so a path outside the registry isn't discouraged, it's **unusable**. There is nothing for an override to override.
## 6. Moving an installed app ## 6. Moving an installed app
`libreportal app move <app> <location>` — a task like any other, with progress rows in the WebUI. `libreportal app move <app> <location>` — a task like any other, with progress rows in the WebUI.
@ -174,7 +185,15 @@ Where it's enforced:
**8.3 — Cross-app literal paths.** Traefik, Prometheus, Grafana, AdGuard, Gluetun, CrowdSec, Headscale are reached into by name from other apps' code. They're also small and infrastructural, so pinning them to the primary location costs a user nothing real. **8.3 — Cross-app literal paths.** Traefik, Prometheus, Grafana, AdGuard, Gluetun, CrowdSec, Headscale are reached into by name from other apps' code. They're also small and infrastructural, so pinning them to the primary location costs a user nothing real.
Do it as a **declared property of the app, not a hardcoded list in the engine**: `CFG_<APP>_STORAGE_PINNED=true` in those templates, exactly mirroring how `CFG_<APP>_MULTI_INSTANCE` already gates instancing. The engine asks the config; the WebUI renders the field disabled with the reason; relaxing an app later is a one-line template change plus its call sites, with no core edit. The WebUI's own dir is pinned permanently and structurally (`webuiDir`), not by this flag. Express it with **no new key at all.** "Pinned" is not a fact about the app's storage, it's a statement about whether the field is editable — and the config format already has a place for that: the comment. `**ADVANCED**` is parsed out of a field's comment today (`webui_generate_configs.sh:326`) to drive the editor's Advanced reveal. Add `**READONLY**` beside it, parsed the same way, and these templates simply ship:
```
CFG_TRAEFIK_STORAGE=default # Storage Location - Fixed: other apps reference Traefik by path **READONLY**
```
The value stays a normal `CFG_<APP>_STORAGE`, so `appDir` and the migrate path need no special case; only the renderer treats it differently. Relaxing an app later is deleting one token from a comment plus converting its call sites. And `**READONLY**` immediately earns its keep elsewhere: derived fields already warn in prose that nothing enforces — `crowdsec.config:72` literally says *"editing this value does not re-register the bouncer"* next to an editable input.
The WebUI's own dir stays pinned permanently and *structurally* (`webuiDir`), not by any config marker.
**8.4 — Filesystem capability.** exFAT/NTFS/vfat can't hold POSIX ownership, so a rootless app dir on one is broken from the start — worse than for backups, where it's only a warning. For storage locations this must be a **refusal**, not a notice. Also check `noexec`/`nosuid` mount flags (some apps execute out of their data dir) and that the filesystem supports the sub-UID range rootless docker maps into. **8.4 — Filesystem capability.** exFAT/NTFS/vfat can't hold POSIX ownership, so a rootless app dir on one is broken from the start — worse than for backups, where it's only a warning. For storage locations this must be a **refusal**, not a notice. Also check `noexec`/`nosuid` mount flags (some apps execute out of their data dir) and that the filesystem supports the sub-UID range rootless docker maps into.