diff --git a/containers/libreportal/frontend/core/setup/js/setup-wizard.js b/containers/libreportal/frontend/core/setup/js/setup-wizard.js
index b74f785..36e2363 100755
--- a/containers/libreportal/frontend/core/setup/js/setup-wizard.js
+++ b/containers/libreportal/frontend/core/setup/js/setup-wizard.js
@@ -448,9 +448,7 @@ class SetupWizard {
Where is it? Backups live in a repository \u2014 a folder on a
disk, or a remote server. Not a single file.
-
-
+
+
`) +
group('sftp', 'SFTP server',
'Reached over SSH, with the key or password this server already uses.',
diff --git a/docs/roadmap/first-run-restore.md b/docs/roadmap/first-run-restore.md
index 323c9a3..6417b40 100644
--- a/docs/roadmap/first-run-restore.md
+++ b/docs/roadmap/first-run-restore.md
@@ -574,6 +574,31 @@ The placeholder now comes from this machine — the first repository found, or
the install's own backups root — because a placeholder's whole job is to show
the shape of the answer, and only a real one does that.
+Both the results and the verdict live **under the Folder field**, inside its
+box: they are answers about that input, and floating them above the whole form
+made them look like a separate step. What is shown depends on how many were
+found, because those are genuinely different situations:
+
+| Found | What happens |
+|---|---|
+| one | it is the answer, not a choice — filled in, with its verdict. No card: the card and the verdict said the same thing twice |
+| several | listed as buttons, most snapshots first. The field stays empty — this is genuinely ambiguous and guessing would be worse |
+| none | says so. "We looked and there is nothing here" is information; empty space is not |
+
+The first version filled in only the *placeholder*, which left the field empty —
+so pressing **Check** replied "give a full path, starting with /" about the very
+backup displayed directly above it.
+
+**Not done: listing individual snapshots before the password.** The count is a
+directory listing, but the *identity* of each snapshot — its host, its tags,
+what it holds, when its contents are from — lives in the encrypted object. All
+that could be shown unlocked is a column of hex IDs and file timestamps, which
+is not a thing anyone can choose between. Once the password is in, the Contents
+step already lists what is there, grouped the way people think about it: the
+settings snapshot, and one entry per app. Choosing a specific *older* snapshot
+to restore from is a real feature, but it belongs there, after unlocking, per
+app — not here.
+
### The index that moved
Inserting `Start` shifted every step index by one, and `validateStep` was a
diff --git a/scripts/dev/lp-restore-wizard-test b/scripts/dev/lp-restore-wizard-test
index 924e302..f5d21bf 100755
--- a/scripts/dev/lp-restore-wizard-test
+++ b/scripts/dev/lp-restore-wizard-test
@@ -129,16 +129,50 @@ read -r -d '' DRIVE <<'JS'
}
out.scanFoundSomething = (w.foundBackups || []).length > 0;
out.foundCarrySnapshotCounts = (w.foundBackups || []).every(f => typeof f.snapshots === 'number');
- const card = $('.setup-found-backup');
- out.foundRenderedAsButton = !!card && card.tagName === 'BUTTON';
- if (card) {
- card.click();
- await new Promise(r => setTimeout(r, 200));
- out.clickFillsThePath = ($('#sw-rs-path') || {}).value === (w.foundBackups[0] || {}).path;
- out.clickReportsWithoutPassword = /snapshot/i.test(($('#sw-rs-verify-result') || {}).textContent || '');
- }
- // The placeholder must be a path from THIS machine, never an invented
- // example: /mnt/usb/... sends someone looking for a folder that is not there.
+
+ // Both the list and the verdict belong to the Folder field, under it, rather
+ // than floating above the whole form: they are answers about that input.
+ const localGroup = $('.setup-subgroup[data-rs-group="local"]');
+ out.foundInsideTheGroup = !!(localGroup && $('#sw-rs-found') && localGroup.contains($('#sw-rs-found')));
+ out.verdictInsideTheGroup = !!(localGroup && $('#sw-rs-verify-result')
+ && localGroup.contains($('#sw-rs-verify-result')));
+
+ // --- exactly one found: it is the answer, not a choice ---
+ $('#sw-rs-path').value = '';
+ w.foundBackups = [{ path: '/only/one', snapshots: 4, newest: '2026-08-29T05:51:00+01:00' }];
+ w.renderFoundBackups();
+ out.singlePrefillsThePath = ($('#sw-rs-path') || {}).value === '/only/one';
+ out.singleShowsItsVerdict = /4 snapshots/.test(($('#sw-rs-verify-result') || {}).textContent || '');
+ // The card and the verdict would otherwise say the same thing twice.
+ out.singleDrawsNoDuplicateCard = $('#sw-rs-found .setup-found-backup') === null;
+ // Pressing Check must confirm, not reply "give a full path" about the backup
+ // shown directly above it — which is what an empty field did.
+ await w.verifyBackupPath();
+ out.singleCheckDoesNotScold = !/full path/i.test(($('#sw-rs-verify-result') || {}).textContent || '');
+
+ // --- several found: a real choice, so ask ---
+ $('#sw-rs-path').value = '';
+ w.foundBackups = [
+ { path: '/repo/a', snapshots: 3, newest: '2026-08-01T10:00:00+01:00' },
+ { path: '/repo/b', snapshots: 9, newest: '2026-08-20T10:00:00+01:00' }
+ ];
+ w.renderFoundBackups();
+ out.multipleAreListed = document.querySelectorAll('#sw-rs-found .setup-found-backup').length;
+ out.multipleRenderedAsButtons =
+ Array.from(document.querySelectorAll('#sw-rs-found .setup-found-backup')).every(b => b.tagName === 'BUTTON');
+ // Never guessed when it is genuinely ambiguous.
+ out.multipleLeaveThePathEmpty = ($('#sw-rs-path') || {}).value === '';
+ $('#sw-rs-found .setup-found-backup').click();
+ out.pickingOneFillsThePath = ($('#sw-rs-path') || {}).value === '/repo/a';
+ out.pickingOneReportsWithoutPassword =
+ /snapshot/i.test(($('#sw-rs-verify-result') || {}).textContent || '');
+
+ // --- none found ---
+ $('#sw-rs-path').value = '';
+ w.foundBackups = [];
+ w.renderFoundBackups();
+ // "We looked and there is nothing here" is information; empty space is not.
+ out.noneSaysSo = /nothing found/i.test(($('#sw-rs-found') || {}).textContent || '');
out.placeholderIsReal = !/mnt\/usb/.test(($('#sw-rs-path') || {}).placeholder || '');
// Pointing at the folder that HOLDS the repositories is the common near-miss,
@@ -264,14 +298,29 @@ chk "missing password refused" "$(g .missingPasswordRefused)" true
chk "a complete source accepted" "$(g .completeAccepted)" true
echo "finding backups without a password"
-chk "the scan found one" "$(g .scanFoundSomething)" true
-chk "with a snapshot count" "$(g .foundCarrySnapshotCounts)" true
-chk "rendered as a button" "$(g .foundRenderedAsButton)" true
-chk "clicking fills the path" "$(g .clickFillsThePath)" true
-chk "and reports before any password" "$(g .clickReportsWithoutPassword)" true
-chk "placeholder is a real path" "$(g .placeholderIsReal)" true
+chk "the scan found one" "$(g .scanFoundSomething)" true
+chk "with a snapshot count" "$(g .foundCarrySnapshotCounts)" true
+chk "the list sits under the field" "$(g .foundInsideTheGroup)" true
+chk "so does the verdict" "$(g .verdictInsideTheGroup)" true
+
+echo " one result is the answer, not a choice"
+chk "it fills the path in" "$(g .singlePrefillsThePath)" true
+chk "and shows its verdict" "$(g .singleShowsItsVerdict)" true
+chk "without a duplicate card" "$(g .singleDrawsNoDuplicateCard)" true
+chk "Check confirms, not scolds" "$(g .singleCheckDoesNotScold)" true
+
+echo " several is a real choice, so ask"
+chk "all are listed" "$(g .multipleAreListed)" 2
+chk "as buttons" "$(g .multipleRenderedAsButtons)" true
+chk "and nothing is guessed" "$(g .multipleLeaveThePathEmpty)" true
+chk "picking one fills the path" "$(g .pickingOneFillsThePath)" true
+chk "and reports before any password" "$(g .pickingOneReportsWithoutPassword)" true
+
+echo " none found"
+chk "says so rather than nothing" "$(g .noneSaysSo)" true
+chk "placeholder is a real path" "$(g .placeholderIsReal)" true
chk "the parent-folder mistake is explained" "$(g .parentFolderExplained)" true
-chk "and the real path is offered" "$(g .offersTheRealPath)" true
+chk "and the real path is offered" "$(g .offersTheRealPath)" true
echo "the password"
chk "goes through the secret channel" "$(g .passwordWasStashed)" true