Automating Boat Cleaning Dispatch and Calendar Synchronization: Replacing Manual Workflows with Infrastructure-as-Code
What Was Done
This session addressed a critical operational gap: the cancellation of FancyHands cleaning services left Queen of San Diego without a systematic way to manage boat cleaning schedules. Rather than replacing one manual service with another, we architected an automated solution that:
- Created a Python-based dispatch system to queue cleaning tasks
- Integrated boat platform credentials (GetMyBoat, Boatsetter) with calendar synchronization
- Replaced manual Google Apps Script deployments with version-controlled code and automated pushing
- Established a unified calendar API backend (Lambda) to handle both event creation and external platform syncing
The core insight: instead of managing cleaning through a third-party service, treat it as a first-class operational system with calendar awareness, audit trails, and integration hooks.
Technical Architecture
Dispatch System
Created /Users/cb/Documents/repos/tools/dispatch_boat_cleaner.py as the entry point for cleaning task creation. This script:
- Accepts boat identifier and desired cleaning date/time
- Validates against existing calendar events to prevent scheduling conflicts
- Posts to the Lambda calendar API endpoint with proper authentication
- Logs results to CloudWatch for audit and debugging
The dispatch approach is intentionally simple—it's a thin wrapper around the calendar API. This means cleaning tasks flow through the same authorization and logging layer as all other calendar operations, providing consistency and reducing surface area for bugs.
Calendar Synchronization Service
The CalendarSync.gs file in /Users/cb/Documents/repos/sites/queenofsandiego.com/rady-shell-events/apps-script-replacement/ handles bidirectional sync between Google Calendar and external boat platforms:
- Outbound: When a cleaning event is created in Google Calendar, CalendarSync detects the event type and pushes availability/booking data to GetMyBoat and Boatsetter iCal feeds
- Inbound: Platform bookings are pulled via iCal and created as read-only calendar entries for visibility
- Conflict Detection: Before syncing, the service checks for overlapping events and alerts operators of potential double-bookings
This file was version-controlled and deployed via .clasp.json configuration, which maps the local directory to Google Apps Script project ID. Deployment is now push-based rather than manual editing in the GAS web editor, enabling code review and rollback capability.
Lambda Calendar API Backend
The existing Lambda function (invoked via API Gateway) was extended with new action routes:
add-calendar-event: Creates events with metadata tagging (cleaning, booking, hold, etc.)list-calendar-events: Returns events with optional date range filteringsync-platform-events: Triggers CalendarSync.gs to poll external platforms
The Lambda function uses environment variables to store:
- Google Calendar API credentials (service account JSON)
- GetMyBoat/Boatsetter API keys (loaded from
repos.env) - SES sender email addresses for notifications
Authentication to the API Gateway endpoint uses a Bearer token stored in the dashboard environment, preventing unauthorized task creation.
Integration Points
Email Workflow
When a cleaning task is dispatched, the system sends notifications via Amazon SES to stakeholders:
- Confirmation email to operations (From: dashboard-api@queenofsandiego.com)
- Calendar invite to boat captain/crew if email is available
- Follow-up reminders 24 hours before scheduled cleaning
These are triggered from the Lambda function using the SES API; template HTML is stored in /Users/cb/Documents/repos/tools/templates/.
Dashboard Integration
The main dashboard (/tmp/dashboard_index.html) was updated to include:
- A "Dispatch Cleaning" card that calls the dispatch script with a simple form (boat, date, time)
- Real-time display of upcoming cleaning tasks pulled from the calendar API
- Status indicators showing sync state with external platforms
The dashboard token is rotated periodically and stored in repos.env to prevent accidental exposure.
Key Decisions
Why Google Calendar as the Source of Truth
Instead of a custom database, Google Calendar serves as the operational ledger. Rationale:
- Human-readable: Carole and the team can view, edit, and understand events without technical overhead
- Integration-friendly: iCal export is a universal standard; external platforms can consume it easily
- Audit trail: Google's activity logs provide immutable records of who created/modified events and when
- Mobile-first: Calendar apps on phones make it easy to check availability from the boat
- No additional infrastructure: Avoids maintaining a separate database, backup strategy, and disaster recovery plan
Why Lambda Instead of Scheduled Tasks
The CalendarSync service runs on Lambda (triggered via API Gateway) rather than a cron job for:
- Scalability: Handles spikes in sync requests without pre-provisioning capacity
- Cost: Pay only for execution time; no idle container/VM charges
- Simplicity: No need to manage polling intervals or handle lambda function timeouts manually
- Observability: CloudWatch logs and X-Ray tracing are built-in
Version Control for Apps Script
CalendarSync.gs is now stored in the git repo instead of only in the GAS web editor because:
- Code review: Changes can be reviewed before deployment (catches bugs early)
- Rollback: If a sync breaks platform integrations, reverting is a one-line git command
- CI/CD ready: Future automation can run tests before pushing to GAS
The .clasp.json file in that directory points to the live GAS project; clasp push deploys code immediately.
Deployment and Testing
Tested end-to-end by:
- Invoking the Lambda function directly to confirm Google Calendar API credentials work
- Calling the API Gateway endpoint with the dashboard token to add sample calendar events
- Verifying that events created via API appear in the Google Calendar web interface within seconds
- Manually triggering CalendarSync.gs to confirm iCal export from GetMyBoat/Boatsetter pull succeeds