```html

Building a Dynamic Charter Document Pipeline: From Calendar Events to Published S3 Manifests

Over the past development session, I built out a complete document generation and publishing pipeline for JADA Operations' charter management system. This post walks through the architecture, infrastructure decisions, and the specific tooling that makes it work.

What Was Done

The core problem: charter captains and crew need up-to-date manifests and trip sheets before each weekend's departures. Previously, these documents were static or manually generated. The solution involved:

  • Fetching charter event data from JADA's internal calendar API
  • Generating HTML manifests and trip sheets dynamically from charter details
  • Publishing these documents to S3 with proper CloudFront invalidation
  • Persisting document metadata and charter records locally for durability
  • Integrating with the existing shipcaptaincrew web application's document serving layer

Technical Architecture

Document Generation Pipeline

The pipeline lives in /Users/cb/Documents/repos/jada-ops/ with two primary Python modules:

  • charter_provisioner.py — orchestrates the entire workflow: fetches calendar events, generates documents, manages S3 publishing
  • send_charter_emails.py — handles post-publication notifications to crew

The document generation process extracts charter details (passenger names, dates, special provisioning notes, payment status) and renders them into two formats:

  • Manifests — passenger rosters with liability info, published as quinn-male-manifest.html
  • Trip Sheets — crew operational guides with timing, weather notes, and special requests, published as quinn-male-trip-sheet.html

Both templates inherit styling from the main shipcaptaincrew application, ensuring consistency across the crew portal.

Calendar Integration

The system queries JADA's internal Google Calendar API to identify weekend charters. The workflow:

1. Authenticate to calendar API with OAuth token refresh logic
2. Query events for a given weekend date range
3. Extract event metadata: charter ID, captain assignment, passenger count
4. Cross-reference with pricing proposals (stored in /jada-ops/proposals/)
5. Generate manifests with all necessary crew and passenger details

This approach keeps the source of truth in the calendar system while allowing documents to be regenerated on-demand without re-entering data.

Infrastructure & Publishing

S3 Document Storage

Documents are published to two S3 locations for redundancy and organizational clarity:

  • s3://shipcaptaincrew-docs/manifests/quinn-male-manifest.html — primary location, indexed by manifest type
  • s3://shipcaptaincrew-docs/crew-page/{event_id}/quinn-male-manifest.html — secondary location, indexed by event for easy crew-page lookup

Both locations use Content-Type: text/html; charset=utf-8 to ensure proper rendering in browsers. The dual-publish strategy provides both direct access and event-scoped queries.

CloudFront Cache Invalidation

Documents are served through a CloudFront distribution (ID and domain names withheld for security). After publishing to S3, the pipeline automatically invalidates the cache:

aws cloudfront create-invalidation \
  --distribution-id {DISTRIBUTION_ID} \
  --paths '/manifests/quinn-male-manifest.html' '/crew-page/{event_id}/*'

This ensures that crew viewing the documents see fresh content within 60 seconds, critical for last-minute passenger changes or special notes.

Lambda Integration

The existing Lambda function at /Users/cb/Documents/repos/sites/queenofsandiego.com/tools/shipcaptaincrew/lambda_function.py serves as the API backend. Key modifications:

  • Extended the document retrieval logic to check both S3 prefixes
  • Modified build_event_pages to dynamically link to published manifests
  • Updated the handle_get_doc route to resolve event IDs correctly

The Lambda function now acts as both a document gateway and a client-side router—the frontend SPA requests documents via a clean REST endpoint, and Lambda resolves the correct S3 key without exposing bucket structure to clients.

Local Persistence & Durability

To avoid losing charter state, we store metadata locally:

  • /Users/cb/Documents/repos/jada-ops/quinn-male/quinn-male-manifest.html — local archive copy
  • /Users/cb/Documents/repos/jada-ops/quinn-male/quinn-male-trip-sheet.html — local crew notes
  • /Users/cb/Documents/repos/agent_handoffs/projects/quinn-male-charter.md — charter metadata and decision log
  • /Users/cb/Documents/repos/jada-ops/weekend-charters-readiness-2026-05-29.md — weekend readiness report aggregating all active charters

This dual-store approach (local + cloud) allows the team to reconstruct any charter's documents if S3 becomes temporarily unavailable, and provides an audit trail of all charter iterations.

Key Technical Decisions

Why HTML Over PDF

We chose HTML manifests over PDF generation because:

  • HTML renders natively in browsers without external dependencies
  • Mobile crew viewing the documents doesn't require a PDF reader
  • CSS can be styled consistently with the rest of the shipcaptaincrew application
  • Future interactive elements (notes, checkboxes) are trivial to add

Why Dual S3 Locations

Publishing to both /manifests/ and /crew-page/{event_id}/ enables two access patterns:

  • Direct manifest links in crew communications
  • Event-scoped document discovery for the crew portal UI

The cost of duplication is negligible; the benefit is flexibility.

Why CloudFront Invalidation

CloudFront's default TTL could leave stale documents cached for hours. Given that charter details can change rapidly (passenger cancellations, weather updates), we invalidate on every publish. This trades cache efficiency for operational accuracy.

Why Local Archive

Storing copies in the git repository allows:

  • Version control of all document changes
  • Offline charter reference for developers and operations
  • Decoupling local tooling from S3 availability during development

Command Examples

Publishing a manifest and invalidating CloudFront:

# Generate and stage the manifest locally
python charter_provisioner.py --charter quinn-male --action generate

# Publish to both S3 locations
aws s3 cp quinn-male-manifest.html \
  s3://shipcaptaincrew