PageSourceSearch

https://riederalm.lovable.app/sw.js

js riederalm.lovable.app collected 2026-10-03 03:45:10 UTC 13,341 bytes, 417 lines download raw bytes

1// Service Worker für Push-Benachrichtigungen - Riederalm
2// Version: 3.2.0 - iOS push reliability improvements
3const SW_VERSION = '3.3.0';
4const SW_BUILD = Date.now();
5
6console.log(`[SW v${SW_VERSION}] ===== SERVICE WORKER LOADING =====`);
7console.log(`[SW v${SW_VERSION}] Build timestamp: ${SW_BUILD}`);
8console.log(`[SW v${SW_VERSION}] User Agent: ${self.navigator?.userAgent || 'N/A'}`);
9
10// Badge management helpers
11let badgeCount = 0;
12
13async function getBadgeCount() {
14  try {
15    const cache = await caches.open('badge-cache');
16    const response = await cache.match('badge-count');
17    if (response) {
18      const data = await response.json();
19      badgeCount = data.count || 0;
20      return badgeCount;
21    }
22  } catch (e) {
23    console.log('[SW] Could not retrieve badge count from cache');
24  }
25  return 0;
26}
27
28async function storeBadgeCount(count) {
29  try {
30    const cache = await caches.open('badge-cache');
31    await cache.put('badge-count', new Response(JSON.stringify({ count })));
32    badgeCount = count;
33  } catch (e) {
34    console.log('[SW] Could not store badge count in cache');
35  }
36}
37
38// iOS Detection
39function isIOS() {
40  return /iPhone|iPad|iPod/.test(self.navigator?.userAgent || '');
41}
42
43// Android Detection
44function isAndroid() {
45  return /Android/.test(self.navigator?.userAgent || '');
46}
47
48// Check if running as PWA
49function isPWA() {
50  return true; // Assume PWA if SW is running
51}
52
53// Set badge count
54async function setBadgeCount(count) {
55  console.log(`[SW v${SW_VERSION}] 🔢 Setting badge count: ${count}, iOS: ${isIOS()}, Android: ${isAndroid()}`);
56  
57  try {
58    await storeBadgeCount(count);
59    
60    if (!isIOS() && 'setAppBadge' in self.navigator) {
61      await self.navigator.setAppBadge(count);
62      console.log(`[SW v${SW_VERSION}] ✅ Badge API updated to: ${count}`);
63    } else {
64      console.log(`[SW v${SW_VERSION}] ℹ️ iOS detected or Badge API not available, badge from payload`);
65    }
66  } catch (err) {
67    console.error(`[SW v${SW_VERSION}] ❌ Badge update failed:`, err);
68  }
69}
70
71// CRITICAL: Force immediate activation on install
72self.addEventListener('install', (event) => {
73  console.log(`[SW v${SW_VERSION}] 🔧 Service Worker INSTALLING`);
74  console.log(`[SW v${SW_VERSION}] 🔧 Calling skipWaiting() to activate immediately`);
75  
76  event.waitUntil(
77    self.skipWaiting().then(() => {
78      console.log(`[SW v${SW_VERSION}] ✅ skipWaiting() completed`);
79    }).catch((err) => {
80      console.error(`[SW v${SW_VERSION}] ❌ skipWaiting() failed:`, err);
81    })
82  );
83});
84
85self.addEventListener('activate', (event) => {
86  console.log(`[SW v${SW_VERSION}] 🚀 Service Worker ACTIVATING`);
87  
88  event.waitUntil(
89    (async () => {
90      await self.clients.claim();
91      console.log(`[SW v${SW_VERSION}] ✅ clients.claim() completed`);
92
93      // Alte Runtime-Caches entfernen, damit neue Deployments durchkommen
94      try {
95        const keys = await caches.keys();
96        await Promise.all(
97          keys
98            .filter((key) => key.startsWith('runtime-v') && key !== `runtime-v${SW_VERSION}`)
99            .map((key) => caches.delete(key))
100        );
101      } catch (e) {
102        console.log('[SW] Runtime-Cache-Cleanup fehlgeschlagen');
103      }
104      
105      const clients = await self.clients.matchAll({ type: 'window' });
106      console.log(`[SW v${SW_VERSION}] 📢 Notifying ${clients.length} client(s) of activation`);
107      
108      for (const client of clients) {
109        client.postMessage({ 
110          type: 'SW_ACTIVATED',
111          version: SW_VERSION,
112          build: SW_BUILD
113        });
114      }
115      
116      // Force reload all clients to pick up new code
117      for (const client of clients) {
118        client.postMessage({ type: 'SW_FORCE_RELOAD' });
119      }
120      
121      const cacheNames = await caches.keys();
122      console.log(`[SW v${SW_VERSION}] 🧹 Found ${cacheNames.length} cache(s)`);
123      
124      try {
125        if ('periodicSync' in self.registration) {
126          await self.registration.periodicSync.register('push-keepalive', {
127            minInterval: 15 * 60 * 1000
128          });
129          console.log(`[SW v${SW_VERSION}
129] ✅ Periodic Background Sync registered`);
130        }
131      } catch (e) {
132        console.log(`[SW v${SW_VERSION}] ℹ️ Periodic sync not available:`, e.message);
133      }
134    })()
135  );
136});
137
138// Periodic Background Sync handler
139self.addEventListener('periodicsync', (event) => {
140  if (event.tag === 'push-keepalive') {
141    console.log(`[SW v${SW_VERSION}] 🔄 Keep-alive ping from periodic sync`);
142    event.waitUntil(Promise.resolve());
143  }
144});
145
146// Listen for messages from the app
147self.addEventListener('message', (event) => {
148  console.log(`[SW v${SW_VERSION}] 📨 Message received:`, event.data);
149  
150  if (event.data && event.data.type === 'SKIP_WAITING') {
151    console.log(`[SW v${SW_VERSION}] ⚡ SKIP_WAITING received, activating NOW`);
152    self.skipWaiting();
153  }
154  
155  if (event.data && event.data.type === 'GET_VERSION') {
156    if (event.source) {
157      event.source.postMessage({
158        type: 'SW_VERSION',
159        version: SW_VERSION,
160        build: SW_BUILD
161      });
162    }
163  }
164  
165  if (event.data && event.data.type === 'PING') {
166    if (event.source) {
167      event.source.postMessage({
168        type: 'PONG',
169        version: SW_VERSION,
170        timestamp: Date.now()
171      });
172    }
173  }
174  
175  // KEEPALIVE handler - keeps SW active for iOS push reliability
176  if (event.data && event.data.type === 'KEEPALIVE') {
177    console.log(`[SW v${SW_VERSION}] 💓 Keepalive ping received at ${new Date().toISOString()}`);
178    if (event.source) {
179      event.source.postMessage({
180        type: 'KEEPALIVE_ACK',
181        version: SW_VERSION,
182        timestamp: Date.now()
183      });
184    }
185  }
186});
187
188// Handle navigation events
189// Runtime-Cache für die App-Shell (Handheld offline nutzbar).
190// Absichtlich OHNE hart codierte Build-Dateinamen: der Cache füllt sich zur
191// Laufzeit (Stale-While-Revalidate), alte Versionen werden beim Activate entfernt.
192const RUNTIME_CACHE = `runtime-v${SW_VERSION}`;
193
194async function staleWhileRevalidate(request) {
195  const cache = await caches.open(RUNTIME_CACHE);
196  const cached = await cache.match(request);
197  const network = fetch(request)
198    .then((response) => {
199      if (response && response.ok && response.type !== 'opaque') {
200        cache.put(request, response.clone()).catch(() => {});
201      }
202      return response;
203    })
204    .catch(() => null);
205
206  if (cached) {
207    network.catch(() => {});
208    return cached;
209  }
210  const fresh = await network;
211  if (fresh) return fresh;
212  throw new Error('offline und nicht im Cache');
213}
214
215self.addEventListener('fetch', (event) => {
216  const request = event.request;
217
218  if (request.mode === 'navigate') {
219    event.respondWith(
220      fetch(request).catch(async () => {
221        const cache = await caches.open(RUNTIME_CACHE);
222        return (await cache.match('/')) || (await caches.match('/')) || fetch('/');
223      })
224    );
225    return;
226  }
227
228  if (request.method !== 'GET') return;
229
230  let url;
231  try {
232    url = new URL(request.url);
233  } catch {
234    return;
235  }
236  if (url.origin !== self.location.origin) return;
237  if (url.pathname.startsWith('/sw.js')) return;
238
239  const isAsset =
240    /\.(js|mjs|css|woff2?|ttf|png|svg|webp|ico)$/i.test(url.pathname) ||
241    url.pathname.startsWith('/assets/');
242  if (!isAsset) return;
243
244  event.respondWith(staleWhileRevalidate(request).catch(() => fetch(request)));
245});
246
247// PUSH EVENT - iOS-compatible with robust fallbacks
248// CRITICAL: On iOS PWA, showNotification() MUST be called synchronously within waitUntil
249// or iOS will silently drop the notification. Keep this handler as lean as possible.
250self.addEventListener('push', (event) => {
251  console.log(`[SW v${SW_VERSION}] 🔔 PUSH RECEIVED at ${new Date().toISOString()}`);
252  
253  // Parse payload synchronously before any async work
254  let title = 'Riederalm';
255  let body = 'Neue Benachrichtigung';
256  let icon = '/icons/icon-192x192.png';
257  let tag = `push-${Date.now()}`;
258  let data = {};
259  
260  if (event.data) {
261    try {
262      const payload = event.data.json();
263      console.log(`[SW v${SW_VERSION}] ✅ Payload parsed:`, payload.title);
264      title = payload.title || title;
265      body = payload.body || body;
266      icon = payload.icon || icon;
267      tag = payload.tag ? `${payload.tag}-${Date.now()}` : tag;
268      data = payload.data || {};
269      if (!data.url && payload.url) {
270        data.url = payload.url;
271      }
272    } catch (e) {
273      console.log(`[SW v${SW_VERSION}] ⚠️ Parse error, using defaults`);
274    }
275  }
276  
277  const isIOSDevice = isIOS();
278  const isAndroidDevice = isAndroid();
279  
280  // Build notification options - minimal for iOS compatibility
281  const options = {
282    body,
283    icon,
284    tag,
285    data,
286    requireInteraction: !isIOSDevice, // iOS ignores this, avoid potential issues
287    renotify: true
288  };
289  
290  // Add vibration only for Android
291  if (isAndroidDevice) {
292    options.vibrate = [200, 100, 200];
293  }
294  
295  console.log(`[SW v${SW_VERSION}] 📱 Showing notification: "${title}" (${isIOSDevice ? 'iOS' : isAndroidDevice ? 'Android' : 'Web'})`);
296  
297  // CRITICAL: Use event.waitUntil with the showNotification promise directly
298  // This ensures iOS doesn't kill the SW before the notification is displayed
299  const notificationPromise = self.registration.showNotification(title, options)
300    .then(() => {
301      console.log(`[SW v${SW_VERSION}] ✅ Notification shown successfully!`);
302    })
303    .catch((error) => {
304      console.error(`[SW v${SW_VERSION}] ❌ showNotification failed:`, error);
305      // Last resort fallback with absolute minimum options
306      return self.registration.showNotification('Riederalm', {
307        body: 'Neue Benachrichtigung',
308        icon: '/icons/icon-192x192.png'
309      });
310    });
311  
312  // Chain client notification after showing the system notification
313  const fullPromise = notificationPromise.then(async () => {
314    // Forward to all open app windows for in-app toast (especially for iOS foreground)
315    try {
316      const allClients = await self.clients.matchAll({ type: 'window', includeUncontrolled: true });
317      console.log(`[SW v${SW_VERSION}] 📢 Forwarding push to ${allClients.length} client(s)`);
318      allClients.forEach(client => {
319        client.postMessage({
320          type: 'PUSH_RECEIVED',
321          payload: { title, body, data }
322        });
323      });
324    } catch (e) {
325      console.log(`[SW v${SW_VERSION}] ⚠️ Could not notify clients of push`);
326    }
327  });
328  
329  event.waitUntil(fullPromise);
330});
331
332self.addEventListener('notificationclick', (event) => {
333  console.log(`[SW v${SW_VERSION}] 🖱️ Notification clicked:`, event.action);
334  
335  event.notification.close();
336  
337  const action = event.action;
338  const data = event.notification.data;
339  const isIOSDevice = isIOS();
340
341  // Determine target URL - Riederalm routes
342  let url = '/notifications';
343  if (data?.url) {
344    url = data.url;
345  } else if (data?.type === 'chat') {
346    url = '/chat';
347  }
348  
349  event.waitUntil(
350    clients.matchAll({ type: 'window', includeUncontrolled: true }).then(async (clientList) => {
351      console.log(`[SW v${SW_VERSION}] 📢 Broadcasting session refresh to ${clientList.length} client(s)`);
352      for (const client of clientList) {
353        client.postMessage({
354          type: 'REFRESH_SESSION_BEFORE_NAVIGATION',
355          url: url
356        });
357      }
358      
359      await new Promise(resolve => setTimeout(resolve, 100));
360      
361      for (const client of clientList) {
362        if (client.url.includes(url.split('?')[0]) && 'focus' in client) {
363          return client.focus();
364        }
365      }
366      
367      // Try to focus any existing window and navigate
368      for (const client of clientList) {
369        if (client.url.includes(self.location.origin) && 'focus' in client) {
370          try {
371            await client.focus();
372            if ('navigate' in client) {
373              await client.navigate(url);
374            } else {
375              client.postMessage({ type: 'NAVIGATE', url: url });
376            }
377            return;
378          } catch (e) {
379            console.log(`[SW v${SW_VERSION}] ⚠️ Focus/navigate error:`, e);
380          }
381        }
382      }
383      
384      // Open new window as last resort
385      if (clients.openWindow) {
386        return clients.openWindow(url);
387      }
388    })
389  );
390});
391
392// Subscription change handler - attempt auto re-subscribe (Riederalm-specific)
393self.addEventListener('pushsubscriptionchange', (event) => {
394  console.log(`[SW v${SW_VERSION}] 🔄 Push subscription changed - attempting re-subscribe`);
395  event.waitUntil((async () => {
396    let hasNewSubscription = false;
397    
398    try {
399      if (event.oldSubscription) {
400        const newSubscription = await self.registration.pushManager.subscribe(
401          event.oldSubscription.options
402        );
403        console.log(`[SW v${SW_VERSION}] ✅ Re-subscribed successfully:`, newSubscription.endpoint.substring(0, 50));
404        hasNewSubscription = true;
405      }
406    } catch (e) {
407      console.warn(`[SW v${SW_VERSION}] ⚠️ Auto re-subscribe failed:`, e);
408    }
409
410    const allClients = await self.clients.matchAll({ type: 'window', includeUncontrolled: true });
411    for (const c of allClients) {
412      c.postMessage({ type: 'PUSH_SUBSCRIPTION_CHANGED', hasNewSubscription });
413    }
414  })());
415});
416
417console.log(`[SW v${SW_VERSION}] ===== SERVICE WORKER LOADED =====`);

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.