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