Files
Inventarsystem/Web/static/service-worker.js
T
Aiirondev_dev f98ad5d0ee Implement Web Push Notifications system
- Add push_notifications.py module for managing subscriptions and sending notifications
- Create Flask API endpoints for subscription management (/api/push/*)
- Integrate push sending with existing notification system (_create_notification)
- Create frontend JavaScript library (push-notifications.js) for client-side subscription
- Update base.html template with manifest link and Service Worker registration
- Add manifest.json and service-worker.js for PWA support
- Add pywebpush dependency to requirements.txt
- Create generate-vapid-keys.sh script for VAPID key generation
- Add comprehensive documentation (WEB_PUSH_NOTIFICATIONS.md)

Features:
- Subscribe/unsubscribe users from push notifications
- Store subscriptions in MongoDB with automatic indexing
- Send push to individual users or all admins
- Notification deduplication and versioning
- Service Worker handles offline caching and push events
- Full PWA support for mobile app installation
- VAPID authentication for secure push communication
2026-04-19 13:59:00 +02:00

186 lines
6.0 KiB
JavaScript

/**
* Service Worker for Web Push Notifications
* Handles incoming push events and displays notifications
*/
const CACHE_NAME = 'inventarsystem-v1';
const ASSETS_TO_CACHE = [
'/',
'/static/css/styles.css',
'/static/js/scripts.js',
'/offline.html'
];
// Install event - cache assets
self.addEventListener('install', (event) => {
console.log('[Service Worker] Installing...');
event.waitUntil(
caches.open(CACHE_NAME).then((cache) => {
return cache.addAll(ASSETS_TO_CACHE).catch((err) => {
console.warn('[Service Worker] Cache add error:', err);
// Don't fail the installation
return Promise.resolve();
});
})
);
self.skipWaiting();
});
// Activate event - clean up old caches
self.addEventListener('activate', (event) => {
console.log('[Service Worker] Activating...');
event.waitUntil(
caches.keys().then((cacheNames) => {
return Promise.all(
cacheNames.map((cacheName) => {
if (cacheName !== CACHE_NAME) {
console.log('[Service Worker] Deleting old cache:', cacheName);
return caches.delete(cacheName);
}
})
);
})
);
self.clients.claim();
});
// Push event - handle incoming push notifications
self.addEventListener('push', (event) => {
console.log('[Service Worker] Push notification received');
let notificationData = {
title: 'Inventarsystem',
body: 'Neue Benachrichtigung',
icon: '/static/img/logo-192x192.png',
badge: '/static/img/badge-72x72.png',
tag: 'inventarsystem-notification',
requireInteraction: false,
};
// Parse the push event data if available
if (event.data) {
try {
const data = event.data.json();
notificationData = {
title: data.title || notificationData.title,
body: data.message || data.body || notificationData.body,
icon: data.icon || notificationData.icon,
badge: data.badge || notificationData.badge,
tag: data.tag || notificationData.tag,
requireInteraction: data.requireInteraction || false,
data: {
url: data.url || '/',
reference: data.reference || null,
type: data.type || 'info',
},
};
console.log('[Service Worker] Push data parsed:', notificationData);
} catch (err) {
console.error('[Service Worker] Failed to parse push data:', err);
notificationData.body = event.data.text();
}
}
event.waitUntil(
self.registration.showNotification(notificationData.title, {
body: notificationData.body,
icon: notificationData.icon,
badge: notificationData.badge,
tag: notificationData.tag,
requireInteraction: notificationData.requireInteraction,
data: notificationData.data,
actions: [
{
action: 'open',
title: 'Öffnen',
icon: '/static/img/open-icon.png'
},
{
action: 'close',
title: 'Schließen',
icon: '/static/img/close-icon.png'
}
],
})
);
});
// Notification click event - handle user clicks on notifications
self.addEventListener('notificationclick', (event) => {
console.log('[Service Worker] Notification clicked:', event.action);
event.notification.close();
const urlToOpen = event.notification.data.url || '/';
event.waitUntil(
clients.matchAll({
type: 'window',
includeUncontrolled: true
}).then((windowClients) => {
// Check if the app is already open
for (let i = 0; i < windowClients.length; i++) {
const client = windowClients[i];
if (client.url === urlToOpen && 'focus' in client) {
return client.focus();
}
}
// If not open, open a new window
if (clients.openWindow) {
return clients.openWindow(urlToOpen);
}
})
);
});
// Notification close event - track when users dismiss notifications
self.addEventListener('notificationclose', (event) => {
console.log('[Service Worker] Notification closed');
// Could send analytics here
});
// Background fetch event (optional, for large data syncs)
self.addEventListener('sync', (event) => {
console.log('[Service Worker] Background sync triggered:', event.tag);
if (event.tag === 'sync-notifications') {
event.waitUntil(
fetch('/api/notifications/sync').then((response) => {
console.log('[Service Worker] Notifications synced');
})
);
}
});
// Fetch event - serve cached content when offline
self.addEventListener('fetch', (event) => {
// Skip non-GET requests
if (event.request.method !== 'GET') {
return;
}
// Skip API calls - always use network
if (event.request.url.includes('/api/')) {
return;
}
event.respondWith(
caches.match(event.request).then((response) => {
if (response) {
return response;
}
return fetch(event.request).then((response) => {
// Cache successful responses
if (response && response.status === 200) {
const responseToCache = response.clone();
caches.open(CACHE_NAME).then((cache) => {
cache.put(event.request, responseToCache);
});
}
return response;
}).catch(() => {
// Return offline page
return caches.match('/offline.html');
});
})
);
});