```html

Building the Sailor Board: Real-Time Photo Galleries for Charter Events

Over the past development session, we completed the infrastructure and frontend implementation for per-event photo galleries—what we're calling the "Sailor Board"—deployed live on Keely's May 24 afternoon charter. This post walks through the architectural decisions, deployment pipeline, and cross-service integration that powers guest photo uploads, moderation, and real-time display.

What Was Built

The Sailor Board is a per-event guest page that lives at /g/{YYYY-MM-DD}-{guest-name}-{time-of-day} (e.g., https://queenofsandiego.com/g/2026-05-24-keely-afternoon). It serves two flows:

  • Pre-sail: Event countdown and CTA to book additional sails
  • Post-sail (5 PM PT event-day cutoff): Photo/video upload UI, real-time Instagram hashtag feed (#jada, #queenofsandiego), and moderation workflow

Key constraints implemented:

  • 24-photo-at-once upload cap (line 515 in sailor-board/index.html: Array.from(files).slice(0, 24))
  • Event code gating (spam prevention; no code → review queue)
  • Accepted formats: JPEG, PNG, WebP, HEIC, HEIF, MP4, MOV, AVI, WebM
  • Same-day Instagram hashtag integration via Lambda enrichment

File Structure and Deployment Paths

New files created:

  • /Users/cb/Documents/repos/sites/queenofsandiego.com/sailor-board/index.html — Guest-facing page; 14 iterative edits during this session
  • /Users/cb/Documents/repos/sites/queenofsandiego.com/booking-widget.js — Extracted Stripe checkout logic into a reusable widget
  • /tmp/backfill-thumbs.py — Thumbnail generation backfill utility for existing photos

Modified files:

  • tools/shipcaptaincrew/lambda_function.py — 7 edits to wire up photo upload presigning, Instagram feed enrichment, and moderation workflows
  • index.html (site root) — 2 edits to add navigation tokens and "Book a Sail" deep-link modal opening

Infrastructure and API Integration

Lambda Function: shipcaptaincrew

The core upload and feed logic lives in tools/shipcaptaincrew/lambda_function.py. Key endpoints:

  • POST /api/g/{event_id}/presign — Issues S3 presigned URLs for multipart upload. Validates event code (if required) and rate-limits by IP.
  • GET /api/g/{event_id}/photos — Returns gallery metadata: user-uploaded photos from DynamoDB + same-day Instagram posts matching #jada or #queenofsandiego hashtags (fetched via Instagram Graph API during request).
  • POST /api/g/{event_id}/confirm — Marks uploads as confirmed; triggers Lambda invocation for thumbnail generation and moderation notification emails.

S3 Bucket Strategy

Guest uploads land in a private staging bucket pending moderation. On approval, they're copied to the public gallery bucket. CORS configuration was updated to allow cross-origin requests from https://queenofsandiego.com (and staging/dev origins) to support presigned URL uploads from the browser.

Thumbnail generation runs asynchronously via Lambda; backfill was necessary for photos uploaded before thumbnail support existed. The backfill-thumbs.py script iterates DynamoDB, generates missing thumbnails via Pillow, and uploads them to S3 with metadata updates.

DynamoDB Schema

Photo records include:

  • event_id (partition key)
  • upload_ts (sort key, ISO 8601)
  • guest_email, guest_name
  • s3_key, file_size, content_type
  • thumb_key (populated post-generation), thumb_size
  • moderation_status (pending, approved, rejected)
  • source (user_upload, instagram)

Frontend: Sailor Board Page Structure

The page is a single-file HTML app (~22.5 KB deployed) with embedded CSS and JavaScript. Architecture:

  • #hero — Pre-sail countdown and booking CTA
  • #upload-section — Post-sail file picker, event code input, progress tracking
  • #photo-grid — Renders both user uploads and Instagram feed in chronological order
  • #ig-grid — Instagram posts (rendered inline in the photo grid)

Key JavaScript Functions

  • readCode() (line 424) — Validates event code; stores in sessionStorage. No code → uploads go to review queue.
  • handleFiles() (line 514) — Accepts up to 24 files; validates MIME types; initiates multipart uploads.
  • uploadFile(file) — Calls presign endpoint, streams chunks to S3, confirms on completion.
  • refreshPhotos() — Polls GET /api/g/{event_id}/photos every 10 seconds post-sail.

Event Mode Flip

The page checks FLIP_UTC (event date at 5 PM PT = 00:00 UTC next day) to switch from pre-sail to post-sail UI. In production, this is hardcoded per event during page generation; dynamic events would require a metadata endpoint.

Booking Widget Extraction and Modal Deep-Linking

During this session, we isolated the Stripe checkout logic into booking-widget.js to enable reusable booking modals. The root index.html was updated so "Book a Sail" buttons immediately invoke the modal rather than navigating away.

Implementation:

  • Modal auto-open on page load if URL contains ?open-booking=true
  • All "Book a Sail" links now pass this query param instead of href to external booking page
  • Deployed to staging CloudFront distribution; invalidated cache to pick up changes

Deployment Pipeline

Lambda Deployment