SkateSpots/public/boot-guard.js

123 lines
5.4 KiB
JavaScript

/*
BOOT CRASH GUARD (native-safe).
Loaded as an external script so the web build can ship a strict
Content-Security-Policy (script-src 'self') with no 'unsafe-inline'.
Paints any uncaught boot error directly to the screen using inline styles
only — so a failure that would otherwise be a silent white screen becomes a
readable message on device. If the app boots normally it clears itself.
*/
(function () {
var shown = false;
// Reload-loop breaker. A stale app-shell worker can reload before the boot
// guard's 12s error panel is ever shown. After three loads inside one minute,
// clear the shell cache/worker once and reload into a clean shell.
try {
var ATTEMPT_KEY = 'skatespots:boot-attempts';
var now = Date.now();
var attempts = { count: 0, at: now };
try {
var stored = JSON.parse(sessionStorage.getItem(ATTEMPT_KEY) || 'null');
if (stored && typeof stored.at === 'number' && now - stored.at < 60000) {
attempts.count = Number(stored.count) || 0;
}
} catch (e) {}
attempts.count += 1;
attempts.at = now;
try { sessionStorage.setItem(ATTEMPT_KEY, JSON.stringify(attempts)); } catch (e) {}
if (attempts.count >= 3) {
try { sessionStorage.removeItem(ATTEMPT_KEY); } catch (e) {}
var finished = false;
var finish = function () {
if (finished) return;
finished = true;
location.reload();
};
var jobs = [];
try {
if ('serviceWorker' in navigator) {
jobs.push(navigator.serviceWorker.getRegistrations().then(function (regs) {
regs.forEach(function (reg) { try { reg.unregister(); } catch (e) {} });
}).catch(function () {}));
}
if (window.caches) {
jobs.push(window.caches.keys().then(function (keys) {
return Promise.all(keys.filter(function (key) { return key.indexOf('skate-spots-shell-') === 0; })
.map(function (key) { return window.caches.delete(key).catch(function () {}); }));
}).catch(function () {}));
}
} catch (e) {}
if (jobs.length) Promise.all(jobs).then(finish).catch(finish);
else finish();
setTimeout(finish, 1000);
return;
}
} catch (e) { /* best effort */ }
// Native WebViews can keep a service worker from an older build forever.
// That worker can serve a stale index.html whose hashed chunks no longer
// exist, producing a white/black reload loop. The native shell is served from
// https://localhost; clear its app-shell registrations before the bundle runs.
try {
var isNativeShell = location.protocol === 'https:' && location.hostname === 'localhost';
if (isNativeShell && 'serviceWorker' in navigator) {
navigator.serviceWorker.getRegistrations().then(function (regs) {
regs.forEach(function (reg) { try { reg.unregister(); } catch (e) {} });
}).catch(function () {});
if (window.caches) {
window.caches.keys().then(function (keys) {
keys.filter(function (key) { return key.indexOf('skate-spots-shell-') === 0; })
.forEach(function (key) { window.caches.delete(key).catch(function () {}); });
}).catch(function () {});
}
}
} catch (e) { /* best effort */ }
function paint(kind, err) {
if (shown) return;
shown = true;
try {
var msg = (err && (err.stack || err.message)) ? (err.stack || err.message) : String(err);
var el = document.createElement('div');
el.setAttribute('style', [
'position:fixed', 'inset:0', 'z-index:2147483647',
'background:#000', 'color:#fff', 'padding:20px',
'font:13px/1.5 -apple-system,system-ui,monospace',
'overflow:auto', '-webkit-overflow-scrolling:touch'
].join(';'));
var head = document.createElement('div');
head.setAttribute('style', 'color:#ff0040;font-weight:700;font-size:16px;margin-bottom:8px');
head.textContent = 'SkateSpots failed to start';
var sub = document.createElement('div');
sub.setAttribute('style', 'color:#9ca3af;margin-bottom:12px');
sub.textContent = kind;
var pre = document.createElement('pre');
pre.setAttribute('style', 'white-space:pre-wrap;word-break:break-word;color:#f3f4f6;margin:0');
pre.textContent = msg || '(no message)';
var btn = document.createElement('button');
btn.setAttribute('style', 'margin-top:16px;padding:10px 16px;background:#ff0040;color:#000;border:0;border-radius:8px;font-weight:700');
btn.textContent = 'Reload';
btn.addEventListener('click', function () { location.reload(); });
el.appendChild(head);
el.appendChild(sub);
el.appendChild(pre);
el.appendChild(btn);
document.body.appendChild(el);
} catch (e) { /* last resort: nothing we can do */ }
}
window.addEventListener('error', function (e) {
paint('Uncaught error', e.error || e.message);
});
window.addEventListener('unhandledrejection', function (e) {
paint('Unhandled promise rejection', e.reason);
});
// If nothing has rendered into #root after 12s, surface a hint.
setTimeout(function () {
var root = document.getElementById('root');
if (root && root.childElementCount === 0) {
paint('App did not render (timeout)', 'The bundle loaded but nothing mounted within 12s. This usually means a boot-time exception was swallowed.');
}
}, 12000);
// Let the app signal a clean boot to cancel the timeout guard.
window.__SKATESPOTS_BOOTED__ = function () { shown = true; };
})();