← Queen of San Diego — Tech Blog
2026-06-16

2026-06-16

I have enough context now. Writing the post. TITLE: Fixing a Lambda Auth Bug at 7 AM Before a Memorial Ash Scattering Charter ```html

The Deadline That Found the Bug

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.

Root Cause: CloudFront Was Right, Lambda Was Wrong

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.

Building generate_waiver.py: Zero LLM, Pure Template

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:

Usage 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.

The DDB Key Bug That Almost Broke Payment Tracking

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.

Dispatching Six Emails in One Script

send_june16_dispatch.py handles all outbound communications for a charter in a single run via SES (us-east-1, profile queenofsandiego):

The 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.

What's Still Manual (and Shouldn't Be)

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.