A figure outlives the deck it was drawn for: the deck gets replaced, the diagram
is still good. Until now a generated image could only be seen in the resource it
was made for, and there was no way to find one again or to delete it.
Library → Images is a grid of every finished image the account owns, across all
three workflows, newest first. GET /api/generated-images is scoped by owner_id
in the statement rather than filtered after, returns only finished jobs — an
unfinished one is a broken frame in a gallery — and pages by keyset, because a
gallery that grows while you scroll repeats or skips rows under OFFSET.
Most of this already existed. Thumbnails were already rendered at 256 and 640 by
sharp and already served by ?w=, with their own checksum so the client's
tamper check passes on a derived copy; hydrateImage already handles auth, the
account boundary and caching. The tiles ask for the 256px preview, so thirty of
them cost a few kB each rather than thirty full-size downloads, and the prompt
is decrypted for the caption because it is the only human-readable label an
image has.
Deleting needed new work. The storage interface had no remove at all, so a
delete that dropped the row would have left the object and both previews in the
bucket — paid for, and still readable by anything with credentials. Storage now
removes all three keys, and the bytes go before the row: a row pointing at a
missing object is a broken image in a gallery, while an object without its row
is only wasted space, and unreachable storage refuses the delete outright rather
than reporting a success that left the picture behind.
THUMB_WIDTHS now has one definition, in generatedImageStorage. Two copies drift,
and the drift that matters is a width that gets written and never deleted.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dv6sqaY6Vq3ChZHMem3cnU