1/** 2 * Service Worker - Bowling Houseleague App 3 * Network-first: Immer aktuelle Version laden, Cache nur als Offline-Fallback. 4 * + Firebase Cloud Messaging Push Notifications 5 */ 6 7// Firebase SDK für Background Messaging 8importScripts('https://www.gstatic.com/firebasejs/10.7.1/firebase-app-compat.js'); 9importScripts('https://www.gstatic.com/firebasejs/10.7.1/firebase-messaging-compat.js'); 10 11// Firebase initialisieren (nur für FCM im SW) 12firebase.initializeApp({ 13 apiKey: "AIzaSyDwEZWUkpWeADc5pSPKlLCj8X3ZjudXHt8", 14 authDomain: "after-eight-liga.firebaseapp.com", 15 projectId: "after-eight-liga", 16 storageBucket: "after-eight-liga.firebasestorage.app", 17 messagingSenderId: "177858182876", 18 appId: "1:177858182876:web:bf8ae2c20839cb6d263e19" 19}); 20 21const messaging = firebase.messaging(); 22 23// Nur Same-Origin-Ziele erlauben (sonst Open-Redirect über eine fremde Push-Payload 24// bzw. manipulierte fcmOptions.link) â bei fremder Origin auf "/" zurückfallen. 25function sanitizeNotifUrl(url) { 26 try { 27 const resolved = new URL(url, self.location.origin); 28 return resolved.origin === self.location.origin ? resolved.href : '/'; 29 } catch { 30 return '/'; 31 } 32} 33 34// Background-Nachrichten: Notification anzeigen wenn App nicht im Vordergrund 35messaging.onBackgroundMessage((payload) => { 36 const title = payload.notification?.title || 'Bowling Spieltag!'; 37 const options = { 38 body: payload.notification?.body || 'Es steht ein Spieltag an!', 39 icon: '/icon-192.png', 40 badge: '/icon-192.png', 41 tag: 'game-day-reminder', 42 renotify: true, 43 data: { url: sanitizeNotifUrl(payload.fcmOptions?.link || '/') } 44 }; 45 return self.registration.showNotification(title, options); 46}); 47 48// Klick auf Notification â App öffnen 49self.addEventListener('notificationclick', (e) => { 50 e.notification.close(); 51 const url = sanitizeNotifUrl(e.notification.data?.url || '/'); 52 e.waitUntil( 53 self.clients.matchAll({ type: 'window', includeUncontrolled: true }).then(clients => { 54 // Existierendes Fenster fokussieren 55 for (const client of clients) { 56 if (client.url.includes(self.location.origin) && 'focus' in client) { 57 // #27: bestehendes Fenster fokussieren UND zum Deep-Link navigieren 58 return client.focus().then(fc => ('navigate' in fc && url && url !== '/') ? fc.navigate(url).catch(() => {}) : fc); 59 } 60 } 61 // Neues Fenster öffnen 62 return self.clients.openWindow(url); 63 }) 64 ); 65}); 66 67const CACHE_NAME = 'ael-v71'; 68const STATIC_ASSETS = [ 69 '/', 70 '/index.html', 71 '/style.css', 72 '/script.js', 73 '/ui.js', 74 '/utils.js', 75 '/lanes.js', 76 '/config.js', 77 // L5 (Code-Review Teil B, 2026-09-15): fehlten bisher - alle drei werden per statischem 78 // ES-Modul-Import geladen (i18n.js/draw-show.js aus script.js, holidays.js aus ui.js) und 79 // waren dadurch offline (kein Netz, Cache-Fallback) eine Luecke. 80 '/i18n.js', 81 '/holidays.js', 82 '/draw-show.js', 83 '/manifest.json', 84 '/icon-192.png', 85 '/icon-512.png' 86]; 87 88// Install: Statische Assets cachen + sofort aktivieren 89self.addEventListener('install', (e) => { 90 e.waitUntil( 91 caches.open(CACHE_NAME) 92 .then(cache => cache.addAll(STATIC_ASSETS)) 93 .then(() => self.skipWaiting()) 94 ); 95}); 96 97// Activate: Alle alten Caches löschen + sofort übernehmen 98self.addEventListener('activate', (e) => { 99 e.waitUntil( 100 caches.keys().then(keys => 101 Promise.all(keys.filter(k => k !== CACHE_NAME).map(k => caches.delete(k))) 102 ).then(() => self.clients.claim()) 103 ); 104}); 105 106// Fetch: Network-first, Cache nur als Offline-Fallback 107self.addEventListener('fetch', (e) => { 108 const url = new URL(e.request.url); 109 110 // Firebase/API-Requests immer direkt durchlassen 111 if (url.hostname.includes('firebasedatabase.app') || 112 url.hostname.includes('googleapis.com') || 113 url.hostname.includes('gstatic.com') || 114 url.hostname.includes('google.com') || 115 url.hostname.includes('sentry')) { 116 return; 117 } 118 119 // Firebase Auth Handler nicht cachen (wichtig für signInWithRedirect) 120 if (url.pathname.startsWith('/__/auth/')) { 121 return; 122 } 123 124 // Eigene Assets: Network-first, Cache als Fallback 125 if (e.request.method === 'GET' && url.origin === self.location.origin) { 126 e.respondWith( 127 fetch(e.request).then(response => { 128 if (response.ok) { 129 const clone = response.clone(); 130 caches.open(CACHE_NAME).then(cache => cache.put(e.request, clone)); 131 } 132 return response; 133 // Sentry 146571286: Erst den EXAKTEN Treffer (mit ?v=) versuchen - ignoreSearch lieferte sonst 134 // sofort irgendeine Variante, auch die vorab gecachte alte /config.js zu neuem script.js. 135 // ignoreSearch bleibt zweiter Rueckfall, weil STATIC_ASSETS unversioniert precached werden. 136 }).catch(() => caches.match(e.request) 137 .then(r => r || caches.match(e.request, { ignoreSearch: true })) 138 .then(r => r || (e.request.mode === 'navigate' ? caches.match('/index.html', { ignoreSearch: true }) : undefined))) 139 ); 140 } 141});
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.