localgreenchain/public/sw.js
Claude 62c1ded598
Add comprehensive notification system (Agent 8)
Implement multi-channel notification system with:
- Core notification service with email, push, and in-app channels
- Email templates for all notification types (welcome, plant registered,
  transport alerts, farm alerts, harvest ready, demand matches, weekly digest)
- Push notification support with VAPID authentication
- In-app notification management with read/unread tracking
- Notification scheduler for recurring and scheduled notifications
- API endpoints for notifications CRUD, preferences, and subscriptions
- UI components (NotificationBell, NotificationList, NotificationItem,
  PreferencesForm)
- Full notifications page with preferences management
- Service worker for push notification handling
2025-11-23 03:52:41 +00:00

206 lines
5.5 KiB
JavaScript

/**
* Service Worker for Push Notifications
* LocalGreenChain PWA Support
*/
// Cache name versioning
const CACHE_NAME = 'lgc-cache-v1';
// Files to cache for offline support
const STATIC_CACHE = [
'/',
'/offline',
'/icons/icon-192x192.png'
];
// Install event - cache static assets
self.addEventListener('install', (event) => {
console.log('[SW] Installing service worker...');
event.waitUntil(
caches.open(CACHE_NAME)
.then((cache) => {
console.log('[SW] Caching static assets');
return cache.addAll(STATIC_CACHE);
})
.catch((error) => {
console.log('[SW] Failed to cache:', error);
})
);
self.skipWaiting();
});
// Activate event - clean up old caches
self.addEventListener('activate', (event) => {
console.log('[SW] Activating service worker...');
event.waitUntil(
caches.keys().then((cacheNames) => {
return Promise.all(
cacheNames
.filter((name) => name !== CACHE_NAME)
.map((name) => {
console.log('[SW] Deleting old cache:', name);
return caches.delete(name);
})
);
})
);
self.clients.claim();
});
// Push event - handle incoming push notifications
self.addEventListener('push', (event) => {
console.log('[SW] Push notification received');
let data = {
title: 'LocalGreenChain',
body: 'You have a new notification',
icon: '/icons/icon-192x192.png',
badge: '/icons/badge-72x72.png',
data: {}
};
if (event.data) {
try {
data = { ...data, ...event.data.json() };
} catch (e) {
data.body = event.data.text();
}
}
const options = {
body: data.body,
icon: data.icon,
badge: data.badge,
vibrate: [100, 50, 100],
data: data.data,
actions: data.actions || [
{ action: 'view', title: 'View', icon: '/icons/check.png' },
{ action: 'dismiss', title: 'Dismiss', icon: '/icons/close.png' }
],
tag: data.tag || 'lgc-notification',
renotify: true,
requireInteraction: data.requireInteraction || false
};
event.waitUntil(
self.registration.showNotification(data.title, options)
);
});
// Notification click event - handle user interaction
self.addEventListener('notificationclick', (event) => {
console.log('[SW] Notification clicked:', event.action);
event.notification.close();
if (event.action === 'dismiss') {
return;
}
// Default action or 'view' action
const urlToOpen = event.notification.data?.url || '/notifications';
event.waitUntil(
clients.matchAll({ type: 'window', includeUncontrolled: true })
.then((windowClients) => {
// Check if there's already a window open
for (const client of windowClients) {
if (client.url.includes(self.location.origin) && 'focus' in client) {
client.navigate(urlToOpen);
return client.focus();
}
}
// Open new window if none found
if (clients.openWindow) {
return clients.openWindow(urlToOpen);
}
})
);
});
// Notification close event
self.addEventListener('notificationclose', (event) => {
console.log('[SW] Notification closed');
// Track notification dismissal if needed
const notificationData = event.notification.data;
if (notificationData?.trackDismissal) {
// Could send analytics here
}
});
// Fetch event - network-first with cache fallback
self.addEventListener('fetch', (event) => {
// Skip non-GET requests
if (event.request.method !== 'GET') return;
// Skip API requests
if (event.request.url.includes('/api/')) return;
event.respondWith(
fetch(event.request)
.then((response) => {
// Clone the response for caching
const responseClone = response.clone();
caches.open(CACHE_NAME).then((cache) => {
cache.put(event.request, responseClone);
});
return response;
})
.catch(() => {
// Return cached response if available
return caches.match(event.request)
.then((cachedResponse) => {
if (cachedResponse) {
return cachedResponse;
}
// Return offline page for navigation requests
if (event.request.mode === 'navigate') {
return caches.match('/offline');
}
return new Response('Offline', { status: 503 });
});
})
);
});
// Message event - handle messages from client
self.addEventListener('message', (event) => {
console.log('[SW] Message received:', event.data);
if (event.data && event.data.type === 'SKIP_WAITING') {
self.skipWaiting();
}
if (event.data && event.data.type === 'GET_VERSION') {
event.ports[0].postMessage({ version: CACHE_NAME });
}
});
// Periodic sync for background updates (if supported)
self.addEventListener('periodicsync', (event) => {
if (event.tag === 'check-notifications') {
event.waitUntil(checkForNewNotifications());
}
});
async function checkForNewNotifications() {
try {
const response = await fetch('/api/notifications?unreadOnly=true&limit=1');
const data = await response.json();
if (data.success && data.data.unreadCount > 0) {
self.registration.showNotification('LocalGreenChain', {
body: `You have ${data.data.unreadCount} unread notification(s)`,
icon: '/icons/icon-192x192.png',
badge: '/icons/badge-72x72.png'
});
}
} catch (error) {
console.log('[SW] Failed to check notifications:', error);
}
}
console.log('[SW] Service worker loaded');