sw.js 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127
  1. // Service Worker for PilotDeck PWA
  2. // Cache only manifest (needed for PWA install). HTML and JS are never pre-cached
  3. // so a rebuild + refresh always picks up the latest assets.
  4. // Bump this token whenever a cached asset's contents change (icons, manifest).
  5. // The activate handler below purges every cache whose name doesn't match,
  6. // so existing PWAs pick up the new visuals on the next page load.
  7. const CACHE_NAME = 'politdeck-v1';
  8. const urlsToCache = [
  9. '/manifest.json'
  10. ];
  11. // Install event
  12. self.addEventListener('install', event => {
  13. event.waitUntil(
  14. caches.open(CACHE_NAME)
  15. .then(cache => cache.addAll(urlsToCache))
  16. );
  17. self.skipWaiting();
  18. });
  19. // Fetch event — network-first for everything except hashed assets
  20. self.addEventListener('fetch', event => {
  21. const url = event.request.url;
  22. // Never intercept API requests or WebSocket upgrades
  23. if (url.includes('/api/') || url.includes('/ws')) {
  24. return;
  25. }
  26. // Navigation requests (HTML) — always go to network, no caching
  27. if (event.request.mode === 'navigate') {
  28. event.respondWith(
  29. fetch(event.request).catch(() => caches.match('/manifest.json').then(() =>
  30. new Response('<h1>Offline</h1><p>Please check your connection.</p>', {
  31. headers: { 'Content-Type': 'text/html' }
  32. })
  33. ))
  34. );
  35. return;
  36. }
  37. // Hashed assets (JS/CSS in /assets/) — cache-first since filenames change per build
  38. if (url.includes('/assets/')) {
  39. event.respondWith(
  40. caches.match(event.request).then(cached => {
  41. if (cached) return cached;
  42. return fetch(event.request).then(response => {
  43. const clone = response.clone();
  44. caches.open(CACHE_NAME).then(cache => cache.put(event.request, clone));
  45. return response;
  46. });
  47. })
  48. );
  49. return;
  50. }
  51. // Everything else — network-first
  52. event.respondWith(
  53. fetch(event.request).catch(() => caches.match(event.request))
  54. );
  55. });
  56. // Activate event — purge old caches
  57. self.addEventListener('activate', event => {
  58. event.waitUntil(
  59. caches.keys().then(cacheNames =>
  60. Promise.all(
  61. cacheNames
  62. .filter(name => name !== CACHE_NAME)
  63. .map(name => caches.delete(name))
  64. )
  65. )
  66. );
  67. self.clients.claim();
  68. });
  69. // Push notification event
  70. self.addEventListener('push', event => {
  71. if (!event.data) return;
  72. let payload;
  73. try {
  74. payload = event.data.json();
  75. } catch {
  76. payload = { title: 'PilotDeck', body: event.data.text() };
  77. }
  78. const options = {
  79. body: payload.body || '',
  80. icon: '/logo-256.png',
  81. badge: '/logo-128.png',
  82. data: payload.data || {},
  83. tag: payload.data?.tag || `${payload.data?.sessionId || 'global'}:${payload.data?.code || 'default'}`,
  84. renotify: true
  85. };
  86. event.waitUntil(
  87. self.registration.showNotification(payload.title || 'PilotDeck', options)
  88. );
  89. });
  90. // Notification click event
  91. self.addEventListener('notificationclick', event => {
  92. event.notification.close();
  93. const sessionId = event.notification.data?.sessionId;
  94. const provider = event.notification.data?.provider || null;
  95. const urlPath = sessionId ? `/session/${sessionId}` : '/';
  96. event.waitUntil(
  97. self.clients.matchAll({ type: 'window', includeUncontrolled: true }).then(async clientList => {
  98. for (const client of clientList) {
  99. if (client.url.includes(self.location.origin)) {
  100. await client.focus();
  101. client.postMessage({
  102. type: 'notification:navigate',
  103. sessionId: sessionId || null,
  104. provider,
  105. urlPath
  106. });
  107. return;
  108. }
  109. }
  110. return self.clients.openWindow(urlPath);
  111. })
  112. );
  113. });