23-Jun-2026 — run resize on the integration worker, defer drop-time resize until after cropping, and... #257

build 718commit 58114fc


Worker move (the original #257 scope): Per-site 512MB Fly VMs were being OOM-killed by sharp during large album resizes, taking the whole site process down. The sharp/S3 work now runs on the 1024MB shared integration worker, mirroring the existing migration job pattern: the site dispatches a job, the worker runs it and posts progress/result callbacks, and the site relays those onto the existing WebSocket so the editor UI is unchanged.

Drop-then-crop resize UX: Dropping images into the album editor immediately compressed each whole image to the maximum size, so cropping then kept a region of an already-degraded image and the byte budget was spent on the whole frame rather than the part kept.

Resize byte-budget fix: resizeBase64Image compared the base64 string length against a budget derived straight from the byte target. Base64 is ~4/3 larger than the bytes it encodes, so a 250kb cap produced ~169kb images (roughly target x 0.74). The budget now converts bytes to base64 chars (x 4/3, with a 2% safety margin), so output lands just under the cap instead of well below it - and the dimension grow-step can use the full budget for sharper images.

Bulk-drop read resilience: Dropping many files at once read them all concurrently via Promise.all; a single FileReader NotFoundError (common when reading 50+ dropped files at once) rejected the whole batch as an unhandled error, so nothing was inserted. Reads now run in bounded-concurrency batches (6 at a time) with a single retry per file; unreadable files are skipped with a warning showing how many, instead of failing the entire drop. onFileSelectOrDropped is wrapped so it can never surface an unhandled rejection.

Editor display fixes:

Lint hardening (prevent recurrence): Both Date.now() and Object.entries() were documented as forbidden in AGENTS.md but had no AST selector, so they passed lint and were only caught at review. Audited the whole AGENTS.md forbidden table; these two were the only documented-but-unenforced rules (new Date, DateTime.now, typeof checks, Array.isArray, the loop rules and inline comments were already enforced).