* Keep deleted pages in a trash so a teammate's unsent work can be restored Deleting a page now moves it to a trash on the server (pages.deletedAt): hidden from every read and refusing changes (PAGE_DELETED), with its content kept for 30 days, then purged. pages:restore brings it back at its place. The deleted-page notice offers Restore page next to Discard; the work refused while the page was gone is sent again under new op ids. The delete confirmation says who presence last saw on the page. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Read only live pages' content, and keep old clients' trashed-page deletes a no-op From Astra's review of the migration: - Live pages come from an index on (strategyId, deletedAt), and full snapshots and strategy-wide element and lineup lists read content page by page, so thirty days of trash never count against a live read. - A delete of content on a trashed page is refused only for clients that send checkTrashedPageDeletes (this one); older clients get the no-op they got when the content was purged. - A replayed rejection shows no current copy of content in the trash. - Deleting a strategy leaves its trashed pages to the trash purge, so the number of scheduled purges stays bounded by its live pages. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Restore holds up against late reads and a page that comes back From Astra's review of the client: - A page deleted from this device stops counting as deleted here once a read at a later strategy revision lists it again (restored), so a teammate's later delete of it shows the notice. - A read of the shell and page that a newer read or page selection overtook changes nothing, and a watched shell older than the one held is ignored: a read from before a restore cannot undo it. - Restore fails honestly if sends already made for the page never answer, and reports success only if the notice really cleared. - Discard works once the page is back: it shows the server's copy. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Late reads and replayed deletes cannot undo a restore From Astra's second review: - A delete answered on replay (noop at the strategy's current revision) no longer outlives a restore at that same revision: a page listed at the delete's revision or later is back. - A refresh drops a shell older than the one held, as the live read already did, so it cannot hide a page the live read just restored. - A page read that a newer selection overtook no longer reports its failure over the newer state. - Element and lineup lists read only their own table. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Send again what a lost answer's replay brings back refused From Astra's third review: a send whose answer was lost waits, queued, to be replayed under its own op id, and the server answers the replay with the refusal it recorded while the page was in the trash. Restore now waits for the page's sends to be answered after its flush and sends such refusals again. Sends still unanswered after the wait keep saving as usual; a late refusal among them shows in the sync status. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Only a page delete the server applied counts as this device's From Astra's fourth review: a delete replayed after its answer was lost is answered with a no-op at the strategy's current revision, which may come after a teammate restored the page and deleted it again. A no-op only says the page was already gone, not by whom, so it no longer marks the page as deleted here; the teammate's delete shows the notice. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * A page seen live after this device's delete is not deleted here From Astra's fifth review: an applied delete's ack is published only after the outbox write, so it can reach the session after reads showing a teammate's restore and second delete. The session now remembers the latest revision each page was seen live, and counts a delete as this device's only while it is newer than that, whichever order the ack and the reads arrive in. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Show teammates' names in bold, in their cursor colour, in the delete warning Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Ask the server which images it still shows before dropping an upload The upload queue decided whether a pending upload was still wanted by reading the strategy's full snapshot. It now asks the new images:listReferencedAssetIds query: every image the server's content shows, read from the reference rows, or null while the reference backfill has not finished (the queue then keeps the upload and asks again). Nothing changes for users today. This ships before the page trash (#228). The trash hides deleted pages from the full snapshot while their images are still wanted, so a client still reading the snapshot could drop the bytes of an image on a page that can be restored. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Test the uncertain answer on a staged upload, the job it holds A durable upload may upload while the server cannot tell (nothing the user made is lost that way); a staged one keeps its check, its job and its bytes, and never uploads. The test now covers the staged case, where the answer decides. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Ask only about the images in question, a bounded batch at a time listReferencedAssetIds read every live reference row of the strategy, so a strategy with tens of thousands of image references (thousands of lineups sharing screenshots) would pass Convex's read limit and never get an answer. It now takes the asset ids the client holds uploads for, at most 100 per call, and looks each up through the strategy/asset/ deleted index. The repository asks in batches of 100; if any batch cannot tell, the answer is null. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * List recently deleted pages, with who deleted them, and restore from there Deleting a page records who deleted it (pages.deletedBy, optional, set by the page.delete op and the older pages:delete; cleared on restore). The new query pages:listTrashed (editor role, like delete and restore) lists the strategy's restorable pages, newest first, with the deleter's stored display name, whether it was you, and when the page is purged. A Recently deleted button in the pages bar's footer (cloud strategies, for those who can delete pages) opens a dialog listing them: "Deleted by Sam, 2 days ago · 28 days left", or "Deleted 2 days ago" when the deleter is not known, with Restore. Restore puts the page back where it was and sends again the changes the server refused while it was in the trash. The delete confirmation now says a cloud page can be restored from Recently deleted for 30 days; a local page still cannot be undone. The deleted-page notice is titled "This page was deleted". Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Keep the new tests' constants const and imports lean Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Say, when reading a full snapshot, that the trash's pages may be left out getFullSnapshot takes an optional acceptsTrashedPagesLeftOut, ignored for now; this client sends it. Once the server keeps deleted pages in a trash (#228), it can refuse a snapshot to a client that does not send it while the strategy holds trashed pages: such a client decides from the snapshot whether an upload is still wanted, and would drop the bytes of an image on a page that can be restored. A refused read already keeps the bytes. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Refuse a trash-blind snapshot to old tabs, and tighten Recently deleted getFullSnapshot now refuses, with CLIENT_UPGRADE_REQUIRED, a client that does not send acceptsTrashedPagesLeftOut (#233) while the strategy holds trashed pages. Such a client decides from the snapshot whether an upload is still wanted, and would drop the bytes of an image on a page that can be restored; its upload check keeps the bytes when the read fails, and a reload brings the current build. From Astra's review of Recently deleted: - Restoring from the list waits for the page's queued sends too, and sends again what a lost answer's replay brings back refused. - A page whose time runs out while the list is open loses Restore; the list refreshes its times every minute. - listTrashed reads only the restorable range of the index, one read per deleter. - Rows use the body and label type roles and the spacing steps. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Read each deleter once, and test Restore going away while the list is open listTrashed started a read per page before deduplicating; it now reads each deleter once. Recently deleted reads the time through package:clock, so a widget test can move past a page's time while the list is open and see Restore go. The outbox test's keys are const. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Offer Undo after deleting a cloud page; lead Recently deleted rows with the name Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Undo: go when the strategy closes, re-check it's the same strategy, open the page only once it's listed Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Undo: pin the strategy before the delete, check after every wait, say only what's true Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Undo: check the strategy after the confirmation, recheck the list before claiming it Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Undo: a failed restore after leaving names the strategy the page is in Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Undo: say when a page can no longer come back; name no strategy when unknown Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Say a restore couldn't be confirmed, not that it failed: its reply can be lost Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Undo's unconfirmed restore points to where to look instead of guessing Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Offer Undo only for a delete that applied; re-send refused edits whenever their page is live again Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Don't re-send during Use cloud or Keep mine; catch up once a strategy finishes opening Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Say which choice the re-send guard covers Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Show Recently deleted only when a page is in it Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Try a failed trash read again so Recently deleted comes back online Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Read the trash again when its first page's 30 days run out Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
48 files changedLine totals unavailable