/** * Boot watchdog for production PWAs. * Recovers from white/black screens when stale HTML points to missing JS chunks. * Does not clear caches automatically while offline to protect unsynced data. */ (function () { if (location.hostname === 'localhost' || location.hostname === '127.0.0.1') { return } var BOOT_TIMEOUT_MS = 12000 var ATTEMPT_WINDOW_MS = 120000 var ATTEMPT_COUNT_KEY = 'pwa_boot_watchdog_attempt_count' var ATTEMPT_LAST_KEY = 'pwa_boot_watchdog_attempt_last_ts' var PENDING_EVENTS_KEY = 'pwa_boot_pending_events' var MAX_PENDING_EVENTS = 12 function enqueueEvent(name, props) { try { var current = JSON.parse(sessionStorage.getItem(PENDING_EVENTS_KEY) || '[]') if (!Array.isArray(current)) current = [] current.push({ name: name, props: props, ts: Date.now() }) if (current.length > MAX_PENDING_EVENTS) { current = current.slice(current.length - MAX_PENDING_EVENTS) } sessionStorage.setItem(PENDING_EVENTS_KEY, JSON.stringify(current)) } catch (_) { /* ignore analytics queue errors */ } } function emit(name, props) { if (typeof window.plausible === 'function') { if (props && Object.keys(props).length > 0) { window.plausible(name, { props: props }) } else { window.plausible(name) } return } enqueueEvent(name, props) } function hasBootstrapped() { return window.__KDB_APP_BOOTSTRAPPED === true } function resetAttempts() { try { sessionStorage.removeItem(ATTEMPT_COUNT_KEY) sessionStorage.removeItem(ATTEMPT_LAST_KEY) } catch (_) { /* ignore storage errors */ } } function nextAttempt() { try { var now = Date.now() var last = Number(sessionStorage.getItem(ATTEMPT_LAST_KEY) || '0') var count = Number(sessionStorage.getItem(ATTEMPT_COUNT_KEY) || '0') if (now - last > ATTEMPT_WINDOW_MS) { count = 0 } count += 1 sessionStorage.setItem(ATTEMPT_COUNT_KEY, String(count)) sessionStorage.setItem(ATTEMPT_LAST_KEY, String(now)) return count } catch (_) { return 1 } } function createRecoveryUrl(reason) { try { var url = new URL(location.href) url.searchParams.set('boot_recover', reason) url.searchParams.set('_', String(Date.now())) return url.toString() } catch (_) { return location.href } } async function clearServiceWorkerCaches() { if ('serviceWorker' in navigator) { try { var registrations = await navigator.serviceWorker.getRegistrations() await Promise.all( registrations.map(function (registration) { return registration.unregister() }) ) } catch (_) { /* ignore SW cleanup errors */ } } if ('caches' in window) { try { var keys = await caches.keys() await Promise.all( keys.map(function (key) { return caches.delete(key) }) ) } catch (_) { /* ignore cache cleanup errors */ } } } function renderFallback(isOffline) { var root = document.getElementById('root') if (!root) return root.innerHTML = '
' + (isOffline ? 'Die App konnte offline nicht sauber starten. Deine lokalen, nicht synchronisierten Daten bleiben erhalten.' : 'Die App konnte nicht sauber starten. Deine lokalen, nicht synchronisierten Daten bleiben erhalten.') + '
' + '' + (isOffline ? 'Bitte neu laden. Wenn wieder Netz verfügbar ist, kann die App-Engine automatisch repariert werden.' : 'Du kannst jetzt eine App-Reparatur ausfuehren, ohne IndexedDB-Logbuchdaten zu loeschen.') + '
' + '' + (!isOffline ? '' : '') + '