* fix(preview): generate lightbox previews for external/reference photos (#1078)
ensurePreviewImage() resolved its source only via resolvePhotoStorageKey(),
which returns null for external/reference photos by design — those live on a
media mount outside the managed storage tree. The null went straight into
withLocalCopy(), which throws ("LocalFsStorage: invalid relative path: null"),
so the preview route fell back to redirecting at the full-size original. A
gallery whose photos are all external got no benefit from the preview tier
(#492) at all: guests paid 5-12 MB on every lightbox open, with nothing
surfaced in the admin UI.
Add the external branch ensureThumbnail() has had since #423: resolve via
resolvePhotoFilePath() and feed the mount path to generatePreviewImage()
directly, with an ext<id>_ output basename so two events referencing the same
NAS filename can't clobber each other's preview.
Also close the adjacent hole that made the failure a throw rather than the
documented null: a row with no source_origin in a reference-mode event takes
its mode from the event, so resolvePhotoStorageKey returns null for it too.
Return null instead of handing that to withLocalCopy.
Claude-Session: https://claude.ai/code/session_01Ra4hcsYiKuQLbbRsg6EjAc
* fix(preview): select the columns the external branch needs on bulk regenerate
POST /api/admin/thumbnails/regenerate-previews selected only id, event_id,
path, media_type, mime_type and preview_path, so photo.source_origin was
undefined by the time ensurePreviewImage branched on it. Every external row in
a reference gallery took the managed path, resolvePhotoStorageKey returned null
for it, and the endpoint reported success while generating nothing.
Add source_origin, external_relpath and filename to the select, plus a
source-inspection test pinning the caller contract and a service-level test
showing a column-starved row is indistinguishable from a managed one.
Claude-Session: https://claude.ai/code/session_01Ra4hcsYiKuQLbbRsg6EjAc
* style(test): single-quote the source-inspection needles
Matches the repo eslint quotes rule (no avoidEscape) by dropping the nested
quotes from the search strings rather than escaping them.
Claude-Session: https://claude.ai/code/session_01Ra4hcsYiKuQLbbRsg6EjAc
---------
Co-authored-by: Paul Nothaft <[email protected]>