seepuls/docs/NOTIFICATIONS.md
Till JS 6f77d0095b
Some checks are pending
CI / validate (push) Waiting to run
feat: echte Suche, Amenities/Region ausgespielt, Folgen→Web-Push
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>
2026-06-02 15:51:50 +02:00

4.6 KiB
Raw Permalink Blame History

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)
  1. Abo (venue_follows) — ein push_endpoint folgt einem venue_slug mit einer cadence (instant | daily | off). Gespiegelt aus der lokalen „Folgen"-Liste beim Aktivieren der Benachrichtigungen.
  2. Gerät (push_endpoints) — pseudonyme Web-Push-Subscription (endpoint + p256dh/auth). user_id optional (Cross-Device, noch ungenutzt).
  3. Outbox (notification_outbox) — Warteschlange. (endpoint_id, event_id) ist unique → Re-Crawls doppeln nie. Robust gegen Crashes, auditierbar.
  4. Worker (notification-worker.ts) — zieht pending, stellt per Web Push zu, markiert sent/failed, prunt tote Endpoints (404/410), respektiert Ruhezeiten (2208 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 /gemerkt in 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_SUBJECT
  • SEEPULS_PUBLIC_API_URL (Web): API-Basis, die der Browser anspricht
  • SEEPULS_NOTIFICATIONS=off schaltet den Worker ganz aus
  • SEEPULS_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 nur instant zugestellt).
  • 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 0005 angewandt, echte Subscription + echter Crawl-Auslöser → Zustellung.