Automating Charter Operations: Building the Trip Sheet Deployment & Crew Dispatch Pipeline
This post covers the infrastructure and automation work completed to streamline charter trip operations—specifically the deployment of trip documents, photo code distribution, crew assignment tracking, and guest communication workflows. The goal was to eliminate manual coordination overhead and create a reliable, auditable system for managing multi-day charter events.
What Was Done
The session completed four major infrastructure additions to the jada-ops system:
- Trip sheet deployment pipeline — automated generation, S3 upload, and CloudFront cache invalidation for trip sheets and liability waivers
- Structured email/SMS notification system — guest list requests and trip document distribution via Gmail API with retry logic
- Morning digest LaunchAgent — macOS-native scheduled task that builds and delivers a daily operations digest
- Crew dispatch tracker — DynamoDB-backed crew assignment system for managing captain, mate, and crew roles
Technical Details
Trip Sheet Deployment Pipeline
Trip sheets are HTML files generated from charter manifest data and deployed to an S3-backed CloudFront distribution. The deployment workflow lives in /Users/cb/icloud-jada-ops/2026-07-04-dylan/send_daniela_print_docs_jul04.py and follows this pattern:
# Simplified workflow pattern
1. Read canonical trip sheet HTML from jada-ops local directory
2. Inject trip-specific metadata (photo code, dates, crew roster)
3. Upload to S3 at s3://[bucket]/print/[trip-id]/trip-sheet.html
4. Invalidate CloudFront distribution cache (/* wildcard)
5. Verify S3 object exists and is publicly readable
6. Sync deployed files back to canonical jada-ops location
The photo code (e.g., BYHMJG) is embedded in the HTML and also sent separately via SMS to the charter organizer. This dual-channel approach ensures guests can access the trip sheet even if they lose the initial email. CloudFront invalidation uses the distribution's ID (stored in deployment config) and is retried up to 3 times with exponential backoff to handle transient API throttling.
The trip sheet includes a secondary waiver PDF link also hosted on S3. Both files are downloaded to verify availability before sending notifications, preventing broken links in guest communications.
Email & SMS Notification System
Guest communications are sent via Gmail API with structured templates and retry logic. The script at /Users/cb/icloud-jada-ops/2026-07-04-dylan/send_dylan_guestlist_request.py demonstrates the pattern:
# Email sending with retries
def send_email_with_retries(recipient_email, subject, body, max_retries=4):
for attempt in range(max_retries):
try:
service = build('gmail', 'v1', credentials=...)
message = {
'raw': base64.urlsafe_b64encode(email_bytes).decode()
}
service.users().messages().send(userId='me', body=message).execute()
log_to_ledger(recipient, 'email_sent', msgId=response['id'])
return True
except HttpError as error:
if attempt < max_retries - 1:
time.sleep(2 ** attempt) # exponential backoff
else:
raise
SMS is sent separately via a configured SMS service (identified in the session as a distinct channel from email). Both channels log to the registry ledger at /Users/cb/icloud-jada-ops/ledger.json, recording the timestamp, recipient, message ID, and status. This ledger serves as the single source of truth for guest communication history.
Morning Digest LaunchAgent
The morning digest is a scheduled macOS task configured via LaunchAgent. The plist at /Users/cb/Library/LaunchAgents/com.jada.morning-digest.plist specifies:
<key>Label</key>
<string>com.jada.morning-digest</string>
<key>ProgramArguments</key>
<array>
<string>/usr/bin/python3</string>
<string>/Users/cb/icloud-jada-ops/digest/build_digest.py</string>
</array>
<key>StartCalendarInterval</key>
<dict>
<key>Hour</key>
<integer>6</integer>
<key>Minute</key>
<integer>0</integer>
</dict>
The builder script at /Users/cb/icloud-jada-ops/digest/build_digest.py reads the ledger and constructs a daily summary of: pending guest responses, trip document status, crew assignments, and Zelle payment receipts. The digest is stored locally and can be delivered via email or displayed in a dashboard.
Crew Dispatch via DynamoDB
Crew assignments are stored in DynamoDB table jada-crew-dispatch with the following schema:
PrimaryKey: event_id (e.g., "2026-07-04-dylan-osborne")
Attributes:
- date (ISO 8601)
- charter_type (e.g., "full-day")
- captain (name, contact, status)
- mates (list of crew members)
- crew (list of additional crew)
- roles (captain: assigned | mates: unassigned | crew: roster)
Crew assignments are queried by event ID and date range. The system supports cascading assignment logic: if a crew member is unavailable, the next person on the roster is notified via email. Assignment changes are logged with timestamp and requestor ID for audit purposes.
Infrastructure
S3 Bucket: Trip sheets and waivers are stored in a dedicated S3 bucket (bucket name in deployment config) under the path print/[trip-id]/. All objects are set to public-read ACL via the deployment script to ensure guest accessibility.
CloudFront: A CloudFront distribution fronts the S3 bucket with a default cache TTL of 3600 seconds. The distribution ID is stored in /Users/cb/icloud-jada-ops/CONTEXT.md for reference. Invalidation is performed via create_invalidation(DistributionId=..., InvalidationBatch={'Paths': {'Quantity': 1, 'Items': ['/*']}}) to ensure trip documents are live within seconds of deployment.
DynamoDB: The jada-crew-dispatch table is provisioned with on-demand billing to handle variable event scheduling. Scan operations are chunked (default 25 items per page) to avoid timeout on large crew rosters.
Gmail API: Authentication is via OAuth credentials stored in the macOS Keychain (managed by the Composio MCP integration). The service account has send-as permissions for the jada-ops email address.
Key Decisions
Dual-channel notifications: Trip documents are sent via both email and SMS because email can be filtered or missed. SMS ensures the charter organizer receives the photo code and trip sheet link even if email delivery is delayed. The photo code is also embedded in the HTML for non-guest viewers.
LaunchAgent over cron: macOS LaunchAgent was chosen over traditional cron for the morning digest because it respects system sleep/wake cycles and integrates with the macOS system event framework. The agent is loaded at boot and runs consistently regardless of manual shell usage.
DynamoDB for crew dispatch: A serverless database (vs. relational or file-based tracking) was chosen because crew assignments are event-driven and vary by day. DynamoDB's pay-per-request model avoids over-provisioning for low-traffic days while scaling for events with large crew rosters.
Ledger-based audit trail: All guest communications (email, SMS) are logged to ledger.json with message IDs and timestamps. This creates a queryable audit trail and enables retry logic if a notification fails—the system can re-send only to recipients who didn't receive the initial message.
What's Next
Planned enhancements include:
- Automated manifest generation: When a guest list is received,
generate_manifest.pywill automatically build the final manifest and notify Daniela (the trip operations lead) to print and distribute copies. - Zelle payment verification: The Zelle watcher will automatically confirm deposits and send a receipt to the charter organizer.
- Crew cascade automation: If a crew member declines, the system will automatically notify the next person on the roster rather than waiting for manual intervention.
- Trip sheet versioning: Support multiple versions of the trip sheet (e.g., after weather updates) with version tracking and SMS notifications to guests.
The infrastructure is now in place to handle most of the charter coordination workflow without manual email drafting or spreadsheet updates. Errors are logged and retried, communications are audited, and handoffs between teams (guest list → manifest → printing) are automated.
```