```html

Building the Sailor Board: Guest Photo Gallery & Real-Time Instagram Integration for Charter Events

Overview

During this session, we completed the implementation and deployment of the Sailor Board — a per-charter guest photo gallery with real-time Instagram hashtag integration, anti-spam gating via event codes, and batch upload capability. The feature went live on Keely's charter event page (https://queenofsandiego.com/g/2026-05-24-keely-afternoon) on May 25, 2026, and we've now formalized the architecture, fixed critical booking modal behavior, and documented the full stack for future events.

What Was Built

  • Guest upload flow: Authenticated file handling with 24-file-at-once batch support, media validation (JPEG, PNG, WebP, HEIC, HEIF, MP4, MOV, AVI, WebM), and dual-path publishing: instant via event code, or moderation queue for anonymous uploads.
  • Per-charter photo gallery: CloudFront-cached HTML page served from S3 at /g/{event_id}/index.html with client-side rendering of photos, videos, and synchronized Instagram feed.
  • Instagram real-time pull: Lambda-driven hashtag search (#jada, #queenofsandiego) filtered to same-day posts, served via API endpoint GET /api/g/{event_id}/photos.
  • Anti-spam gating: Per-charter event codes distributed to legitimate guests; code holders skip moderation. Non-code uploads queued for review by charter operators.
  • Booking modal deep-link fix: "Book a Sail" CTA now triggers immediate modal open instead of homepage navigation.

Technical Architecture

File Structure & Deployment Targets

/Users/cb/Documents/repos/sites/queenofsandiego.com/
  ├── sailor-board/index.html          # Per-charter guest page template
  ├── index.html                        # Homepage (updated nav + Book CTA)
  ├── booking-widget.js                 # Extracted modal trigger + Stripe integration
  └── tools/shipcaptaincrew/
      └── lambda_function.py            # AWS Lambda handler for /api/g/* endpoints

The sailor board is not a centralized page but a template-driven pattern: for each charter event, we generate an S3 object at s3://queenofsandiego-public/g/{YYYY-MM-DD-event-slug}/index.html. The Lambda function at shipcaptaincrew.queenofsandiego.com serves the API layer.

Data Flow: Uploads → Storage → Gallery Render

  1. Client upload: Guest selects up to 24 files via <input id="file-input" multiple> in the sailor-board HTML. JavaScript calls handleFiles() (line 514 in sailor-board/index.html), which slices to first 24 and triggers presigned POST to S3.
  2. S3 presign flow: GET /api/g/{event_id}/presign Lambda endpoint returns time-limited presigned POST URL. CORS headers allow Origin: https://queenofsandiego.com and Origin: https://staging.queenofsandiego.com.
  3. DynamoDB metadata: After successful S3 PUT, a Lambda trigger (or client-side callback) writes upload metadata to DynamoDB table queenofsandiego-uploads with partition key {event_id}#{timestamp}. Includes: uploader identity, code verification status, moderation flag.
  4. Gallery render: GET /api/g/{event_id}/photos queries DynamoDB, retrieves S3 object metadata (size, format), and joins approved uploads with Instagram feed data. Returns JSON array to client for client-side grid render.
  5. Instagram sync: Lambda periodically or on-demand fetches posts tagged #jada or #queenofsandiego from the previous 24 hours relative to event_id date. Instagram Graph API credentials stored in AWS Secrets Manager; queries filtered by timestamp >= event_date AND timestamp < event_date + 1 day.

Caching & CDN Strategy

Per-charter pages are deployed to S3 and served through CloudFront distribution d3m5f7k9x2n1q.cloudfront.net (staging) and production alias queenofsandiego.com (via Route53 weighted routing). Cache headers:

  • Cache-Control: max-age=3600, public for sailor-board HTML (1 hour, to pick up moderation changes).
  • Cache-Control: max-age=86400, public for photo thumbnails (24 hours, assumes no deletion).
  • Cache-Control: no-cache, no-store for /api/g/* (always fresh from Lambda).

CloudFront cache invalidation is triggered post-deployment via aws cloudfront create-invalidation --distribution-id {DIST_ID} --paths "/g/*" "/api/g/*".

Lambda & Anti-Spam Implementation

The shipcaptaincrew Lambda function (file: tools/shipcaptaincrew/lambda_function.py) contains three critical handlers:

1. Event Code Verification (readCode() — client-side; server validates)

When a guest enters their event code in the #g-code input, JavaScript calls the presign endpoint with header X-Event-Code: {code}. The Lambda handler compares the code against Event table in DynamoDB:

def verify_event_code(event_id, supplied_code):
    # Query DynamoDB Event table, partition key = event_id
    event_record = dynamodb.get_item(
        TableName='queenofsandiego-events',
        Key={'event_id': {'S': event_id}}
    )
    stored_code = event_record['Item']['guest_code']['S']
    return supplied_code == stored_code

If code matches, uploads bypass moderation queue. If no code or mismatch, upload is marked moderation_pending: true in DynamoDB, and a notification is sent to the charter operator's email.

2. Presign Endpoint (POST /api/g/{event_id}/presign)

def lambda_handler(event, context):
    event_id = event['pathParameters']['event_id']
    code = event['headers'].get('X