Vier Review-Verbesserungen: - Suche real: /api/v1/search liefert echte unified Events+Orte (war Stub); Event-q trifft auch Venue-Namen; Startseite zeigt Orte-Treffer bei Suche. - Amenities/Öffnungszeiten/Region ausgespielt: Venue-Liste liefert openingHours/amenities/smoking/heroUrl, neuer amenity- + q-Filter; Orte-Seite mit Ausstattungs-/Region-Chips, „Jetzt geöffnet"-Badge (isOpenNow, über-Mitternacht-fest) + Amenity-Badges; Region-Chips auch auf der Startseite. - Folgen → Web Push (login-frei): kanal-agnostischer Kern (push_endpoints + venue_follows + notification_outbox, Migration 0005), Crawler-Auslöser (nur neue Events, best-effort), notification-worker (Drain + Ruhezeiten 22–8h + Pruning toter Endpoints), öffentliche /api/v1/push/*-Routen, Service Worker + lib/push.ts + Schalter auf /gemerkt (iOS-PWA-Hinweis). web-push-Lib (lädt unter Bun). Ohne VAPID-Keys bleibt Push schlafend (subscribe→503). Doku: docs/NOTIFICATIONS.md. Offen vor Push-Live: VAPID-Keys+SOPS, VVT-Eintrag. Tests: 112 API + 34 Web grün; api+web type-check grün; web build grün. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
4.6 KiB
Benachrichtigungen — „Folgen → Push"
Stand: 2026-06-02. Erster Kanal: Web Push, login-frei. Der Kern ist kanal-agnostisch gebaut; APNs (nativ) und E-Mail-Digest sind als weitere Endstücke vorgesehen, aber noch nicht verdrahtet.
Warum so
„Folgen" war im Web rein gerätelokal (localStorage) und ohne Wirkung — man folgte einem Ort, aber es passierte nichts. Diese Funktion gibt dem Folgen einen Zweck, ohne die Local-First-Haltung aufzugeben: kein Konto, keine E-Mail. Die Web-Push-Subscription (eine unratbare Endpoint-URL, die der Browser vergibt) ist selbst die pseudonyme Identität. Der Server lernt nur „dieses anonyme Gerät folgt diesen Orten".
Architektur
Vier Schichten, der Kanal ist nur das Endstück:
Crawler findet NEUES Event an gefolgtem Ort
│ (enqueueNewEventNotifications)
▼
notification_outbox ── pending ──► notification-worker ──► Web Push
(Dedup je Gerät+Event) (Drain, Ruhezeiten) (RFC 8291)
- Abo (
venue_follows) — einpush_endpointfolgt einemvenue_slugmit einercadence(instant|daily|off). Gespiegelt aus der lokalen „Folgen"-Liste beim Aktivieren der Benachrichtigungen. - Gerät (
push_endpoints) — pseudonyme Web-Push-Subscription (endpoint + p256dh/auth).user_idoptional (Cross-Device, noch ungenutzt). - Outbox (
notification_outbox) — Warteschlange.(endpoint_id, event_id)ist unique → Re-Crawls doppeln nie. Robust gegen Crashes, auditierbar. - Worker (
notification-worker.ts) — ziehtpending, stellt per Web Push zu, markiertsent/failed, prunt tote Endpoints (404/410), respektiert Ruhezeiten (22–08 Uhr Europe/Berlin).
Auslöser
In event-source-crawl.ts: nur frisch eingefügte Events (nicht
Re-Crawl-Updates) werden gesammelt und nach der Schleife best-effort
eingereiht. Die Zustellung darf den Crawl nie brechen (try/catch in
enqueueNewEventNotifications). Da der Re-Crawl im 24h-Takt läuft, ist
Push batch-ig, nicht sekundengenau — passt zu „was ist neu", nicht Spam.
API (öffentlich, login-frei)
| Method | Pfad | Zweck |
|---|---|---|
GET |
/api/v1/push/vapid-public-key |
Public Key für PushManager.subscribe |
POST |
/api/v1/push/subscribe |
Gerät + gefolgte Orte registrieren/abgleichen |
POST |
/api/v1/push/unsubscribe |
Gerät + Abos löschen |
subscribe setzt das Folgen-Set absolut (nicht additiv): der Client
schickt die volle Liste, der Server gleicht ab. Rate-Limit 60/min/IP.
Web-Client
- Service Worker:
apps/web/public/sw.js(Scope/) —push+notificationclick. - Client-Lib:
apps/web/src/lib/push.ts— subscribe/sync/disable. - UI: Schalter „🔔 Bei neuen Events benachrichtigen" auf
/gemerktin der Sektion „Gefolgte Orte". Hält das Server-Set bei jeder Folgen-Änderung nach (debounced). - iOS: Web Push geht nur als installierte PWA (Safari 16.4+). Im normalen Safari zeigt die UI den Hinweis „Zum Home-Bildschirm hinzufügen". Für iPhone-Nutzer ohne PWA ist APNs (native App) der vorgesehene Weg.
Betrieb
VAPID-Schlüsselpaar einmalig erzeugen und in SOPS/age ablegen:
npx web-push generate-vapid-keys --json
Env (siehe .env.example):
SEEPULS_VAPID_PUBLIC_KEY/SEEPULS_VAPID_PRIVATE_KEY/SEEPULS_VAPID_SUBJECTSEEPULS_PUBLIC_API_URL(Web): API-Basis, die der Browser ansprichtSEEPULS_NOTIFICATIONS=offschaltet den Worker ganz ausSEEPULS_NOTIFY_TICK_MS(Default 20000)
Ohne Keys bleibt alles still: Worker idle, subscribe/vapid → 503,
der Rest der App läuft normal.
DSGVO
push_endpoints ist pseudonym + zweckgebunden (nur Benachrichtigung),
EU-Storage. Kein Tracking über die Subscription hinaus. Löschung:
POST /push/unsubscribe (Cascade räumt venue_follows +
notification_outbox). Bei verknüpfter user_id zusätzlich über den
Service-Key-DSGVO-Pfad. Vor Live: VVT-Eintrag ergänzen.
Offen / nächste Schritte
- VVT-Eintrag für
push_endpoints(vor Live-Schaltung). - APNs (native App) als zweiter Kanal — nutzt den vorhandenen
Favoriten-Sync +
user_id. - E-Mail-Digest via mana-notify für eingeloggte Nutzer (
cadence: daily). daily-Digest-Job für Web Push (heute wird nurinstantzugestellt).- iCal-Folge-Feed (
/api/v1/venues/follow.ics?venues=…) als billiger login-freier Zusatzkanal. - Push-Icon als PNG (manche Plattformen rendern SVG-Notification-Icons nicht).
- Live-Smoke nach Deploy: Migration
0005angewandt, echte Subscription + echter Crawl-Auslöser → Zustellung.