Copy images when duplicating a cloud strategy (#197)

* Copy images when duplicating a cloud strategy

Duplicating a cloud strategy now runs as one server mutation,
strategies:duplicate. It copies pages, page settings, live elements and
lineups under fresh publicIds, and gives the copy its own imageAssets row
for each image it shows, pointing at the same R2 object. The cleanup
worker already deletes bytes only when no other row points at them, so
deleting either strategy leaves the other's images intact.

The client previously rebuilt the copy op by op and never created asset
rows, so the copy's placed images and lineup images had nothing to
render, and deleting the original deleted their bytes.

A duplicate is refused while an image is still uploading (client and
server), and failures now show a message instead of throwing silently.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Show "Image unavailable" for a cloud image with no asset

A cloud image whose asset row did not exist showed "Syncing image"
forever: a copy's image before the duplicate fix, or an image whose
asset was cleaned up. The page snapshot is a live query over the
page's assets, so once it has loaded, a missing row means no bytes are
on the server and no upload has started anywhere. Such an image now
shows "Image unavailable", unless this device still has its upload
queued. A pending asset and a page still loading keep the spinner.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Expect images that land before their upload

An image element (or lineup image) and its upload reach the server
independently. When the content landed first, nothing recorded that an
image was coming: a duplicate made meanwhile copied no asset and
succeeded without the image for good, and other devices showed "Image
unavailable" for an image that was only slow.

applyBatch now gives each image that accepted content newly shows a
pending placeholder asset row when nothing exists for it. The upload
intent adopts the placeholder, deleting the image element drops it, and
the stale-upload sweep removes it if the upload never comes, after which
the image reads as unavailable. Duplicate already refuses while an
image is pending, so it now waits for these too.

Also from review:
- a duplicated lineup group's agent and abilities point at the copy's
  group id;
- duplicating while browsing someone else's folder lands in the
  caller's library root instead of failing;
- a missing asset stays "Syncing image" until the upload queue can
  answer (account known, outbox readable), so startup does not flash
  "Image unavailable".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
ad2cc41a43Dara Adedeji committed on 9/25/2026, 11:38:53 PM· committed by GitHubparent478f311
15 files changedLine totals unavailable