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();
});
}