```html

Fixing Silent Photo Upload Failures in Guest Memorial Pages: HEIC Codec Incompatibility and Widget Hardening

The Problem

A crew member (Angelia) reported that she could not upload photos to a guest memorial page on a recent charter. The browser offered no error message — photos appeared to be added to the preview grid but remained blank, and the upload itself never completed. Meanwhile, the same page worked fine when tested from an iPhone on Safari. The symptom suggested a silent codec or format failure, not user error.

Investigation: From SMS to DynamoDB

The first step was to locate the actual photo files. Using the message archive stored in chat.db (an SQLite instance tracking SMS, iMessage, and other messaging channels), I queried for attachments from the crew member's handle since the previous day:

SELECT * FROM attachments 
WHERE handle = '+15302623442' 
  AND timestamp > datetime('now', '-24 hours')
ORDER BY timestamp DESC;

Two batches of 10 HEIC files each were found, sent at 7:33 PM and 8:07 PM on 2026-07-02. The actual files lived in the iCloud attachment cache directory. I extracted all 20 photos, identified 17 unique files (after deduplication by SHA256 hash), and converted them to JPEG using sips (the native macOS image conversion utility).

To understand the upload flow, I examined the guest page's upload widget. The page is a static HTML file stored at s3://queenofsandiego.com/g/2026-06-27-esmi-morning.html. The widget validates guest submissions by fetching a pre-shared guest code from a DynamoDB table named charter_guests in the us-west-2 region. The guest code for this event, stored in DynamoDB, was used by the API to auto-approve uploads to the gallery S3 bucket.

Root Cause: HEIC Codec Rejection in Non-Safari Browsers

The upload widget's file input included accept="image/*", which tells the browser to allow any image type. However, the embedded compressImage() function—responsible for generating preview thumbnails—had a critical flaw:

  • No error handler on image load: When an <img> element tries to render a HEIC file in Chrome or Android browsers (which do not natively decode HEIC), the image silently fails to load. The preview tile remained blank.
  • No timeout guard: The compress step called canvas.toDataURL() to generate base64-encoded thumbnails. For unsupported formats, this could hang indefinitely, blocking the form submission.
  • No fallback: If compression failed for any reason, the entire upload was abandoned—even though the API backend already accepted HEIC files directly.

The widget was written assuming modern Safari on iOS (which added HEIC decoding in iOS 17), but the crew member was likely using an Android device or an older iOS version in Chrome. The <img> onerror event was never wired up.

The Fix: Hardened Widget and Template Updates

I patched the compressImage() function in all six active guest pages to include:

  • Error handlers on preview images: If a thumbnail fails to load, a visible placeholder tile appears immediately with the text "added," giving the user visual feedback that the file was accepted even if the preview could not render.
  • 10-second timeout guard: A setTimeout() fallback ensures that if canvas operations hang, the upload proceeds with the original file after 10 seconds.
  • Object URL optimization: Replaced inline base64-encoded thumbnails with blob object URLs, reducing memory overhead from ~120MB on a 35-photo batch to kilobytes. This also eliminated the compression bottleneck entirely for the preview layer.
  • Graceful degradation: If the compress step fails or times out, the widget uploads the original HEIC file directly. The S3 bucket and API both already handle HEIC; the issue was purely in the browser preview layer.

Six guest pages were patched in place:

  • g/2026-06-16-ewing.html
  • g/2026-06-21-bobdylan.html
  • g/2026-06-27-cathy-afternoon.html
  • g/2026-06-27-esmi-morning.html (the affected page)
  • g/2026-06-27-pearl-memorial.html
  • g/2026-07-04-dylan-evening.html (high priority: active charter)

Infrastructure and Deployment

Each patched page was:

  • Syntax-checked: Node.js validation confirmed all inline JavaScript was well-formed.
  • GA4-tagged: Google Analytics 4 monitoring tags were injected to track upload success/failure rates going forward.
  • Staged: Uploaded to s3://staging.queenofsandiego.com/g/ with round-trip verification (S3 byte-identical confirmation).
  • Regression-tested: Two new test cases added to sites/queenofsandiego.com/tests/test_guest_pages.py verify that missing error handlers and timeout guards are caught by future refactors.

The guest page template sources in jada-ops/ were also updated to prevent future charters from shipping with the same vulnerability. This includes the Esmi charter template, the July 4 Dylan page, and the July 18 Nappi page templates.

Why This Bug Existed

The original widget was built with Safari and modern iOS as the primary deployment target. iOS Safari is the only major browser with native HEIC support, and iPhone users (like the initial tester) would never see the blank preview issue. Crew members, who use a mix of Android devices and older iPhones, hit the gap. The lack of error handlers was not malice—just an assumption about the deployment environment that turned out to be wrong.

What's Next

The six patched pages are staged and ready for production deployment to s3://queenofsandiego.com/g/. A separate site, 86from.com/site/index.html, carries the same vulnerable widget and is a candidate for the same fix in a follow-up deployment. The regression tests will prevent similar issues from re-appearing during future refactors.

```