PageSourceSearch

https://after-eight-liga.web.app/sw.js

js after-eight-liga.web.app collected 2026-10-03 12:15:54 UTC 5,372 bytes, 141 lines download raw bytes

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.