Skip to content

Ask the server which images it still shows before dropping an upload - #233

Merged
SunkenInTime merged 4 commits into
icarus-cloudfrom
page-trash-client-first
Oct 1, 2026
Merged

SunkenInTime merged 4 commits into
icarus-cloudfrom
page-trash-client-first

Conversation

@SunkenInTime

@SunkenInTime SunkenInTime commented Sep 29, 2026 •

Copy link
Copy Markdown
Owner

Base icarus-cloud. First half of the page trash; #228 is the second half and stacks on this one.

Merge order

  1. Merge this PR. Deploy Web runs: Convex, then the web build.
  2. Wait until that Deploy Web run has completed successfully: the Convex deploy and the Cloudflare Pages publish of the web build. Deploy Web cancels a running deploy when a newer push starts, so merging Keep deleted pages in a 30-day trash, with Recently deleted and restore #228 early could cancel this web deploy after its Convex step.
  3. Retarget Keep deleted pages in a 30-day trash, with Recently deleted and restore #228 to icarus-cloud if GitHub hasn't, check that it shows only its own changes, then merge it. That turns the trash on.

What changes for users
Nothing today. Before the upload queue drops a pending image upload as no longer wanted, it now asks the server whether the strategy's content still shows that image. Before, it read the strategy's full snapshot and looked for the image there.

Why this ships first
#228 moves deleted pages to a 30-day trash. The trash hides them from the full snapshot, but their images are still wanted, because the page can be restored. A web build that still reads the snapshot can drop the bytes of an image that is still uploading when a teammate trashes its page. After a restore, that image shows as missing. Greptile and Astra both flagged this on #228. Convex deploys before the web build, so the live web build must stop reading the snapshot before the trash is on the server.

Server change (additive only)

  • New query images:listReferencedAssetIds({ strategyPublicId, assetPublicIds }) (viewer role). It returns which of the asked images the strategy's content shows, looking each one up in assetReferences through the strategy/asset/deleted index. Tombstoned content is left out.
  • The query answers at most 100 ids per call (MAX_REFERENCED_ASSET_IDS_PER_QUERY), so it reads at most 100 rows however large the strategy is. It returns null until the reference backfill has finished; production finished it with Serve the web beta from the production Convex deployment #206.
  • strategy:getFullSnapshot takes a new optional argument, acceptsTrashedPagesLeftOut, ignored here. It tells Keep deleted pages in a 30-day trash, with Recently deleted and restore #228's server that this client doesn't decide uploads from the snapshot. Keep deleted pages in a 30-day trash, with Recently deleted and restore #228 refuses, with CLIENT_UPGRADE_REQUIRED, a snapshot to any client that doesn't send it while the strategy holds trashed pages. A tab from before this PR's deploy can then fail an export until it reloads, but it can't drop image bytes: its upload check keeps them when the read fails.
  • No schema change. The web build live now never calls the new query and never sends the new argument.

Client change

  • ConvexStrategyRepository.fetchFullSnapshot always sends acceptsTrashedPagesLeftOut: true (export, apply-to-all-pages, video export, share-link reads all go through it).
  • ConvexStrategyRepository.fetchReferencedAssetIds asks in batches of 100. If any batch can't tell, the whole answer is null.
  • The upload queue's three reference checks now ask about exactly the uploads they are deciding on: the recheck after discarded work, dropping a stale staged job, and promoting jobs at restart.
  • If the server can't tell (null), or the query fails or is missing (for example on a deployment that lags, like dev), nothing is dropped. A staged upload (its placing change has not landed) keeps its check and its bytes and waits for a real answer. A durable upload (its change landed) may upload, as it could before when the snapshot read failed.

Correct on its own, before and after #228

  • Today's server has no trash. For the image references this client writes (an image element's data.id, a lineup link's images[*].id), the reference rows and the snapshot name the same images. Astra found two edge differences:
    • The old lineup check matched any nested id in a lineup payload, so an upload whose id equalled some lineup's entity id was kept by accident. The references count only screenshots.
    • References briefly still name images on a page an older delete removed, until its orphan purge runs. That keeps bytes; it never drops them.
  • After Keep deleted pages in a 30-day trash, with Recently deleted and restore #228, the trash keeps a trashed page's content and reference rows, so its images still count as referenced. That is what this client needs to know.
  • If this build ever ran without its server half, the query would be missing: the call throws, and the queue keeps everything.

Left for Dara

Review
Static review by Codex (GPT-6 Astra), STATIC ONLY.

  • Round 1: one blocking finding. The null-answer test claimed nothing uploads, but a durable job may. The test now uses a staged job. Two non-blocking findings:
    • The first version read every reference row, which could pass Convex's 32k read limit on huge strategies. The query is now bounded, as described above.
    • The body overstated equivalence with the snapshot check. It is corrected.
  • Round 2: no blocking findings; "good to ship". Its one note, this body being out of date, is fixed here.
  • A separate rollout pass found that tabs from before this PR could still lose bytes once Keep deleted pages in a 30-day trash, with Recently deleted and restore #228 deploys. This PR now adds the snapshot argument, and Keep deleted pages in a 30-day trash, with Recently deleted and restore #228 refuses snapshots without it. A second rollout pass confirmed the path closed.

Tests (at 79e444e)

  • npm run test:convex: 175 passed, including the new convex/referencedAssetIds.test.ts (5, the fifth a full snapshot read with the new argument):

    • answers only for images the content shows, deleted content left out;
    • refuses more than 100 ids;
    • returns null before the backfill;
    • a viewer can read it; a stranger is refused.
  • flutter test --no-pub: 1356 passed, 5 skipped. New tests:

    • test/collab/referenced_asset_ids_test.dart: asks in batches of 100, deduplicated; a null batch makes the whole answer null; full snapshots are read with acceptsTrashedPagesLeftOut.
    • test/cloud_media_upload_queue_provider_test.dart: an upload whose image the server still names is kept, with its bytes, even when the full snapshot doesn't show it. A staged upload keeps its job and bytes and is not sent while the server can't tell.

    Both queue tests fail when the queue goes back to reading the full snapshot (checked by swapping the loader back).

  • npx tsc --noEmit, npm run audit:convex-contract (46 public functions, 38 error codes). convex/function_spec.json was hand-edited in its sorted format, as Refuse lineup links whose origin or landing is gone #209/Refuse deleting a lineup origin or landing a live link still names #213 did. lib/collab/generated was regenerated with tool/icarus_convex_codegen.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Improvements
    • Media upload checks now verify only the requested asset IDs instead of loading a full strategy snapshot, reducing unnecessary data transfer.
    • Checks handle large requests in batches and defer decisions when reference information is unavailable, helping preserve uploads until their status is confirmed.
  • Reliability
    • Added coverage for access permissions, deleted or unreferenced images, batch limits, and pending upload recovery.

RetriggerConfidence Score: 4/5

Do not merge until snapshot requests work with older deployments, or the updated server contract is deployed before this client.

Findings

  1. P1 Older servers reject snapshots ▶
  2. P1 Stacked image calls fail ▶

Summary

The upload queue now asks the server which images remain referenced instead of relying on a full snapshot. Snapshot requests also send a new argument that older server deployments reject, preventing snapshot-dependent actions during a client/server rollout.

Reviews (2) · Last reviewed commit: "Say, when reading a full snapshot, that ..."

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>
@coderabbitai

coderabbitai Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: f1cdd89b-e90d-4f61-a68b-faa0296b89b2

📥 Commits

Reviewing files that changed from the base of the PR and between 3cbd0f7 and d5f9209.

⛔ Files ignored due to path filters (2)
  • lib/collab/generated/convex_models.dart is excluded by !**/generated/**
  • lib/collab/generated/icarus_convex_api.dart is excluded by !**/generated/**
📒 Files selected for processing (8)
  • convex/function_spec.json
  • convex/images.ts
  • convex/referencedAssetIds.test.ts
  • lib/collab/convex_strategy_repository.dart
  • lib/providers/collab/cloud_media_upload_queue_provider.dart
  • test/cloud_media_upload_queue_provider_test.dart
  • test/collab/referenced_asset_ids_test.dart
  • test/web_media_bytes_upload_test.dart

Included review availability: This review used your included allowance. Your plan provides up to 1 included review per hour; 0 remain after this review.


📝 Walkthrough

Walkthrough

The change adds a Convex query for referenced asset IDs, a client method that batches requests, and upload-queue checks that use those results instead of fetching full strategy snapshots.

Changes

Referenced asset lookup

Layer / File(s) Summary
Server query and access contract
convex/function_spec.json, convex/images.ts, convex/referencedAssetIds.test.ts
Adds a viewer-authorized query that accepts up to 100 asset IDs. It returns null before backfill readiness; otherwise, it returns sorted IDs with non-deleted references. Tests cover results, request limits, readiness, and access.
Client batching and result aggregation
lib/collab/convex_strategy_repository.dart, test/collab/referenced_asset_ids_test.dart
Adds a repository method that deduplicates IDs, fetches batches of up to 100, and combines results. It returns null if any batch result is null. Tests verify batching and null handling.
Upload queue reference checks
lib/providers/collab/cloud_media_upload_queue_provider.dart, test/cloud_media_upload_queue_provider_test.dart, test/web_media_bytes_upload_test.dart
Replaces full-snapshot reference checks with requested-ID lookups for staged admission, missing-source cleanup, and job reconciliation. Tests cover the loader change, referenced and unknown results, pending bytes, and recovery behavior.

Priority: ➖ Normal

Estimated code review effort: 3 (Moderate) | ~20 minutes

Change: Bug fix

Sequence Diagram(s)

sequenceDiagram
  participant UploadQueue as Cloud media upload queue
  participant ReferenceLoader as CloudMediaReferenceLoader
  participant Repository as ConvexStrategyRepository
  participant Convex as images:listReferencedAssetIds
  UploadQueue->>ReferenceLoader: Request references for asset IDs
  ReferenceLoader->>Repository: Fetch referenced IDs
  Repository->>Convex: Query batches of up to 100 IDs
  Convex-->>Repository: Return referenced IDs or null
  Repository-->>ReferenceLoader: Return combined IDs or null
  ReferenceLoader-->>UploadQueue: Return lookup result
Loading

Merge Risk: ⚪ Minimal · up to d5f92

The referenced-image lookup appears ready to merge after normal checks. Unavailable answers preserve staged uploads rather than treating their images as unreferenced.

Security Architecture Review

Security architecture risk: 🟡 Moderate · up to d5f92

The new lookup limits the data fetched and defers deletion when the server cannot answer. Image retention still depends on the lookup’s accuracy and on completing this web deployment before the planned trash rollout. No introduced security vulnerability was established.

Retained concerns
No architecture-level concerns identified.

Security review details

Security Blast Radius

  • inferred — An authenticated viewer can ask about up to 100 supplied IDs per call, but results are limited to references in a strategy for which that user has an effective viewer role. The inspected path does not expose cross-strategy reference rows.

Trust Boundaries and Controls

  • observed — The viewer check obtains the caller identity and effective strategy role server-side. Unlike the snapshot’s share-token-aware read path, this query has no share-token argument; the inspected authorization path therefore does not broaden access to anonymous share-link holders.

Resilience and Maintainability Implications

  • inferred — Deletion still relies on a prior server-reference answer: local account and job checks do not make that answer atomic with local byte removal. This limits what a negative lookup proves under concurrent content changes, although the supplied comparison does not establish a newly introduced race.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 5 functions across 2 files. (6 skipped: 6… Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: querying the server for referenced images before removing uploads.
Full details: Docstring Coverage

Explanation

Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 5 functions across 2 files. (6 skipped: 6 unsupported.)

  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

SunkenInTime and others added 2 commits September 29, 2026 09:30
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>
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>
@SunkenInTime

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Sep 29, 2026 •

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Comment thread convex/images.ts
export const listReferencedAssetIds = query({
args: {
strategyPublicId: v.string(),
assetPublicIds: v.array(v.string()),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Stacked image calls fail

PR #228 still calls images:listReferencedAssetIds with only strategyPublicId, including from its repository method and page-trash tests. Requiring assetPublicIds rejects those calls before the query runs, so the stacked client cannot check image references. Update PR #228’s callers and generated contract before merging the stack.

Artifacts

Higher-PR caller and candidate validator source

  • Captured the executed git-show commands identifying the one-argument caller and the new required validator field, establishing the contract mismatch.

Query test against higher PR before the change

  • Ran the higher PR’s query test against its own revision; the one-argument call passed.

Same query test against candidate PR after the change

  • Ran the unchanged higher-PR test against the candidate revision; validation rejected the missing `assetPublicIds` field.

View artifacts

T-Rex Ran code and verified through T-Rex

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right that the stack must move together, and it already has. #228 (branch page-trash) merged this change in acdcb33: its repository method passes assetPublicIds in batches of 100, its tests pass them, and lib/collab/generated was regenerated from the updated function_spec.json. The one-argument calls you saw are at f3eec36, which predates that merge. No shipped client ever called the one-argument form: the query is new in this PR, and the live web build doesn't call it.

@greptile-apps

greptile-apps Bot commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

Comments Outside Diff

These findings could not be posted inline.

  • P1 New snapshot argument breaks clients connected to a legacy Convex deployment ▶

    • Bug
      • When the updated client reaches a server running the previous query contract, strategy:getFullSnapshot rejects its request. Cloud strategy export, cloud video export, and the cloud settings operation that call fetchFullSnapshot cannot proceed on that version-skew path.
    • Cause
      • fetchFullSnapshot unconditionally sends acceptsTrashedPagesLeftOut: true, while the previous server’s argument validator does not define that field; the repository does not retry without it.
    • Fix
      • Deploy the server contract before clients send the field, or provide a narrowly scoped compatibility fallback for the legacy argument-validation error.

… 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>
SunkenInTime added a commit that referenced this pull request Sep 29, 2026
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>
Comment on lines +199 to 202
// This client checks image references apart, so it can take a
// snapshot without the pages in the server's trash.
acceptsTrashedPagesLeftOut: const ConvexOptional.present(true),
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Older servers reject snapshots

If this client connects to a deployment that has not yet accepted acceptsTrashedPagesLeftOut, fetchFullSnapshot sends an argument the server rejects and does not retry without it. Cloud strategy export, video export, and the all-pages settings action then cannot complete. Deploy the server contract first or handle the older contract.

Artifacts

Local version-skew validation command

  • The authored command reconstructs the previous query from Git, runs both local contract checks and the client check, then removes the temporary copy; it provides a reproducible safe run.

Previous and current server contract test

  • The authored test sends the same new-client arguments to the previous and current query validators; it isolates the version-skew difference.

Local contract test configuration

  • The authored configuration selects only the version-skew test in the local Convex test environment; no deployment is involved.

Previous server rejects the new argument

  • A local run against the previous query contract succeeded without the field and rejected the new-client request with an unexpected-field validator error; the legacy contract is incompatible.

Current server accepts the new argument

  • A local run sent the same request to the current query and received a one-page snapshot; the updated contract accepts it.

Dart repository retry test

  • The authored test exercises `fetchFullSnapshot` against a transport returning the observed validation failure; it checks whether the client retries.

Dart repository propagates rejection after one attempt

  • The executed Dart test records the sent arguments, propagated exception, and one query attempt; there is no fallback retry.

Complete local version-skew execution

  • The captured command, working directory, exit code, and output cover both server-contract runs and the Dart retry check; all checks reproduced the compatibility failure without deployment.

View artifacts

T-Rex Ran code and verified through T-Rex

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Production can't hit this: Deploy Web deploys Convex before it publishes the web build, and a cancelled run can leave a new server with the old web, never the reverse. This PR's server accepts the argument before any client sends it. The only backend that lags is the dev deployment (majestic-eel-413), which CI doesn't deploy. It would also be missing images:listReferencedAssetIds, so it has to be redeployed from icarus-cloud before a dev build uses it; both PR bodies say so. I haven't added a retry without the argument. The reason isn't that a flagless read would lose data: against a server that predates the trash it is harmless. It's that telling a validation error apart from other failures means matching error text, and the only case it covers is the dev deployment.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That rollout constraint addresses the finding. Deploy Web publishes the Convex contract before the client bundle, so production cannot receive this client while still serving a server that rejects acceptsTrashedPagesLeftOut; a cancelled run can only leave the new server with the old client, which is compatible.

The lagging majestic-eel-413 deployment is an explicitly known dev-only case that also lacks images:listReferencedAssetIds and must be redeployed before using this build. Given that scope, adding a retry based on matching validation-error text would be brittle and is not necessary. I’m withdrawing this finding.

@SunkenInTime
SunkenInTime merged commit d56ab72 into icarus-cloud Oct 1, 2026
14 checks passed
SunkenInTime added a commit that referenced this pull request Oct 1, 2026
…re (#228)

* 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>
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.

1 participant