Building the Sailor Board: Photo Gallery, Upload Gating, and Real-Time Instagram Integration for Charter Guest Pages
What Was Done
Over this session, we stabilized and expanded the guest photo upload system for charter events—specifically Keely's afternoon sail on 2026-05-24. The work involved:
- Verified all four upload-flow features on the live guest page (
https://queenofsandiego.com/g/2026-05-24-keely-afternoon) - Confirmed event-code gating (spam prevention via shared secret)
- Validated 24-file batch upload limits and MIME type whitelisting
- Traced Instagram hashtag search integration (#jada and #queenofsandiego) from client through Lambda
- Fixed "Book a Sail" modal auto-open behavior (was returning to homepage instead of launching modal)
- Created the
sailor-board/directory structure and deployed a standalone sailor-board index page - Built
booking-widget.jsas an embeddable, self-contained booking modal for charter pages - Backfilled thumbnail images for existing event photos using local Python + Pillow
Technical Architecture: Photo Upload Flow
The guest page photo upload system spans three layers:
1. Client-Side Upload Handler
File: /Users/cb/Documents/repos/sites/queenofsandiego.com/g/2026-05-24-keely-afternoon/index.html (deployed to S3)
- Input element:
#file-input(line 362) accepts multiple files with MIME type filter:image/jpeg, image/png, image/webp, image/heic, image/heif, video/mp4, video/quicktime, video/x-msvideo, video/webm - Batch limit:
Array.from(files).slice(0, 24).forEach(uploadFile)(line 515) enforces strict 24-file-per-session cap - Upload endpoint: Presigned POST to S3 bucket (computed via
GET /api/presignLambda endpoint) - Event code field:
#g-codeinput (line 354) — optional secret that bypasses moderation queue. If code matches event secret, uploads publish immediately; otherwise they enterpending_reviewstate in DynamoDB
2. Presign Lambda
File: /Users/cb/Documents/repos/sites/queenofsandiego.com/tools/shipcaptaincrew/lambda_function.py (lines ~1800–1900)
This function handles GET /api/presign requests. It:
- Validates the event exists and is in post-sail window (after
FLIP_UTCtimestamp) - Generates S3 presigned POST URLs valid for 15 minutes
- Returns JSON with bucket, prefix, credentials, and policy document to client
- CORS requirement: S3 bucket CORS config was updated to permit POST requests from
https://queenofsandiego.comorigins (no wildcard; specificity prevents accidental cross-origin leakage)
3. Post-Upload Metadata Capture
After S3 PUT succeeds, the browser POSTs to /api/upload-complete Lambda with metadata:
- S3 object key (path where file landed)
- Event ID
- Event code (if provided)
- MIME type, file size, dimensions (for images)
Lambda then:
- Stores record in DynamoDB table
events_photoswithevent_idas partition key,photo_id(UUID) as sort key - Sets
status = 'approved'if event code matches, elsestatus = 'pending_review' - Records upload timestamp and uploader IP (for moderation audit trail)
Instagram Hashtag Integration
The guest page displays a live Instagram feed of posts tagged #jada or #queenofsandiego from the event day.
Client-side rendering: Lines 452–459 in the guest HTML iterate over d.instagram array and render thumbnails into #ig-grid.
Server-side population: When the page loads, it calls GET /api/g/{event_id}/photos Lambda endpoint. The Lambda function (in lambda_function.py) queries Instagram's hashtag API (credentials stored in Lambda environment variables—not in code) for posts from the event date, filters by hashtag, and returns JSON with image URLs, captions, and links.
Caching strategy: Instagram results are cached in DynamoDB with a 1-hour TTL to avoid rate limits and reduce API calls.
Event Code Gating (Spam Prevention)
The event code is a pre-shared secret distributed to charter guests (usually in the confirmation email). On the guest page:
- Code is optional (guests can upload without it, but their photos enter review queue)
- If code is correct, upload bypasses moderation and publishes instantly
- If code is wrong or absent, upload is flagged for crew review before appearing on the sailor board
Storage: Event codes are stored in DynamoDB events table, field upload_secret, hashed with SHA-256 (never stored plaintext).
Comparison: Client sends plaintext code; Lambda hashes it and compares digest against stored value.
Thumbnail Generation and Backfill
Images are stored at full resolution in S3. To serve mobile and gallery previews efficiently, we generate thumbnails (200px, 400px widths).
Backfill script: /tmp/backfill-thumbs.py
This local Python script:
- Queries DynamoDB
events_photostable for all photos missing thumbnail records - Downloads full-resolution image from S3 bucket (name:
queenofsandiego-photos-prod) - Uses Pillow to resize: 200px and 400px widths, preserving aspect ratio
- Uploads thumbnails back to S3 with keys like
g/2026-05-24/{photo_id}_thumb_200.jpg - Updates DynamoDB record with thumbnail metadata (size, dimensions)
Runtime: ~2–3 seconds per photo on standard broadband. For Keely's event (22 photos), runtime was ~60 seconds total.
Sailor Board and Booking Widget