A snapshot is one app's data, or the settings tree — never a machine. A
four-snapshot repository is typically two apps plus two versions of the
settings, not four backups to pick between. So the choice belongs on Contents,
after unlocking, where each snapshot has a name and a date rather than being a
hash.
Every row with more than one snapshot gets a picker, defaulting to the newest.
A row with one shows its date as text: a dropdown holding a single entry is a
control that cannot be operated, and it makes a repository with one backup look
like it is hiding something.
The chain already supported this. restorePickSnapshot has always passed any
value that is not the string "latest" straight through as an id; nothing ever
offered the choice. What was missing:
- restoreInspect returns every snapshot per app and for the settings, not
just the newest.
- restoreFirstRunBulk reads an optional RESTORE_SNAPSHOT_CHOICE map instead
of hardcoding "latest". An associative array rather than an argument,
because the CLI wrapper pads argv to nine slots and a per-app map cannot
survive it; the map reaches the host as base64 JSON, validated at the route
against restic short ids and app names since both hit a command line.
- backupRestoreSystemConfig takes a snapshot AND a host.
That host was a real bug. It defaulted to this machine's install name, which is
right for "recover my own settings" and wrong for a rebuild — the snapshots
carry the name of the machine being rebuilt FROM. It surfaced the moment a
restore adopted a config with a different install name and the next lookup
found nothing at all.
Verified by restoring both settings snapshots and diffing: 28bedbb0 brings back
a config carrying example.com, cc5b6bcf one with no domains.
Two CSS traps on the picker: appearance stayed `auto`, so the browser painted
its own control and ignored the colours entirely while the computed styles
looked right; and a `background:` shorthand later in the rule silently reset the
background-image, wiping the arrow set three lines above it.
lp-restore-adopt-test asserted configs/* were mode 0755 and started failing on
configs/webui, which libreportal-ownership sets to 0751:container on purpose —
tighter, and perfectly traversable. It asserts "the container user can traverse
it" now. A test that pins an incidental number reports a regression every time
someone improves the thing it is watching.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The wizard's first question is now "is this a new server, or a replacement for
one?", which §2 of the roadmap described and nothing implemented. Start asks,
and the answer selects one of two disjoint step sets:
new Start > Experience > Identity > Domains > Storage > Backups
> Import > Recommended > (Metrics)
restore Start > Source > Contents > Rebuild
Disjoint deliberately. A restore is never asked for an install name, domains or
an app list — the backup answers all three, and asking invites someone to type
an answer that is about to be written over. The test asserts non-overlap in
both directions, not just that the restore steps appear.
Source collects the repository the way the Backup page does, minus everything
that only means something for a place you write TO: no retention, no schedule,
no enable toggle. The password leaves through the one-shot secret:<ref> channel
and is cleared from the DOM, and the test asserts the value never appears in
the payload — that payload reaches a task command line, and tasks are recorded
world-readable.
Contents is the reconciliation, rendered: apps with sizes, and each domain with
a verdict, checked through the same /api/setup/dns-check the Domains step uses
rather than adding a second way to ask. Plus the offer to leave the strays out
until DNS is repointed.
Rebuild runs `restore rebuild`: settings first (they carry every other
repository's credentials), then domains, then apps with no explicit list so
bulk discovers and re-preflights them itself.
Inserting Start shifted every step index by one. validateStep was a chain of
idx === 1 … idx === 6, carrying a comment that already explained which earlier
insertions had moved them — it is keyed on the step name now.
lp-storage-step-test had the same pin and did not survive: it called
validateStep(3) for Storage, which had become Domains, and reported that
nothing blocked. That reads exactly like validation being broken. Tests look
their step up by name now too.
Also: locationRemove's fix means a failed connect can finally clean up after
itself, so a wrong password no longer leaves a dead destination behind on every
retry.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
portFindNextAvailablePort consulted LibrePortal's own network_resources table
plus a hardcoded list (8080, 6060) and CFG_RESERVED_PORTS_EXTRA — while the
comment above it claimed a picked port "can never collide with a host service at
compose-up time". It can: the list only covers what someone thought to write
down. Ask the kernel instead, via ss, read once per allocation rather than per
candidate. No ss => empty set => exactly the old behaviour.
Found while restoring 13 apps onto a desktop, though not the cause there:
stoat's livekit publishes a FIXED udp range (50000-50100, which it advertises to
clients and so cannot be re-rolled), and kdeconnectd held 50016. That collision
needs its own answer; this fixes the randomly-allocated ports, which had the
same exposure with no reason to.
Also make the bulk restore stop reporting a half-running app as a clean success.
continue-on-error lets a failed compose-up log and carry on, so restoreAppStart
returns 0 either way — which is how that run printed "13 apps restored" while
four of stoat's containers had exited 101. checkSuccess already appends every
failure to error_report.log, so watch it grow across each app and name the ones
that were noisy.
scripts/dev/lp-port-host-test binds a real socket and asserts the allocator
refuses that port; verified it fails when the check is removed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
An end-to-end run restored 4 of 13 apps and reported
"First-run restore complete — 4 apps restored" as a success.
Two truncations, and fixing the first had hidden the second:
* the CLI dispatcher calls handlers with no arguments, so "$@"/shift inside
one operate on an empty list. Fixed earlier with LP_CLI_ARGS.
* LP_CLI_ARGS was built from start.sh's "$@" — but the root wrapper invokes
start.sh with exactly nine hardcoded positional slots. So the array could
never hold more than nine entries, and `${LP_CLI_ARGS[@]:5}` yielded at
most four app names.
The wrapper now forwards the real argv after those nine slots (they stay
untouched: every dispatcher reads them, and unset ones must keep arriving as
the literal "empty"), and start.sh reads it back as "${@:10}". Verified: a
preflight given six apps checks six, where five was the previous ceiling.
footprint_version 7 -> 8, since the wrapper is root-owned and baked at install.
Two further fixes so a truncation cannot pass as success again:
* restoreFirstRunBulk with no app list is now a whole-host restore — it
discovers the host's apps and re-applies the preflight. The installer's
report runs in its own process, so without this an app the user was told
would be skipped got restored anyway. init.sh now passes no list, so a
whole-host restore builds nothing that can be truncated.
* it counts what actually landed and returns non-zero naming the failures,
instead of reporting the length of the list it was handed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
sudo-rs — the default sudo from Ubuntu 25.10, so on 26.04 — does not
implement bare -E. It does not reject it either: it warns to stderr
("preserving the entire environment is not supported, '-E' is ignored")
and runs the command with the environment DROPPED, leaving the exit
status untouched. Callers capture stderr, so the warning is invisible and
the backup engines simply never receive RESTIC_PASSWORD / BORG_PASSPHRASE
/ KOPIA_PASSWORD and cannot open the repository.
Name the nine vars explicitly via --preserve-env=<list>, which sudo-rs
and classic sudo (>=1.8.21, so Debian 10's 1.8.27) both honour, so this
needs no version gate. The list is cross-checked against every
RESTIC_/BORG_/KOPIA_ var the engine env scripts export.
The list lives in variables.sh with a literal fallback in runBackupOp,
because init.sh sources run_privileged.sh directly during install without
ever loading variables.sh — an unguarded empty list would silently
reproduce the same dropped-credential bug.
restoreFirstRunDiscover now goes through runBackupOp rather than issuing
its own sudo. It was the only backup-engine call bypassing that funnel,
which is why it missed this fix by construction; routing it back also
gives it the -H that keeps restic's cache under the backup user's HOME.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A free, open, self-hosted app platform (GNU AGPLv3): one-click app deploys,
Traefik reverse proxy with automatic SSL, rootless Docker support, gluetun
VPN routing, and a web dashboard to manage it all.
Free & open forever to self-host; optional paid hosted services fund it.
See PROMISE.md.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>