A registered drive that is unplugged rendered through the same path as any
other candidate — a "needs care" badge, "free of" with no numbers on either
side, an empty meter. To a first-time installer that reads as two broken disks
the scan turned up, with nothing tying the card back to a drive they registered
and later unplugged. Say "not connected", name the path, and draw no meter: a
meter with nothing in it is a claim about free space nobody measured. The same
locations are withheld from the dropdowns, since the wizard cannot stat a
directory on a drive that is absent.
Both dropdowns now end in "Custom path…", for a NAS mount or an LVM volume the
disk heuristics never rank as a candidate. Validation goes through
validateStep(3) rather than a disabled button: the apply side already refuses a
relative or system path, but its refusal is to fall back to the system disk,
and that is indistinguishable from having chosen the system disk on purpose.
A typed path is not a registered location, so setup_apply registers it via
storageAdd — which is what keeps the empty-directory admission rule and the
fitness checks in play — named after its basename, so it reads as "nas" rather
than "location-3" in the placement menus.
libreportal-storage: accept the name the listing prints. remove matched id and
path only, so `remove location-3` failed against a row displayed as
location-3. Root-owned helper changed, so footprint_version 10 -> 11.
Expose window.setupWizard: the instance was local to a promise in the
orchestrator and unreachable from the console or a test.
lp-storage-custom-test drives the step in a browser. Two holes it found in the
tests themselves, both the shape it exists to catch — a check whose failure
mode is to not run:
- It counted the cards that say "not connected" and asserted over those.
Turn the feature off and the count is zero, every() over an empty list is
true, and the block passed having checked nothing. The expectation now
comes from the feed.
- Both browser tests exited 0 whenever the page returned nothing. Under sudo,
where chromium will not start, they reported PASS having asserted nothing.
They now probe with `lp-shot --url` and curl: if the WebUI answers HTTP the
browser is the only thing that can have broken, and that is a failure.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Reported after looking at the step: the add button unstyled, the dialog missing
the fields a backup location actually has, and its dropdowns not working. Three
real faults, and one reason all three shipped.
* "+ Add destination" carried class .setup-add-domain, which I invented. The
real one is .setup-domain-add, so no rule matched and it rendered as a bare
browser button in the middle of a styled form.
* The dialog asked for name / type / host / user / path / password. A backup
location has SSH port and auth method (key or password — key is the default
and needs nothing typed), S3 access and secret keys, B2 account id and key,
and a path mode. It now asks for what each backend needs, with the wording
taken from the location config so the wizard and the Backup page describe
the same thing the same way.
* .setup-field styled input[type=text] and [type=email] but not [type=password]
or [type=number], so a credential field and the SSH port rendered unstyled
even inside a correct container.
Only the credentials go through the secret channel — SSH password, S3 secret
key, B2 account key. The rest is ordinary configuration and travels as itself.
The reason all three shipped is that I checked the step by querying the DOM and
never looked at it. Structural checks cannot see an unstyled control, and a
dialog is behind a click so a screenshot cannot reach it either. So:
lp-shot --eval <route> <js> run JS in the page and print the result
LP_SHOT_EVAL=<js> run JS before a capture — open a dialog, then shoot
and scripts/dev/lp-backup-dialog-test drives the whole thing in a real browser:
opens it, swaps every backend and asserts only that backend's fields show,
toggles SSH auth and asserts the password field follows, submits, and asserts
the credential is not left in the DOM.
Its styling check needed two attempts, which is the point of mutation-testing
it: "is the background transparent" passes for an unstyled button, because a
native button is grey rather than transparent. It now compares the control
against a bare <button> in the same parent, so "no rule matched" is what fails.
Verified: reintroducing the wrong class fails the test.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The wizard's Storage step builds its first entry from primaryRoot() — the
app-data root — and labelled it "System disk". On a default install those are
the same drive and the name is honest. Installed with --containers-dir on its
own disk they are not, and the step then showed the DATA drive's size under the
system disk's name while the actual system disk never appeared in the list.
Seen on a matrix case-2 install (apps on a 29.4G test disk, system on a 912G
root): "System disk — 26.7G free of 29.4G".
The generator now reports whether that root is really on the OS disk
(is_os_disk, by st_dev against /), and the wizard labels it from that: "System
disk" when they coincide, otherwise the mount point. The "system" badge stays —
it marks the default location, which is still what it is.
Also add lp-shot --token / --cookie-js. A screenshot answers "does it render";
"does this wizard step work" needs clicking, which needs a real browser, which
needs the session lp-shot already knows how to mint from the stored jwtSecret.
This bug was found that way.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The three roots are independently relocatable, and the failures that matter are
the ones where only ONE of them moves: paths are baked into root-owned helpers,
the systemd unit and the CLI wrapper at install time, so anything that resolves
a root at runtime instead works on a default install and points at the wrong
disk on a relocated one. Testing "all default" or "all moved" misses that.
scripts/dev/lp-testdisk loopback ext4 disks — a real superblock, its own
st_dev and free space, thrown away between runs
scripts/dev/lp-install-matrix installs across the four root combinations and
checks each landed on the intended DEVICE, that
the helpers were baked (no __PLACEHOLDER__ left)
and that the WebUI answers
First thing the harness turned up: lp-shot hardcoded /libreportal-containers for
both the compose file it reads the published port from and the .auth.json it
signs a session with. On an install whose app data is on another disk it fell
back to a default port and a missing auth file — which looks exactly like a
WebUI that failed to boot. It now reads the baked LP_CONTAINERS_DIR back out of
the CLI wrapper.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The WebUI screenshot helper CLAUDE.md already tells agents to use only
ever existed on the maintainer's box. Vendoring it means it survives a
machine rebuild and the setup steps are written down.
It does NOT ship: make_release.sh builds with `git archive`, which honours
export-ignore, so scripts/dev joins scripts/release and docs on that list.
Verified — the staged tarball has 1666 files and none under scripts/dev.
Keeping it out of releases is deliberate, not incidental. lp-shot signs
itself a session from the jwtSecret in frontend/.auth.json, which is fine
on a box where you already own that file, and has no business sitting in
a user's install where it would read as a backdoor.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>