diff --git a/index.html b/index.html index c7c6430..9100248 100644 --- a/index.html +++ b/index.html @@ -76,6 +76,35 @@ }, 3000); } + // Render-freeze self-heal. iOS WebKit (iOS 26 / WKWebView — e.g. Edge & Chrome + // on iOS) intermittently loads the page into a frozen render process: the DOM is + // complete and the bundle runs, but the whole content area stays white and ONLY + // a reload (not scroll/touch/rotate) clears it. The Paint Timing API records a + // 'first-contentful-paint' entry only when WebKit actually paints content; if + // it's still absent ~3s in, the page froze — reload once (guarded) to recover. + // This complements the boot-watchdog above (which covers a bundle that never + // runs); here the bundle runs fine but the render is dead until a fresh load. + var RKEY = 'webmap-render-reload'; + setTimeout(function () { + var painted; + try { + painted = performance.getEntriesByType('paint').some(function (p) { + return p.name === 'first-contentful-paint'; + }); + } catch (e) { painted = true; } // no Paint Timing API → never risk a reload loop + if (painted) { try { sessionStorage.removeItem(RKEY); } catch (e) {} return; } + var rAlready; + try { rAlready = sessionStorage.getItem(RKEY) === '1'; } catch (e) { rAlready = false; } + if (!rAlready) { + // Only reload if the guard actually persisted — if sessionStorage is + // unavailable (private mode, storage disabled), reloading would loop + // forever. Mirrors the boot-watchdog above. (Key: 'webmap-render-reload'.) + var rPersisted = false; + try { sessionStorage.setItem(RKEY, '1'); rPersisted = true; } catch (e) { rPersisted = false; } + if (rPersisted) { location.reload(); } + } + }, 3000); + // Blank-state probe: if the map still hasn't rendered after 5s and no error // was captured, dump enough state to see WHY it's blank. setTimeout(function () { diff --git a/src/consent.ts b/src/consent.ts index 8a36c21..d273c8c 100644 --- a/src/consent.ts +++ b/src/consent.ts @@ -65,6 +65,7 @@ export function showConsentModal(): Promise { `; overlay.appendChild(panel); + document.body.appendChild(overlay); function cleanup(accepted: boolean): void { overlay.remove(); @@ -72,10 +73,8 @@ export function showConsentModal(): Promise { resolve(accepted); } - // Wire listeners on the still-detached panel so they're ready before mount - // (querySelector works on a detached element; getElementById would not). - panel.querySelector('#consent-accept')!.addEventListener('click', () => cleanup(true)); - panel.querySelector('#consent-decline')!.addEventListener('click', () => cleanup(false)); + document.getElementById('consent-accept')!.addEventListener('click', () => cleanup(true)); + document.getElementById('consent-decline')!.addEventListener('click', () => cleanup(false)); overlay.addEventListener('click', (e) => { if (e.target === overlay) cleanup(false); }); @@ -88,18 +87,7 @@ export function showConsentModal(): Promise { }; document.addEventListener('keydown', onKey); - // Mount on the next frame, NOT synchronously during the page's first paint. - // iOS WebKit (including Edge/Chrome on iOS, which run on WKWebView) intermittently - // fails to composite a fixed-position layer injected during that first paint, - // leaving a white screen with the modal present in the DOM but unpainted — the - // blank-on-cold-start that "prevents usage" until a manual refresh (confirmed via - // on-screen diagnostic: bundleRan=true, hasConsent=false, nothing visible). - // Mounting after first paint and flushing layout makes the overlay paint reliably. - requestAnimationFrame(() => { - document.body.appendChild(overlay); - void overlay.getBoundingClientRect(); // flush layout → ensure a paint - // Focus the accept button for keyboard accessibility (post-mount). - (panel.querySelector('#consent-accept') as HTMLElement | null)?.focus(); - }); + // Focus the accept button for keyboard accessibility + document.getElementById('consent-accept')!.focus(); }); }