- 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>
65 lines
1.8 KiB
TypeScript
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>
|
|
)
|
|
}
|