fix(webapp): restore is not offered on the version that is already current (BEA-57) - #110
Merged
ssowonny merged 1 commit intoAug 3, 2026
Conversation
…rrent (BEA-57) The newest row for a path IS the file's current content, so its `restore` button could only ever journal a +0 −0 change — attributed to a real person, on a real device, replicated to every teammate, in the audit trail the whole history story depends on. It was also the single most tempting row to click. One rule, enforced at both ends. handleRestore now 409s when the requested sha is already the path's head (journal.Replay, the way the CLI already answers this question), placed after the "no such version of that path" 404 and before CheckWrite so an unknown sha still 404s and a refused restore records no quota. HistoryView computes each path's head from the loaded window — entries are strictly newest-first, so a path's first occurrence decides — and restoreSha returns undefined for bytes that already are the head, which removes the button, its title and its busy state together. The rule is content equality, not row index: an older row hand-reverted to the current bytes is just as much of a no-op, and matching what the server checks means the UI can never show a button that errors. A newest DELETE leaves the path out of the replay, so a deleted file still restores — that is a real change. Confirm-on-restore stays out, deliberately (HistoryRow.tsx:154): the defect was never "restore should ask", it was "restore should not be offered where it cannot do anything". Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ssowonny
deleted the
bea-57-ph-scan-bug-restore-is-offered-on-the-version-that-is
branch
August 3, 2026 02:06
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
TL;DR
restoreon a file's newest version used to write a+0 −0change to everyone's history — that button is gone now, since it's already the file's content.409, so no client can manufacture the empty row either.+0 −0rows already written stay. Journals are append-only.Fixes BEA-57.
Screenshots
Per-file history for
guide.md— the top row is the file's current content:The project-wide feed, where the same rule applies per path (and reaches inside run cards).
restoresurvives exactly where it can do something: theDELETED scratch.mdrow, and every older version:What was wrong
restoreSha(i)returnedentries[i].blobfor any non-delete row, and the newest entry for a path is its current content.HistoryRow'srestorablenever compared against the file's head. So the top row of every version list offered a restore that was a guaranteed no-op write — journaled under a real account, on a real device, replicated to every teammate, in the audit trail BearDrive's history story depends on.handleRestoreaccepted the same call from any client.What changed
Server (
internal/webapp/restore.go) — one guard, positioned deliberately:after the 404 so an unknown sha still 404s, and before
CheckWriteso a refused restore records no quota. Head comes fromjournal.Replay, which is how the CLI already answers this question and which sorts internally —loadOpsreturns ops unsorted, so hand-rolling "last op wins" over that slice would have been wrong.Client (
HistoryView.tsx) — entries are strictly newest-first, so one walk gives each path's head; a newestdeletemaps to "nothing is current", which is what keeps deleted files restorable.restoreShareturnsundefinedfor bytes that already are the head. No change inHistoryRow.tsx:restorablealready requires!!restoreSha, so the button, its title and its busy state disappear together.Matching the server's check exactly means the UI can never show a button that errors — which is why the rule is content equality rather than row index.
Not doing: a confirm dialog on restore. Its absence is deliberate (
HistoryRow.tsx:154— removal reaches every device destructively, a restore is additive). The defect was never "restore should ask", it was "restore should not be offered where it cannot do anything".Deviation from the reviewed plan
One thing the plan didn't anticipate: an existing e2e assertion was pinning the bug.
browse.spec.tsasserted the run card'snotes/readme.mdedit row still offers a restore — but that seeded op is the head of that path, so under the new rule it correctly loses the button. The assertion (and the now-stale seed comment ine2e_serve_test.gocalling that file "restorable") were updated to state the new rule, and the test still proves what it was really about: the run-created row's undo is a removal, and the edit row gets neither control. This also gives the rule coverage inside run cards, which the plan's e2e step didn't reach.Tests
TestRestoreNoOpCurrentVersion— restoring the current version409s with every journal byte-identical; the older version still200s; and once it is current, restoring it409s too, proving the guard is narrow.TestRestoreAfterDeletegained a case: replay drops a deleted path, so restoring it goes through — and once the file is back, the same call is the no-op.guide.mdsaying "Second version"); the API409s the head sha and nothing is written; then, on its own file, a real restore leaves the new top row without a control and the hand-reverted first version without one either.go test ./...,go vet ./..., and all 108 Playwright specs pass;internal/webapp/staticrebuilt and committed (check-dist.sh: fresh).Build session
(only works on the machine that ran the build)