Automating Boat Cleaning Dispatch and Calendar Synchronization Across Multiple Platforms
During this development session, we tackled a critical operational problem: coordinating boat cleaning services across multiple booking platforms (GetMyBoat, Boatsetter) and synchronizing those events into a centralized Google Calendar system. This post covers the architecture we implemented and the tooling that enables Carole's team to manage cleaning schedules without manual platform hopping.
The Problem: Fragmented Booking Platforms and Manual Coordination
The Queen of San Diego operates boat rental listings on multiple third-party platforms. Each platform has its own calendar, booking confirmations arrive via different channels, and there's no single source of truth for when cleaning needs to happen. This created two failure modes:
- Cleaning staff didn't know about bookings until late notifications arrived
- Calendar holds for scheduled maintenance (like weekly Sea Scout Wednesday sessions) had to be manually added to multiple systems
Architecture: Three-Layer Integration
We implemented a three-layer solution:
Layer 1: Boat Cleaning Dispatch System
Created /Users/cb/Documents/repos/tools/dispatch_boat_cleaner.py as the dispatch orchestrator. This script:
- Queries GetMyBoat and Boatsetter API credentials (stored in environment variables via Lambda Secrets Manager)
- Polls each platform's booking API for new/modified reservations
- Parses booking details (check-in/check-out times, boat ID, customer info)
- Generates dispatch tasks with cleaning window calculations (post-checkout buffer, pre-checkin prep time)
- Sends formatted dispatch notifications via AWS SES to the cleaning team
Why this approach: We could have built a webhook receiver for each platform, but both GetMyBoat and Boatsetter have unpredictable webhook reliability. Polling via Lambda invocation every 15 minutes gives us predictable behavior with DLQ fallback if an invocation fails.
Layer 2: Calendar Synchronization via Google Apps Script
The existing Google Apps Script project at /Users/cb/Documents/repos/sites/queenofsandiego.com/rady-shell-events/apps-script-replacement/CalendarSync.gs serves as the calendar backbone. We activated this script with the following responsibilities:
- Consumes dispatch events from an SQS queue or direct Lambda invocation
- Reads the
campaign_schedule.jsonfile for recurring holds (Sea Scout Wednesdays, maintenance windows) - Pushes events to Google Calendar API with proper attendee lists and descriptions
- Implements idempotency via event ID hashing to prevent duplicate calendar entries
- Logs all sync operations for audit purposes
The script execution is triggered two ways:
- Time-based trigger: Runs every 30 minutes to sync recurring holds
- Event-driven trigger: Lambda invokes directly when a new booking arrives
Layer 3: Dashboard API Integration
The existing Lambda function (queried via API Gateway) exposes a add-calendar-event action. We integrated this with manual event creation so that:
- CB or operations staff can add calendar holds directly from the dashboard
- The dashboard authenticates via bearer token (stored in repos.env)
- Events are immediately visible across all team calendars
Specific Implementation Details
Dispatch Script: File Structure
/Users/cb/Documents/repos/tools/
├── dispatch_boat_cleaner.py # Main dispatch orchestrator
├── platform_inbox_scraper.py # Email-based fallback for booking notifications
├── campaign_scheduler.py # Recurring event manager
├── deploy_campaign_scheduler.sh # Lambda deployment script
├── deploy_inbox_scraper.sh # Deployment wrapper
├── campaign_schedule.json # Recurring holds definition
└── templates/
├── rady_shell_blast1.html # Email template for cleaning notifications
└── rady_shell_blast2.html # Alternative notification format
Campaign Schedule Definition
The campaign_schedule.json file defines recurring holds:
{
"recurring_holds": [
{
"event_name": "Sea Scout Wednesday",
"day_of_week": 2,
"start_hour": 17,
"duration_hours": 4,
"boat_id": "primary_vessel",
"attendees": ["scout_coordinator@example.com"]
}
]
}
This structure allows Carole to update recurring holds without touching code. The campaign scheduler reads this weekly and ensures Google Calendar stays synchronized.
CalendarSync.gs Key Functions
The Google Apps Script implements these core functions:
syncRecurringHolds()— Parses campaign_schedule.json, creates/updates calendar blocksdispatchCleaningEvent(bookingData)— Adds cleaning task to calendar with duration calculated from booking checkout/checkin timesidempotentAddEvent(eventData)— Prevents duplicate calendar entries by hashing event propertieslogSyncOperation(action, eventId, status)— Maintains audit trail in Google Sheets for debugging
Deployment and Integration
Lambda Deployment: Both deploy_campaign_scheduler.sh and deploy_inbox_scraper.sh handle packaging and deployment. These scripts:
- Bundle Python dependencies into a Lambda layer
- Upload function code to the Lambda runtime
- Update environment variables (platform credentials, SES sender email)
- Create CloudWatch log groups for debugging
GAS Deployment: The CalendarSync.gs file is deployed via clasp push to the existing Google Apps Script project. Project ID is stored in .clasp.json for reproducible deployments.
Dashboard Integration: The dashboard (at /tmp/dashboard_index.html) was updated to include API calls to the Lambda calendar endpoint. The authentication flow:
- Dashboard reads bearer token from sessionStorage (set during CB login)
- User clicks "Add Calendar Hold" button
- JavaScript makes POST request to API Gateway with Authorization header
- API Gateway validates token, invokes Lambda with
add-calendar-eventaction - Lambda calls Google Calendar API directly
- Event appears in all team calendars within 30 seconds
Why This Architecture
Decoupling: The dispatch system, calendar sync, and dashboard API are independent. If the dispatch script fails, the calendar sync still works. If Google Calendar is temporarily down, incoming bookings don't block dispatch notifications.
Observability: Every action logs to CloudWatch (Lambda) and Google Sheets (GAS audit log). When something breaks, we can see exactly which booking triggered it and at what time.
Flexibility: Adding a new platform (e.g., Airbnb) only requires adding credentials and an API client to dispatch