On June 16, 2026, JADA was running an ash scattering memorial charter for the Ewing family — crew call 10:00 AM, departure 11:00 AM sharp. The day before, while staging trip sheets and waivers, we discovered that every URL under shipcaptaincrew.queenofsandiego.com/ref/* was returning:
{"error":"Unauthorized"}
These are the print-ready crew documents — the captain's trip sheet, the passenger manifest, the liability waiver. Crew needed them. Daniela was going to print them at 7 AM and bring them to the marina. Nothing was reachable.
The shipcaptaincrew.queenofsandiego.com distribution routes /ref/* to a Lambda function via a CloudFront behavior. Every other path serves directly from S3. The Lambda's original intent was to gate crew pages behind magic-link tokens (the ?m= query parameter). The bug was simple but total: the Lambda had no public code path. Every request — token or no token — ran through the auth check, and static print documents don't carry tokens.
The fix was a path check at the top of the handler: if the request path starts with /ref/, serve the S3 object directly without token validation. Trip sheets and waivers are not sensitive — they're meant to be printed and handed to anyone at the dock. The Lambda was redeployed, CloudFront invalidation was issued for /ref/*, and the URLs started returning HTML within a few minutes.
One root cause of the Ewing waiver scramble was that the previous waiver had been hand-edited per charter, which meant legal language drifted. The McLaughlin and Loback waivers weren't identical. That's a liability problem.
We solved it by freezing the legal text in a single template file:
~/icloud-jada-ops/templates/waiver.html.tmpl
The template has eight {{PLACEHOLDER}} tokens — charter title, date, time range, charter type, and the four crew roles. Everything else is static legal language that will not be touched by any automated process. Then generate_waiver.py does the rest deterministically:
jada-crew-dispatch by event_idoccasion field).replace()shipcaptaincrew.queenofsandiego.com for the print link and queenofsandiego.com for the archiveUsage is one line: python3 generate_waiver.py 2026-06-16-ewing. The waiver for any future charter is now a thirty-second operation with no manual editing and no LLM involved.
While debugging the Lambda issue, we caught a separate bug in log_deposit_check.py: the script was reading deposits using charter_id as the DynamoDB key, but the jada-crew-dispatch table's partition key is event_id. The item was never found — the script was silently doing nothing. Fixed to use event_id throughout.
This is exactly the class of bug that's hard to catch without a live charter forcing you to trace every system end-to-end. The deposit for the Ewing charter ($550 balance, Zelle) needed to be recorded the moment it arrived. A silent DDB miss would have left the ledger wrong.
send_june16_dispatch.py handles all outbound communications for a charter in a single run via SES (us-east-1, profile queenofsandiego):
EWNG16, and a button to the guest pageThe template for each email type is inline HTML in the script, using the JADA dark-navy design system (#0a1628 card background, #c9a227 gold accent). All six sends complete in under two seconds. Running it was one command the morning of the charter.
The HANDOFF from this session put the full booking-to-dispatch flow at roughly 30–40% automated. The trip sheet is still hand-assembled per charter. The dispatch script exists but isn't parameterized — it's hard-coded for Ewing. Crew SMS is blocked on adding phone numbers to crew.json. The next step is turning send_june16_dispatch.py into a reusable charter_provisioner.py that reads entirely from DDB and works for any event ID. That's the same pattern as generate_waiver.py: one command, deterministic, no manual intervention.