- Die Züchterin kann auf der Tickets-Seite Push-Benachrichtigungen aktivieren (🔔-Schalter). Service Worker ist BEWUSST push-only (kein fetch/Caching), damit das Laden der App nie beeinträchtigt wird. - Backend: WebPush-Bibliothek + VAPID; PushNotifier sendet an alle Abos, räumt veraltete (404/410) auf. Endpoints: GET /push/vapid-public-key, POST /push/subscribe|unsubscribe. - Ausgelöst über ein notify-Flag auf PUT /feedback: NUR die KI setzt es (z. B. neue Rückfrage / Ticket gelöst) → keine Selbst-Pushes durch Aktionen der Züchterin. Text wird aus dem Status abgeleitet, Klick öffnet das Ticket (?focus=). - Schalter/Logik blenden sich aus, wenn das Gerät kein Push kann oder der Server keine VAPID-Schlüssel hat (z. B. e2e-Mock). Migration WebPushSubscriptions. VAPID-Schlüssel in appsettings.json (lokale Single-User-App im Heimnetz — bewusste, vertretbare Vereinfachung). Tests: 264 Backend grün (+Push-Endpoints), e2e Tickets Desktop grün, vitest 149. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
50 lines
1.4 KiB
JavaScript
50 lines
1.4 KiB
JavaScript
/*
|
|
* Service Worker — NUR für Web-Push-Benachrichtigungen.
|
|
* BEWUSST OHNE fetch-Handler/Caching, damit das Laden der App niemals beeinflusst wird.
|
|
*/
|
|
self.addEventListener('install', () => self.skipWaiting())
|
|
self.addEventListener('activate', (event) => event.waitUntil(self.clients.claim()))
|
|
|
|
self.addEventListener('push', (event) => {
|
|
let data = {}
|
|
try {
|
|
data = event.data ? event.data.json() : {}
|
|
} catch {
|
|
data = {}
|
|
}
|
|
const title = data.title || 'Zucht der kleinen Chaoten'
|
|
const body = data.body || ''
|
|
const url = data.url || '/hilfe/tickets'
|
|
event.waitUntil(
|
|
self.registration.showNotification(title, {
|
|
body,
|
|
icon: '/icon-192.png',
|
|
badge: '/icon-192.png',
|
|
data: { url },
|
|
}),
|
|
)
|
|
})
|
|
|
|
self.addEventListener('notificationclick', (event) => {
|
|
event.notification.close()
|
|
const url = (event.notification.data && event.notification.data.url) || '/hilfe/tickets'
|
|
event.waitUntil(
|
|
self.clients.matchAll({ type: 'window', includeUncontrolled: true }).then((clients) => {
|
|
for (const client of clients) {
|
|
if ('focus' in client) {
|
|
if ('navigate' in client) {
|
|
try {
|
|
client.navigate(url)
|
|
} catch {
|
|
/* ignorieren */
|
|
}
|
|
}
|
|
return client.focus()
|
|
}
|
|
}
|
|
if (self.clients.openWindow) return self.clients.openWindow(url)
|
|
return undefined
|
|
}),
|
|
)
|
|
})
|