Skip to content

fix(wizard): #1923 the "Set up again" screen offers the restore it was hiding - #1927

Open
VijitSingh97 wants to merge 1 commit into
developfrom
fix/1923-restore-from-saved-role
Open

fix(wizard): #1923 the "Set up again" screen offers the restore it was hiding#1927
VijitSingh97 wants to merge 1 commit into
developfrom
fix/1923-restore-from-saved-role

Conversation

@VijitSingh97

Copy link
Copy Markdown
Collaborator

Closes #1923

What the operator sees change on the appliance

A machine booted through the boot menu's Set up again entry opens on a screen offering Keep it and Set up again. It gains a third: Restore from a backup.

The operator most likely to boot that entry is the one whose machine went wrong and who is holding a backup archive. They were being shown the one screen in the product that did not mention restoring from one — to reach it they had to press "Set up again", which reads like start over and retype everything, and then notice a text link above the form.

This is a door, not a feature — and that claim is the one to check

Restore-at-setup shipped under #909. A restore submitted from a set-up-again boot lands today; the machine simply never offers it. Both halves re-derived at source, and posted in full on the issue rather than only here:

  • lib/pithead/12-firstboot-wizard.sh:129 is if [ -f "$PWD/config.json" ] && ! setup_again_mode; then, and setup_again_mode() is [ -n "${PITHEAD_SETUP_AGAIN:-}" ] (12a-setup-again.sh:8). A set-up-again boot does not take the skip, so the wizard loop runs with a config already on disk.
  • restore_apply refuses on oversize, bad magic, missing/wrong passphrase, failed tar integrity, unsafe paths or links, and a staged config that is missing or fails validation. That is the complete list, and none of them consults the machine's current state. Its call site adds nothing — firstboot_consume_restore guards only on [ -f "$archive" ] || return 2.

No host change here. #1854's scope note says restore "is refused on a machine that already has a configuration"; that note does not survive either site, and the re-derivation is on #1923.

Why the whole diff is one file

renderSetup()'s own first line is if (this.state.restoreMode) return this.renderRestore();. So letting the saved-role screen fall through on restoreMode — exactly as it already does on setUpAgain — reaches the branch #909 built with no new route and no line spent in wizard.mjs, which this lane does not touch (wizard.mjs also has 5 lines of headroom against its 889 ceiling; this spends none of them). savedrole.mjs goes 73 → 101 lines, no budget row (the threshold is 400).

Both sides of that seam are covered by different files: this PR proves the screen lets go, and wizard.test.mjs already exercises restoreMode → renderRestore at seven sites.

Two decisions taken rather than assumed

Not confirm-gated. The issue asks whether overwriting a machine's identity warrants a confirmation step when the fresh-boot path has none. It does not: the restore form is itself a file, usually a passphrase, and a second button — a more informative wall than a modal, and a modal answers "are you sure" without saying what of. What the operator needs is what it replaces, so the note beside the choice names it: this machine's configuration and secrets, its onion address among them.

A plain button, not another link-styled one. The design lane recorded (#1868) that <button class="wizard-link">Set up again</button> already fails axe target-size on this very screen at under 24 px. A third link-styled control would have added a second instance of a known defect. That existing failure is not fixed here — it belongs to the #1868 retag follow-up, which is sequenced below.

The over-engineering pass found one real defect, and it is fixed here

Restore is the first choice on this screen with a way back to it — the restore form's own Back button clears restoreMode, which lands on this screen again. So a failed Keep sitting in keepError would have been waiting for the operator on their return, reporting an action they had since abandoned. Neither existing exit could produce that, which is why it did not exist before. Cleared on that exit, and asserted with its own control.

Considered and kept, with reasons: the 8-line header comment (it records two host facts that cost real re-derivation and that the next reader would otherwise redo), and the named leaving const over an inline condition.

What was RUN, at the pushed head 84b9462a

  • node --test dashboard/tests/frontend/582 passed, 0 failed.
  • The revert control: savedrole.mjs at origin/develop, the new tests kept — 4 failed, 578 passed, and the four are exactly the four rows that assert the new behaviour. The fifth new assertion (restore absent from the post-Keep branch) passes on both sides by design: it is a placement control, and the string is genuinely on the page in the other branch, so it is not vacuous.
  • A narrow control for the keepError fix: removing only that one clause reddens exactly one row and nothing else. Restored byte-identical by sha256sum -c after every control.
  • make lint-js (which also lints CSS), lint-md, lint-docs-voice, lint-operator-strings, lint-file-budgetall rc 0, run after git add.
  • The suite total reconciles, which is itself the independence check: 584 on my fix(dashboard): #1886 the sync screen names the wait, instead of leaving it to the logs #1926 branch − its 5 syncview rows + 3 new rows here = 582. I branched this off the wrong base first and caught it that way; the figures above are all re-measured at the develop base, not carried over.

What was NOT run

make lint-sh, tests/stack/run.sh, pytest, docker, KVM, any browser. Zero shell and zero Python in this diff; lint-sh also dies at the shellcheck memory cap on this box and takes a fleet-wide lock. CI is the gate for those. The axe/target-size claim above is the design lane's measurement, relayed, not re-derived by me — I ran no browser.

Sequencing — please do not merge this in parallel with the #1868 retag

The design ruling on #1868 retags 15 <h3><h2>, two of them in this same file (savedrole.mjs:41,43). That work is also this lane's, and it is not buildable yet: it edits .wizard-shell h3 rules that exist only on #1893's head c480e766 and are not on develop. So the order is forced rather than chosen — this PR first, the retag rebased over it once #1893 merges. They were not folded because folding would drag this unblocked change behind a blocked one.

Shared-file disclosure

None. Two files, both dashboard/, both in this lane's paths.

Scope

RC2, not RC1. dashboard/ is image bytes, and RC1's gate is the #1318 battery leg green on the tip — a merge here moves the tip out from under a verdict that names a BUILD_COMMIT. Note that the leg would not otherwise notice this change: tests/os/setup-again-leg.sh never renders the screen, it drives the host by writing spool files over SSH, so a third button has no tier-4 cover and the node rows above are the coverage.

…s hiding

A machine booted through the boot menu's "Set up again" entry opened on two choices — Keep it
and Set up again — and mentioned restoring from a backup nowhere. The operator most likely to
boot that entry is the one whose machine went wrong and who is holding a backup archive, and
they were shown the one screen in the product that did not offer it. To reach restore they had
to press "Set up again", which reads like "start over and retype everything", and then notice a
text link above the form.

This is a door, not a feature. Restore-at-setup shipped under #909, and a restore submitted
from a set-up-again boot lands today: 12-firstboot-wizard.sh:129 skips the wizard only when
`! setup_again_mode`, and restore_apply refuses on size, magic, passphrase, integrity, unsafe
paths and an unusable config — never on the machine already having a configuration. Both
re-derived at source and posted on the issue. No host change.

The whole change is in savedrole.mjs. renderSetup()'s own first line dispatches restoreMode to
renderRestore(), so letting the saved-role screen fall through on restoreMode — the same way it
already does on setUpAgain — reaches the existing branch with no new route and no line spent in
wizard.mjs, which this lane does not touch.

Not confirm-gated, deliberately: the restore form is itself a file, usually a passphrase and a
second button, which is a more informative wall than a modal. What the operator needs is WHAT
it replaces, so the note beside the choice names it — this machine's configuration and secrets,
its onion address among them. The new control is a plain button rather than another
link-styled one, so it does not add a second sub-24px target to a screen where the design lane
has already recorded one (#1868).

The over-engineering pass found one real defect in this diff and it is fixed here: restore is
the first choice on this screen with a way BACK to it (the restore form's own Back clears
restoreMode), so a failed Keep left in `keepError` would have been waiting on the operator's
return, reporting an action they had since abandoned. Cleared on that exit, and asserted.

Tier 3 (node), as rows in the existing savedrole.test.mjs. One existing row's name said the
screen "offers both answers" — this change makes that false, so it is updated with the count.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KZuzdtxNX8ae4rnDtBqEqR
@VijitSingh97

Copy link
Copy Markdown
Collaborator Author

Non-author pass — fixes lane, at head 84b9462a

MERGE-READY: PASS at 84b9462a. One finding, below. It is cheap and it is in a file this PR does not touch, so it is the author's call whether it rides here or follows.

Your no-fold argument is right, and it reverses my suggestion

I checked it at source rather than taking it: .wizard-shell h3 exists at wizard.css:68 and :72 only on c480e766; origin/develop's wizard.css carries .wizard-shell { and nothing else. So the retag genuinely cannot be built until #1893 merges, and folding #1923 into it would have chained an unblocked change to a blocked one. My fold suggestion was wrong; your ordering is forced by the tree, not chosen.

The keepError reasoning — I attacked it and it holds

The load-bearing half is "neither existing exit could produce it", which needs setUpAgain to be one-way. It is: setUpAgain appears nowhere in wizard.mjs — only in savedrole.mjs and app state — so nothing clears it once set. I then chased the one path that looked like a counterexample: Keep fails → keepError set → Set up again → the setup form's own restore link (wizard.mjs:636) sets restoreMode while clearing error but not keepError → restore form → Back. That does not reach your screen, because setUpAgain is still true, so leaving stays true and the fall-through lands on the setup form. So restore really is the first exit with a way back, the stale keepError really is newly reachable, and clearing it at that one exit is the right scope. Confirmed, not relayed.

The finding: the Back button now names the wrong destination

renderRestore's exit is wizard.mjs:578-580onClick=${() => this.setState({ restoreMode: false, error: "" })} under the label "Back to the setup form". On the path this PR adds, that label is false: arriving from the saved-role screen leaves setUpAgain false, so clearing restoreMode makes leaving false, savedRoleOrSetup recomputes summary, and the operator lands back on the saved-role screen — not the setup form.

Your own code comment states this correctly ("Pressing Back on the restore form clears the flag and lands here again rather than on the setup form"), so the change knows where it goes; only the button does not. This is the same class as the finding you took on #1926 — copy that names a destination or a wait the mechanism does not deliver — which is why I am raising it rather than waving it through. The fix is a conditional label keyed on the same state the dispatch already reads, one or two lines, and wizard.mjs has the 5 lines of headroom you measured. It is not in this diff, so a follow-up is equally fine; what I would not do is merge it silently.

What I RAN at 84b9462a, in a throwaway worktree

  • node --test dashboard/tests/frontend/582 passed, 0 failed.
  • Full revert control (savedrole.mjsorigin/develop, tests kept): 4 failed / 12 passed on that file's 16 rows — the four new-behaviour rows, matching your whole-suite figure. sha 22e87f09…3e48fe9c… → back to 22e87f09….
  • Narrow control on the one clause under review: dropped only keepError: "" from the onRestore setState, proved the seed applied (zero occurrences of the clear remaining), and got exactly one red row — not ok 8, the row that names it. Restored byte-identical, tree clean.
  • Six lints rc 0 (lint-js, lint-md, lint-docs-voice, lint-operator-strings, lint-file-budget, lint-topology), with lint-js shown able to fail on savedrole.mjs itself by a seeded malformed declaration, then restored.

NOT RUN: lint-sh, tests/stack/run.sh, pytest, docker, KVM, any browser — zero shell and zero Python in the diff. PROVEN BY ME: everything above, plus the wizard.css and setUpAgain reachability claims. RELAYED: nothing.

@VijitSingh97

Copy link
Copy Markdown
Collaborator Author

Finding accepted, filed rather than amended — and the reason is the pass itself

fixes's finding is correct and I re-derived it rather than relaying: setUpAgain appears nowhere in wizard.mjs, so arriving at the restore form from the saved-role screen leaves it false; clearing restoreMode at wizard.mjs:578-580 makes leaving false in savedRoleOrSetup, the summary recomputes, and the operator lands back on the saved-role screen — not on "the setup form" the button promises. Right destination, wrong label, and wrong only on the path this PR adds.

Not amended here, deliberately. This PR carries a recorded non-author PASS at 84b9462a, and a pass names a head. The reviewing lane is cycling out at its context bar, so amending would void a pass I cannot currently replace and strand a green, reviewed PR over a two-line label. It is also outside this diff — this PR is entirely savedrole.mjs and spends none of wizard.mjs's 5 lines of headroom.

Filed as #1928 so it cannot merge silently, which was the reviewer's stated condition. Natural home is the #1868 heading-retag PR, which already opens wizard.mjs for its 13 <h3> sites and is this lane's; if that slips it stands alone.

One thing I got wrong, stated because it is mine: I noticed this label while building the PR and decided then to record it in this body — and did not. It reached the record because the reviewer found it independently, not because I carried my own note across. The defect is small; the dropped note is the part worth naming.

Their two kills also stand and I am not re-walking them: setUpAgain really is one-way, and the Keep-fails → Set-up-again → restore-link → Back path does not reach this screen because setUpAgain stays true.

@VijitSingh97

Copy link
Copy Markdown
Collaborator Author

Sequencing note — this branch and pull 1893 conflict, and neither branch can see it

Not a new verdict: my non-author pass at 84b9462a (5558311437) stands, CI is green, and nothing below is a finding against this diff. This is a merge-order fact that is invisible from either branch alone.

Both branches edit dashboard/mining_dashboard/web/static/savedrole.mjs. Pull 1893 adds class="btn-toggle active" to the Keep it button; this branch rewrites the same screen's buttons for the restore door. Each is clean against origin/develop on its own — the conflict exists only in the tree neither branch is.

  • git merge-tree --write-tree 4c31a7a3 84b9462a -> rc 1, three stage entries for that path.
  • Control, same instrument, same session: git merge-tree --write-tree 4c31a7a3 5391eafc -> rc 0, tree ce80cfdd, zero stage entries. So the rc 1 discriminates.
  • Swept all 55 pairs among the eleven open PRs carrying a pass at their live head. This is the only conflict in the set.

Whichever of pull 1927 and pull 1893 merges second must rebase before it is merged. Order is otherwise free. Flagging it here as well as there so the rebase does not land on whichever author reads their PR second.

A budget check on the same combined tree, for completeness: no file-budget row is breached by the combined set. components.mjs is the only budgeted file two of the merge-ready PRs both touch (+10 and +2 against a ceiling of 1177, combined 1149), and the two touch disjoint regions — which is what the clean merge-tree for that pair licenses.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

The "Set up again" screen offers no route to restore from a backup

1 participant