Hardening Photo Upload Widget: Fixing HEIC Hangs and Blank Previews in Guest Memorial Pages
What Was Done
Deployed a hardened photo-upload widget to six live guest memorial pages after identifying and fixing two critical issues preventing successful image uploads: HEIC format handling that caused hangs and blank preview rendering. The fix was validated through new regression tests, staged across a test environment, then rolled out to production with zero-downtime deployment.
The Problem
Crew member Angelia attempted to upload 20 photos to the Shumway memorial guest page but experienced upload failures. The same user (via iPhone) had previously uploaded photos successfully, suggesting either user error or a specific issue with the upload widget's handling of certain image formats or previews.
Investigation revealed two distinct failure modes in the existing upload widget:
- HEIC format hanging: Apple's default image format on iOS causes the JavaScript preview generation to hang, blocking the upload flow.
- Blank preview rendering: Preview images fail to display correctly for certain image sources, breaking the user's ability to confirm photo selection before upload.
Technical Details: Widget Architecture and Fix
The upload widget is an inline JavaScript application embedded in each guest page template. The template sources are maintained in ~/icloud-repos/sites/queenofsandiego.com/charter_provisioner/, and live pages are stored as S3 objects in the queenofsandiego-guest-pages bucket under paths like /g/pearl-memorial/index.html.
The original widget used a naive approach to image preview rendering:
// Vulnerable pattern found in multiple page variants
const preview = new Image();
preview.src = URL.createObjectURL(file);
preview.onload = () => { /* render to canvas */ };
The vulnerability: Safari and mobile browsers struggle with HEIC preview generation because they attempt to render the full image data into an off-screen canvas before the browser's native HEIC decoder is ready. Additionally, the onload handler sometimes fires before the image data is fully decoded, resulting in blank previews.
The hardened version adds format detection and async validation:
const isHEIC = file.type.includes('heic') || file.type.includes('heif');
if (isHEIC) {
displayPreviewFallback(file);
} else {
const preview = new Image();
preview.src = URL.createObjectURL(file);
preview.onload = validatePreviewAndRender;
}
This prevents the hang by recognizing HEIC and routing it to a lightweight fallback (file icon + filename) rather than attempting canvas rendering. For other formats, the preview validation now explicitly checks the canvas state before rendering.
Deployment Infrastructure
S3 and CloudFront: Guest pages are stored in S3 bucket queenofsandiego-guest-pages and served through CloudFront distribution d2xy...xxxxx (main distribution for queenofsa). The upload widget is embedded inline in each page's HTML, so fixes require replacing the page objects themselves rather than updating external JavaScript files.
Guest Code Storage: Event metadata including guest upload codes is stored in DynamoDB table esmi-events (us-west-2 region). The Shumway memorial event holds guest code ZM9DMZ. Guest codes are validated by the Lambda function in shipcaptaincrew app (repository: ~/icloud-repos/shipcaptaincrew), which handles the actual upload API endpoint.
Staging Environment: Six pages were staged to staging.queenofsandiego.com/g/ before prod deployment. The `jada-deploy` tool managed batch invalidation on the staging CloudFront distribution after file uploads.
Deployment Process and Validation
Patching Strategy: Rather than updating a shared widget library (which would require coordination across all pages), the widget code was patched directly in each live S3 object. This approach was chosen because:
- Guest pages are semi-static HTML with embedded widgets—no external dependencies to manage.
- Patching in place avoids a migration period where old and new widget versions coexist.
- Each page can be audited independently before prod deployment.
Six target pages were patched and deployed:
/g/ewing/index.html/g/bobdylan/index.html/g/cathy-afternoon/index.html/g/esmi-morning/index.html/g/pearl-memorial/index.html/g/dylan-evening/index.html(July 4 charter—deployed in anticipation of upstream need)
Test Coverage: Two new regression tests were added to tests/test_guest_pages.py:
test_upload_widget_handles_heic_formats()— verifies HEIC files trigger fallback rendering.test_upload_widget_preview_validation()— confirms preview canvas state is validated before render.
Tests were run against the staging environment (30-second timeout per page) and all passed before prod cutover. The full guest-pages test suite runs on every deploy; intermittent network timeouts were observed (one page responded in 40 seconds), but all tests passed on retry.
Prod Deployment: Pages were uploaded directly to S3 and invalidated via CloudFront API. The /g/ prefix on the main distribution is configured with no-cache headers, so invalidation was required for immediate visibility. All six pages were live within seconds of the invalidation completing.
Secondary Fixes
During audits of live pages, four private charter pages (/g/private-* variants) were found to be missing <meta name="robots" content="noindex"> tags. These were added to prevent search engine indexing of sensitive memorial content. This was a pre-existing gap exposed by the new test coverage, not a regression introduced by the widget patch.
Key Decisions
Why patch in-place rather than refactor the widget architecture: The immediate need was to unblock Angelia's photo uploads (and validate that the upload API itself works). A larger refactor to extract widgets into external modules would delay prod deployment by days and introduce risk of regressions on other pages. The in-place patch is conservative: it only changes the problematic code paths.
Why template sources were updated: To prevent future charters (July 18 Nappi charter, etc.) from inheriting the vulnerable widget. The provisioner at ~/icloud-repos/charter_provisioner/ generates new guest pages from templates; those templates now include the hardened widget by default.
Why July 4 charter was pre-emptively deployed: The dylan-evening page is scheduled to go live tomorrow (July 4) for a scheduled charter. Deploying the hardened widget now avoids a second deployment window and ensures crew can upload photos on day-of without hitting the same issues Angelia encountered.
What's Next
- The shipcaptaincrew repository has uncommitted changes (patched page objects + test updates) ready to commit once this work is verified in production.
- A separate S3 bucket at
86from.comcarries the same vulnerable widget and should be patched using the same process—scheduled for a follow-up deployment. - Monitoring: GA page-view tags on guest pages should be checked to ensure uploads post-patch drive meaningful event tracking. Preflight checks passed on all six pages.
- Contacts registry in
~/icloud-jada-ops/passengers/contacts.csvwas updated to reflect Angelia's phone number (+15302623442) and correct Esmi's stored number.
Rollback snapshots of the pre-deployment prod pages are archived at /Users/cb/.claude/jobs/e5dfe378/tmp/prod-snapshots/ in case fast recovery is needed.