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.