feat(tickets): Web-Push-Benachrichtigungen (PWA)

- 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>
This commit is contained in:
2026-06-23 11:32:26 +02:00
parent 750619d3d7
commit f7a1a7724b
19 changed files with 2405 additions and 3 deletions

View File

@@ -447,6 +447,13 @@ export async function installMockApi(page: Page): Promise<MockDb> {
}
return json(route, 405)
}
// WEB-PUSH: im Mock deaktiviert (keine VAPID-Schlüssel) → PushToggle blendet sich aus.
if (path === '/push/vapid-public-key' && method === 'GET') {
return json(route, 200, { enabled: false, publicKey: null })
}
if ((path === '/push/subscribe' || path === '/push/unsubscribe') && method === 'POST') {
return json(route, 200, {})
}
// FEEDBACK-ANHÄNGE: hochladen (POST /feedback/{id}/attachments).
const attUpload = path.match(/^\/feedback\/([^/]+)\/attachments$/)
if (attUpload && method === 'POST') {