Addresses @felixbrandt — fix(backend): "the two try blocks in generate()
overlap — a save failure logs 'generation failed' even though the
thumbnail is already in S3 as an orphan".
generate() now orchestrates four stages, each in its own try+log:
readSourceImage / encodeThumbnail / uploadToStorage / persistThumbnailMetadata
persistThumbnailMetadata emits the distinct "orphaned in storage as <key>"
log line so an operator can see database-side failures after the upload
completed. The deterministic key ensures the next run overwrites cleanly,
so the orphan is self-healing.
Also extracts THUMBNAIL_KEY_PREFIX/SUFFIX constants with a comment
explaining the deterministic-overwrite contract.
Adds test: generate_returnsFailed_whenPersistThrows_butUploadSucceeded.
Refs #307
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>