From 7fef102369f0acd34e410a458181572a869e8f33 Mon Sep 17 00:00:00 2001 From: librelad Date: Sat, 29 Aug 2026 07:27:16 +0100 Subject: [PATCH] Put the found backups under the field they fill in MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The results were rendered above the whole form, which made them read as a separate step rather than as an answer about the Folder input. They now sit under that field, inside its box, alongside the verdict. What is shown depends on how many were found, because those are different situations: one it is the answer, not a choice — filled in, with its verdict. No card, because the card and the verdict said the same thing twice several listed as buttons, most snapshots first; the field stays empty, since this is genuinely ambiguous and guessing would be worse none says so — "we looked and there is nothing here" is information And a bug: the single result filled in only the PLACEHOLDER, so the field was still empty and pressing Check replied "give a full path, starting with /" about the very backup shown directly above it. It fills the value in now, and shows the verdict straight from the scan rather than making the user press Check to be told what is already on screen. Never overwrites something already typed — the user's own answer outranks anything we guessed. _adoptSingleResult moved into renderFoundBackups: "show what we found, and if there is exactly one, take it" is one behaviour, and splitting it meant the test could only reach half of it. Not done, deliberately: listing individual snapshots before the password. The count is a directory listing, but each snapshot's identity — host, tags, contents, when they are from — is in the encrypted object. Unlocked, all that could be shown is a column of hex IDs and file timestamps, which is not something anyone can choose between. After unlocking, the Contents step already lists the settings snapshot and one entry per app, which is how people think about it. Co-Authored-By: Claude Opus 5 --- .../frontend/core/setup/js/setup-wizard.js | 67 +++++++++++---- docs/roadmap/first-run-restore.md | 25 ++++++ scripts/dev/lp-restore-wizard-test | 83 +++++++++++++++---- 3 files changed, 143 insertions(+), 32 deletions(-) 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