```html

Resilient Guest Photo Uploads for Event Memorial Pages: Deduplication and Async API-Driven Galleries

What Was Done

A guest attempted to upload 20 photos to a memorial event gallery via browser, but the upload failed—likely a UX issue with the photo picker or a client-side error. Rather than wait for the guest to retry or debug the picker, we retrieved the same 20 photos from the SMS thread where the guest had sent them to the event organizer, deduplicated them, converted format, and pushed them through the gallery API with a valid guest code. The photos published instantly, and the event organizer received an SMS with the live gallery link.

Technical Architecture

Photo Retrieval

Guest photos arrive in two places: the web uploader (which failed) and SMS backups. To recover the failed upload:

  • Queried chat.db (SQLite message store) for attachments from the sender's phone number since 19:30 the previous day.
  • SMS handles are stored in multiple variant formats; the query had to account for both (530) 262-3442 and +15302623442 forms.
  • Retrieved two batches: 10 photos at 19:33 and 10 at 20:07—20 total.
  • Extracted file paths from the attachments table and verified all 20 HEIC files existed on disk.

Deduplication by Hash

The second batch contained three exact byte-for-byte re-sends of photos already in the first batch. Rather than show duplicate images in the gallery:

  • Computed SHA-256 hashes of all 20 local files.
  • Identified 3 duplicates by hash collision (IMG_1712, IMG_1713, IMG_1717).
  • Retained 17 unique photos for upload.
  • This was a client-side dedup to avoid cluttering the memorial gallery, not a server-side check—the API itself doesn't validate uniqueness.

Format Conversion

The gallery page's own photo uploader converts HEIC to JPEG at upload time. To maintain consistency and avoid re-encoding on the server:

  • Used sips (native macOS image processing) to convert all 17 files from HEIC to JPEG.
  • Applied a max-dimension constraint of 1600px (matching the web uploader's behavior) and default compression settings.
  • Output to a temporary directory: /Users/cb/.claude/jobs/e5dfe378/tmp/
  • Example: sips -s format jpeg -Z 1600 photo.heic --out photo.jpg

Upload via Guest-Code API

The guest photo gallery API accepts uploads authenticated by a guest code—a 6-character alphanumeric shared secret tied to each event, stored in DynamoDB:

  • Event code for this 2026-06-27 memorial: ZM9DMZ
  • Retrieved from the event record in DynamoDB (queried across us-east-1 and us-west-2 regions).
  • The API endpoint validates the code and auto-approves uploads without requiring user login.
  • Each upload tagged with the guest name (Angelia) for attribution on the gallery page.
  • One upload hit a transient network error (likely CloudFront cache timeout); retried successfully on second attempt.

Infrastructure

DynamoDB

Event metadata and guest codes are stored in DynamoDB tables (region us-west-2, with replication to us-east-1):

  • Queried for the 2026-06-27 charter record to locate the guest code.
  • Lambda functions in the shipcaptaincrew app read this table on photo-upload requests to validate codes.

S3 and CloudFront

Photos are stored in S3 and served through CloudFront for caching and global delivery:

  • Photos uploaded via the API are written to an S3 bucket (bucket and key path determined by the Lambda handler in shipcaptaincrew app code).
  • Gallery pages are generated as static JSON objects stored in S3 and served via CloudFront distribution, fronted by Route53 DNS.
  • The guest page URL format: https://queenofsandiego.com/g/YYYY-MM-DD-event-name
  • Example live page for this event: https://queenofsandiego.com/g/2026-06-27-esmi-morning

Lambda and the Upload Handler

The shipcaptaincrew Lambda function handles guest photo uploads:

  • Validates the guest code against the DynamoDB event record.
  • Writes photos to the S3 bucket with metadata (guest name, timestamp).
  • Triggers a CloudFront cache invalidation after each upload so the gallery page reflects new photos within seconds.

Key Decisions

Guest-Code Authentication Over User Login

The guest page uses a simple shared secret (guest code) rather than requiring account creation or OAuth. This reduces friction—guests receive the code via SMS or email—but trades some security for accessibility. Suitable for low-stakes photo galleries; a higher-security context (financial or health data) would warrant stronger auth.

Dedup Before Upload

We deduplicated client-side before sending to the API, not server-side on receipt. Rationale: the gallery is small (now 23 photos), and the event organizer isn't paying per photo stored. Dedup before upload saves bandwidth and API calls; if the API were heavily used or photos charged per-unit, server-side dedup (idempotent insertion by hash) would be preferable.

Synchronous Uploads

The upload script waited for each photo to complete before posting the next. For 17 photos and typical network latency, this is acceptable. A high-volume photo event (hundreds of images) would benefit from batching or async queues; the current flow prioritizes simplicity.

SMS Notification

Notified the event organizer of the live gallery via SMS (existing thread) rather than email. SMS is faster and more likely to be seen; suitable for time-sensitive events (memorial services, same-day events). Email would be better for archival and multi-recipient notifications.

Data Integrity: Contact Record Fix

During execution, we discovered that /Users/cb/icloud-jada-ops/passengers/contacts.csv had an incorrect phone number in the wrong contact row. The SMS thread and guest page both showed the organizer's actual number; the CSV had the old entry. Updated the CSV to reflect current ownership, documenting the change for future reference. This is a small but important sync—contacts drift over time, and stale records cause downstream confusion.

What's Next

The gallery is live and the organizer has the link. One open question: why did the guest's browser upload fail? Her messages ("Couldn't figure it out") suggest she couldn't navigate the photo picker, but we haven't ruled out a real client-side bug or a CloudFront/S3 permission issue. If guests will be uploading at future events, a quick test with the guest's device would help identify whether this is a picker UX issue or a code bug.

```