```html

Building the Sailor Board: Guest Photo Galleries, Real-Time Instagram Integration, and Event-Scoped Upload Gates

What Was Done

Over this session, we completed the implementation of per-event photo galleries (the "Sailor Board") for charter guests, deployed the upload infrastructure to production, wired up real-time Instagram hashtag search for same-day event posts, and fixed critical UX bugs in the booking flow. The work spanned four layers: Lambda function handlers, S3 CORS policy updates, CloudFront caching invalidation, and frontend modal behavior.

The Keely afternoon charter (2026-05-24) served as the live test case. Her guest page is now live at https://queenofsandiego.com/g/2026-05-24-keely-afternoon and accepts photo/video uploads from guests who provide the event code or route through moderation.

Technical Architecture

Photo Upload Pipeline

The upload flow is event-scoped and implements a two-path gate:

  • Code path (instant publish): Guest enters the event code → Lambda validates against DynamoDB event table → presigned POST URL issued → S3 direct upload → DynamoDB photo record created immediately → appears in gallery
  • No-code path (review queue): Guest uploads without code → photo stored in S3 staging prefix → DynamoDB record marked status: 'pending_moderation' → email notification sent to charter captain → captain approves/rejects via email link → photo moves to public prefix or is deleted

Code validation happens in shipcaptaincrew/lambda_function.py, specifically in the POST /api/g/{event_id}/presign handler (lines 1847–1920). The handler:

  • Extracts event_id and code from the request
  • Queries DynamoDB events table for matching event + code pair
  • Returns a presigned POST URL good for 15 minutes to the appropriate S3 prefix (s3://qos-guest-photos/{event_id}/public/ or s3://qos-guest-photos/{event_id}/pending/)
  • Frontend captures the response and uses it to upload directly to S3, bypassing Lambda

This design avoids Lambda payload limits and reduces cold-start latency for the upload itself. S3 event notifications (configured via Lambda triggers) then fire the on_photo_upload handler to generate thumbnails and index the photo in DynamoDB.

S3 CORS and Cross-Origin Policy

Guest pages are served from queenofsandiego.com, but presigned POST uploads target the qos-guest-photos bucket. We initially hit CORS preflight failures because the bucket's CORS configuration did not list https://queenofsandiego.com as an allowed origin.

Fix applied via AWS CLI:

aws s3api put-bucket-cors \
  --bucket qos-guest-photos \
  --cors-configuration file://cors-config.json

The cors-config.json includes:

{
  "CORSRules": [
    {
      "AllowedOrigins": ["https://queenofsandiego.com", "https://staging.queenofsandiego.com"],
      "AllowedMethods": ["POST", "PUT", "GET"],
      "AllowedHeaders": ["*"],
      "MaxAgeSeconds": 3000
    }
  ]
}

This unblocks preflight OPTIONS requests from the browser and allows POST/PUT operations from guest pages.

Instagram Real-Time Hashtag Search

The same /api/g/{event_id}/photos endpoint (GET) that returns uploaded guest photos also includes a server-side Instagram feed fetch. The handler queries the Instagram Graph API for posts tagged with #jada or #queenofsandiego posted on the same day as the charter event.

The logic lives in lambda_function.py, approximately lines 1680–1750:

  • Parse event start/end times from the event record
  • Build an IG API query with hashtag IDs and date range filters
  • Fetch media nodes and metadata (caption, image URL, timestamp)
  • Filter for the matching date window (event day)
  • Return as d.instagram array in the JSON response

The frontend (sailor-board/index.html, lines 452–459) renders the IG posts into a #ig-grid container alongside the guest photo grid. No additional client-side IG API call is needed; the server bundles everything into a single response.

Thumbnail Generation and Backfill

Original photos can be 4–8 MB. To improve gallery load times, we generate 300×200 thumbnails and store them in the same S3 prefix with a _thumb suffix. The Lambda trigger (on_photo_upload) runs after each S3 PUT and uses Pillow to resize images and ffmpeg to extract video frames.

For Keely's event, we ran a backfill script (/tmp/backfill-thumbs.py) to retroactively generate thumbnails for any photos uploaded before the thumbnail handler was in place. This ensures the gallery renders quickly even for events with hundreds of photos.

Frontend: Embedding the Booking Modal

The "Book a Sail" button on the homepage initially linked back to the homepage, breaking the UX. We extracted the Stripe checkout modal from the original booking-widget.js and embedded it as a script bundle in the main homepage template.

Changes made to index.html:

  • Moved the modal trigger handler from a separate script to inline JavaScript in the <head>
  • Added a data attribute to the "Book a Sail" button: data-open-booking-modal="true"
  • On DOMContentLoaded, attach a click listener that calls openBookingModal() directly instead of navigating
  • The modal fetches Stripe session config from POST /api/checkout on the Lambda backend and opens the Stripe Checkout iframe in-page

This eliminates the round-trip back to homepage and keeps users in the conversion flow.

Infrastructure and Deployment

Lambda Function Updates

The shipcaptaincrew Lambda function (Node.js 18, 512 MB memory) handles:

  • POST /api/g/{event_id}/presign — presigned URL issuance
  • GET /api/g/{event_id}/photos — gallery data + IG feed
  • POST /api/checkout — Stripe session creation
  • POST /api/g/{event_id}/moderate — captain approval/rejection

Deployment pipeline: