Files
GerbilManager/gerbil-manager-web/src/components/PushToggle.tsx
Gulum f7a1a7724b 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>
2026-06-23 11:32:26 +02:00

65 lines
1.8 KiB
TypeScript

/**
* Schalter „Benachrichtigungen aktivieren" (Web-Push / PWA).
* Rendert nichts, wenn Push nicht unterstützt wird ODER der Server keine VAPID-Schlüssel hat
* (z. B. im e2e-Mock) — so bleibt die UI dort unverändert.
*/
import { useEffect, useState } from 'react'
import { de } from '../strings/de'
import { useToast } from './toast'
import { disablePush, enablePush, fetchPushConfig, isPushSupported, isSubscribed } from '../push'
export default function PushToggle() {
const t = de.feedback.tickets
const toast = useToast()
const [available, setAvailable] = useState(false)
const [subscribed, setSubscribed] = useState(false)
const [busy, setBusy] = useState(false)
useEffect(() => {
let cancelled = false
;(async () => {
if (!isPushSupported()) return
const cfg = await fetchPushConfig()
if (cancelled || !cfg.enabled) return
setAvailable(true)
setSubscribed(await isSubscribed())
})()
return () => {
cancelled = true
}
}, [])
if (!available) return null
async function toggle() {
setBusy(true)
try {
if (subscribed) {
await disablePush()
setSubscribed(false)
toast.success(t.pushDisabledToast)
} else {
const res = await enablePush()
if (res === 'enabled') {
setSubscribed(true)
toast.success(t.pushEnabledToast)
} else if (res === 'denied') {
toast.error(t.pushDenied)
} else {
toast.error(t.pushUnavailable)
}
}
} catch {
toast.error(t.pushUnavailable)
} finally {
setBusy(false)
}
}
return (
<button type="button" className="btn tickets-push-toggle" onClick={toggle} disabled={busy}>
{subscribed ? `🔕 ${t.pushDisable}` : `🔔 ${t.pushEnable}`}
</button>
)
}