feat: neues Benachrichtigungssystem Konzept hinzugefügt
This commit is contained in:
157
docs/benachrichtigungskonzept.md
Normal file
157
docs/benachrichtigungskonzept.md
Normal file
@@ -0,0 +1,157 @@
|
||||
# Benachrichtigungssystem – Konzept
|
||||
|
||||
## Zielbild
|
||||
|
||||
Nutzer sollen relevante Vorgänge rechtzeitig sehen, ohne mit Meldungen überflutet zu werden. Jede Benachrichtigung ist einer konkreten Aufgabe oder Information zugeordnet und führt mit einem Klick direkt dorthin. Das System unterscheidet zwischen Vereinsverwaltung, Trainerbereich und Mitgliedern, respektiert Rollen und persönliche Einstellungen und bleibt auch dann zuverlässig, wenn jemand gerade nicht angemeldet ist.
|
||||
|
||||
Die zentrale Produktentscheidung lautet: **Der Posteingang in der Anwendung ist die verlässliche Quelle; Live-Hinweise, E-Mail und später Push sind nur zusätzliche Zustellwege.** Dadurch gehen Informationen nicht verloren, auch wenn ein Browser geschlossen ist oder ein Versand fehlschlägt.
|
||||
|
||||
## Nutzergruppen und Grundregeln
|
||||
|
||||
| Nutzergruppe | Erhält Benachrichtigungen zu | Darf nicht sehen |
|
||||
| --- | --- | --- |
|
||||
| Vereinsverwaltung | Vorgängen in ihren berechtigten Modulen, z. B. Anfragen, Freigaben, Aufgaben, Zahlungen | Inhalte ohne Modulberechtigung oder außerhalb ihres Vereins |
|
||||
| Trainer | Eigene Trainings- und Mannschaftsprozesse, (Q)TTR-Änderungen relevanter Spieler sowie Freundschaftsspiel-Anfragen | Verwaltungs- oder Mitgliedsdaten außerhalb der eigenen Berechtigung bzw. Zuordnung |
|
||||
| Mitglied | Eigenen Terminen, Nachrichten, Profilanfragen und Mitgliedschaftsthemen | Daten anderer Mitglieder, interne Verwaltungsnotizen |
|
||||
| Systemadministrator | Technischen oder übergreifenden Systemereignissen | Vereinsinhalte ohne expliziten Support-/Berechtigungsprozess |
|
||||
|
||||
Empfänger werden immer auf dem Server bestimmt. Der Client erhält nur bereits autorisierte Einträge. Eine Rolle ist kein pauschaler Verteiler: Eine neue Probetrainingsanfrage geht beispielsweise an Nutzer mit `requests:read` oder eine künftig konfigurierbare zuständige Rolle, nicht automatisch an alle Vereinsnutzer.
|
||||
|
||||
## Ereigniskatalog für die erste Ausbaustufe
|
||||
|
||||
| Ereignis | Empfänger | Priorität | Standardkanal | Ziel |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| Neue Kontakt-, Probe-, Mitgliedschafts- oder Sponsoringanfrage | Zuständige Nutzer für Anfragen | hoch | Posteingang, Live-Hinweis, E-Mail | Anfrage öffnen |
|
||||
| Neue oder fällige Aufgabe | Zugewiesene Person; bei fehlender Zuweisung verantwortliche Rolle | normal / hoch bei Überfälligkeit | Posteingang, Live-Hinweis | Aufgabe öffnen |
|
||||
| Profiländerung beantragt | Nutzer mit Berechtigung zur Mitgliederpflege | normal | Posteingang, Live-Hinweis | Änderungsanfrage prüfen |
|
||||
| Profiländerung genehmigt/abgelehnt | betreffendes Mitglied | normal | Posteingang, Live-Hinweis | Mitgliederbereich |
|
||||
| Neue direkte Vereinsnachricht | Empfänger-Mitglied | normal | Posteingang, Live-Hinweis, optional E-Mail | Nachricht öffnen |
|
||||
| Termin-/Trainingsänderung oder -absage | betroffene, angemeldete Mitglieder | hoch bei Absage, sonst normal | Posteingang, Live-Hinweis, optional E-Mail | Termin öffnen |
|
||||
| (Q)TTR eines relevanten Spielers hat sich geändert | Zugeordnete Trainer bzw. Trainer mit Lesezugriff auf den Spieler | normal | Posteingang, Live-Hinweis, optional Tagesbündelung | Spieler-/Mannschaftsprofil öffnen |
|
||||
| Eingehende Anfrage für ein Freundschaftsspiel | Zuständige Trainer bzw. Nutzer mit `schedule:read` im Zielverein | hoch | Posteingang, Live-Hinweis, optional E-Mail | Freundschaftsspiel öffnen |
|
||||
| Freundschaftsspiel angenommen, abgelehnt oder wesentlich geändert | Ersteller und zuständige Trainer beider Vereine | normal / hoch bei kurzfristiger Absage | Posteingang, Live-Hinweis | Freundschaftsspiel öffnen |
|
||||
| Zahlung offen, Mahnstufe oder Zahlungseingang | Kasse bzw. betroffenes Mitglied, abhängig vom Vorgang | hoch bei Frist/Mahnung | Posteingang, optional E-Mail | Forderung öffnen |
|
||||
| Rolle oder Zugang geändert | betroffener Nutzer | hoch | Posteingang, E-Mail | Einstellungen bzw. Verein |
|
||||
| Wichtige Produkt-/Sicherheitsinformation | gezielte Nutzergruppe | kritisch | Posteingang, E-Mail; später Push | Informationsdetail |
|
||||
|
||||
Nicht als Benachrichtigung vorgesehen sind reine Lesesichtungen, normale Protokolländerungen und jede kleine Bearbeitung durch andere Personen. Diese gehören in die Historie/Aktivitätsprotokolle. Mehrere gleichartige Ereignisse werden zusammengefasst, etwa: „3 neue Probetrainingsanfragen“.
|
||||
|
||||
## Prioritäten und Zustellregeln
|
||||
|
||||
- **Kritisch:** Sicherheits- oder Zugangsereignisse. Nicht stumm schaltbar; zusätzlich E-Mail.
|
||||
- **Hoch:** Frist, neue externe Anfrage, Absage oder überfällige Aufgabe. Sofort im Produkt; E-Mail standardmäßig an.
|
||||
- **Normal:** Handlungsrelevante Information. Sofort im Produkt; E-Mail ist eine persönliche Einstellung.
|
||||
- **Niedrig:** Reine Information. Nur im Posteingang, standardmäßig gebündelt oder deaktiviert.
|
||||
|
||||
## Sichtbarkeit in Hauptnavigation und Trainerbereich
|
||||
|
||||
Die Anwendung zeigt die Glocke mit dem globalen Zähler ungelesener Einträge **immer sichtbar im authentifizierten Hauptkopf**, unmittelbar neben dem Nutzer-Menü. Sie bleibt damit sichtbar, unabhängig davon, ob der Nutzer gerade die Vereins-, Trainer- oder Mitgliederansicht geöffnet hat. Auf kleinen Bildschirmen bleibt mindestens das Glocken-Icon mit Badge erhalten; der Text kann entfallen.
|
||||
|
||||
Ein Klick öffnet einen kompakten Auszug der neuesten Einträge; „Alle anzeigen“ führt zum persönlichen Posteingang. Dieser Posteingang wird nicht als separater, leerer „Trainer-Posteingang“ dupliziert: Er ist ein gemeinsamer persönlicher Eingang mit klaren Filtern bzw. Bereichen **Alle**, **Trainer**, **Verein** und **Mitglied**. Im Trainerprodukt ist der Filter **Trainer** voreingestellt und zusätzlich als eigener Navigationspunkt „Posteingang“ in der Hauptnavigation sichtbar. Die Glocke zählt aber stets alle für den angemeldeten Nutzer relevanten ungelesenen Einträge.
|
||||
|
||||
Ein neuer Live-Eintrag zeigt einen unaufdringlichen Toast, aber keinen blockierenden Dialog. Ton ist standardmäßig aus.
|
||||
|
||||
Ein Eintrag bleibt ungelesen, bis der Nutzer ihn bewusst öffnet oder einzeln/alle als gelesen markiert. Das Öffnen des Dropdowns allein markiert nichts als gelesen. Wird das Zieldokument archiviert oder die Berechtigung entzogen, bleibt der Eintrag als Hinweis erhalten, zeigt aber eine sichere, erklärende Zielseite.
|
||||
|
||||
## Fachliches Datenmodell
|
||||
|
||||
Die Benachrichtigung wird als immutable Ereignis plus nutzerspezifischer Zustellstatus modelliert. Dadurch kann ein Ereignis mehreren Personen zugestellt werden, ohne Inhalte zu duplizieren.
|
||||
|
||||
```text
|
||||
notification_events
|
||||
id, club_id?, type, priority, actor_user_id?,
|
||||
resource_type, resource_id, title_key, body_key, payload,
|
||||
dedupe_key?, created_at, expires_at?
|
||||
|
||||
notification_recipients
|
||||
id, event_id, user_id, read_at?, archived_at?,
|
||||
delivery_state, email_state, email_sent_at?, created_at
|
||||
|
||||
notification_preferences
|
||||
user_id, club_id?, event_category,
|
||||
in_app_enabled, email_enabled, push_enabled, digest_frequency,
|
||||
quiet_hours_from?, quiet_hours_to?
|
||||
```
|
||||
|
||||
`payload` enthält nur die für Darstellung und Navigation erforderlichen Daten, etwa `{ "clubId": 12, "requestId": 44, "route": "/club-requests?requestId=44" }`. Es enthält keine sensiblen Freitexte, Adressen oder internen Notizen. Titel und Texte werden über Übersetzungsschlüssel erzeugt; das erlaubt Spracheinstellungen und spätere Textanpassungen.
|
||||
|
||||
`dedupe_key` verhindert Doppelungen durch wiederholte Jobs oder parallele Requests, zum Beispiel `club:12:request:44:created`. Für Sammelmeldungen wird ein kurzer Zeitraum verwendet, etwa zehn Minuten je Verein, Typ und Empfängergruppe.
|
||||
|
||||
### Regel für (Q)TTR-Änderungen
|
||||
|
||||
Ein Import, Sync oder erneuter Abruf erzeugt **keine** Benachrichtigung allein deshalb, weil er gelaufen ist. Vor dem Speichern vergleicht der Server je Spieler die normalisierten bisherigen und neuen Werte (`ttr`, `qttr`) sowie den zugehörigen Stichtag/Stand. Nur wenn mindestens ein fachlicher Wert tatsächlich abweicht, wird genau ein Ereignis erzeugt, z. B. „Max Mustermann: QTTR 1.487 → 1.512“.
|
||||
|
||||
Für denselben Spieler und Stichtag verhindert eine eindeutige `dedupe_key` wie `club:12:member:44:qttr:2026-08-11` weitere Meldungen. Trifft ein korrigierter Wert für denselben Stichtag ein, wird der bestehende ungelesene Eintrag aktualisiert statt eine zweite Meldung zu erzeugen. Fehlt der alte Wert oder ist der neue Wert ungültig, wird kein Änderungsalarm versendet; die Änderung bleibt im Importprotokoll nachvollziehbar. Bei vielen Änderungen werden sie pro Trainer und Abruf gebündelt, etwa „12 (Q)TTR-Änderungen in deinen Mannschaften“.
|
||||
|
||||
Empfohlene Indizes: `(user_id, read_at, created_at DESC)` auf `notification_recipients`, `(club_id, created_at DESC)` auf `notification_events` und ein eindeutiger Index auf `dedupe_key`, soweit gesetzt.
|
||||
|
||||
## Technische Architektur
|
||||
|
||||
1. Eine fachliche Aktion wird erfolgreich in der Datenbank gespeichert, etwa eine neue Vereinsanfrage.
|
||||
2. Im selben Datenbank-Commit erzeugt ein `NotificationService` das Ereignis und die berechtigten Empfänger. So existiert kein Hinweis auf einen Vorgang, der später zurückgerollt wird.
|
||||
3. Nach dem Commit wird ein neutrales Echtzeitereignis an den persönlichen Nutzerraum gesendet, z. B. `notification:created` mit Empfänger-ID und Zähler. Bestehendes Socket.IO kann dafür erweitert werden.
|
||||
4. Der Client lädt den Eintrag über die API nach und aktualisiert Glocke und Posteingang. Bei fehlender Socket-Verbindung wird beim App-Start, beim Tab-Fokus und periodisch moderat abgeglichen.
|
||||
5. Ein Hintergrund-Worker verarbeitet E-Mail und später Web-Push anhand der Zustellpräferenzen. Wiederholversuche und Fehler landen in einem Zustellprotokoll.
|
||||
|
||||
Die bisherigen Club-Socket-Räume eignen sich für Aktualisierungen gemeinsamer Ansichten, sind aber für persönliche Hinweise nicht ausreichend sicher. Ergänzung: Socket-Authentifizierung mit dem bestehenden Zugangstoken und persönliche Räume wie `user:{userId}`; der Client darf keine frei wählbaren Nutzer- oder Clubräume betreten.
|
||||
|
||||
## API-Schnittstellen (Vorschlag)
|
||||
|
||||
| Methode | Endpunkt | Zweck |
|
||||
| --- | --- | --- |
|
||||
| `GET` | `/notifications?clubId=&cursor=&unreadOnly=` | Posteingang paginiert laden |
|
||||
| `GET` | `/notifications/unread-count?clubId=` | Zähler für Glocke laden |
|
||||
| `POST` | `/notifications/:recipientId/read` | Eintrag als gelesen markieren |
|
||||
| `POST` | `/notifications/read-all` | Gefilterte Einträge als gelesen markieren |
|
||||
| `POST` | `/notifications/:recipientId/archive` | Persönlich ausblenden/archivieren |
|
||||
| `GET` / `PUT` | `/notification-preferences` | persönliche Kanal- und Kategorieeinstellungen |
|
||||
|
||||
Es gibt bewusst keinen allgemeinen Client-Endpunkt zum Erzeugen einer Benachrichtigung. Fachliche Endpunkte (Anfrage anlegen, Aufgabe zuweisen, Termin absagen) rufen den serverseitigen Dienst auf. Manuell verfasste Vereinskommunikation bleibt im Kommunikationsmodul und erzeugt für Empfänger ihren passenden Posteingangseintrag.
|
||||
|
||||
## Datenschutz, Sicherheit und Betrieb
|
||||
|
||||
- Berechtigungen werden beim Erzeugen **und** beim Abruf bzw. Öffnen der Zielressource geprüft.
|
||||
- E-Mails enthalten möglichst keine vertraulichen Daten; sie nennen Anlass und Verein und verlinken nach Anmeldung auf das Ziel.
|
||||
- Für E-Mail, Push und Telemetrie werden Einwilligung, Widerruf und Versandzeitpunkt protokolliert. Kritische Sicherheitsmails folgen der erforderlichen Rechtsgrundlage und sind nicht abwählbar.
|
||||
- Standardaufbewahrung: gelesene/archivierte Einträge 180 Tage, ungelesene 365 Tage; kritische Sicherheitsereignisse gemäß Sicherheits- und Löschkonzept länger. Die Fristen müssen mit dem finalen Datenschutzkonzept abgestimmt werden.
|
||||
- E-Mail-Versand ist asynchron, idempotent und hat begrenzte Wiederholversuche. Ein Versandfehler verändert nie den In-App-Status.
|
||||
|
||||
## Umsetzung in Stufen
|
||||
|
||||
### MVP
|
||||
|
||||
- Datenmodell, serverseitiger `NotificationService`, REST-API und Posteingang mit Glocke.
|
||||
- Live-Aktualisierung über authentifizierte persönliche Socket-Räume plus Polling-Fallback.
|
||||
- Globale Glocke mit Badge im authentifizierten Hauptkopf und sichtbarer Navigationspunkt „Posteingang“ im Trainerbereich.
|
||||
- Ereignisse: neue Vereinsanfragen, zugewiesene/fällige Aufgaben, Profiländerungen, direkte Vereinsnachrichten, tatsächlich geänderte (Q)TTR-Werte sowie eingehende Freundschaftsspiel-Anfragen.
|
||||
- Gelesen/ungelesen, Navigation zum Kontext und persönliche In-App-Einstellung.
|
||||
|
||||
### Ausbaustufe 2
|
||||
|
||||
- E-Mail-Worker mit Vorlagen, Zustellprotokoll, Bündelung und Ruhezeiten.
|
||||
- Termin-/Trainingsänderungen, Finanzen, Rollen- und Zugangsmeldungen.
|
||||
- Zuständigkeiten je Ereignistyp im Verein konfigurierbar.
|
||||
|
||||
### Ausbaustufe 3
|
||||
|
||||
- Web-Push/PWA, tägliche oder wöchentliche Zusammenfassungen, Eskalationen bei unbehandelten kritischen Vorgängen.
|
||||
- Auswertung: Zustellung, Öffnung, Bearbeitungszeit – ausschließlich aggregiert und datensparsam.
|
||||
|
||||
## Akzeptanzkriterien für das MVP
|
||||
|
||||
- Eine neue Probetrainingsanfrage erscheint für alle zuständigen Nutzer innerhalb weniger Sekunden in Glocke und Posteingang, aber nicht bei Unberechtigten.
|
||||
- Im Trainerbereich ist der Posteingang über Hauptnavigation und Glocke ohne Seitenwechsel sichtbar erreichbar; die Glocke zeigt auch dort zuverlässig den ungelesenen Zähler.
|
||||
- Ein erneuter (Q)TTR-Import mit identischen Werten erzeugt keinen Eintrag. Ändert sich ein TTR- oder QTTR-Wert, sehen nur berechtigte bzw. zugeordnete Trainer eine nachvollziehbare Meldung mit Alt- und Neuwert.
|
||||
- Eine eingehende Freundschaftsspiel-Anfrage wird im Zielverein den zuständigen Trainern zugestellt und führt direkt zur Entscheidung in „Freundschaftsspiele“.
|
||||
- Ein Mitglied sieht die Entscheidung über seine Profiländerung und kann direkt zum passenden Bereich navigieren.
|
||||
- Nach Ab- und erneutem Anmelden bleiben ungelesene Einträge und der Zähler korrekt erhalten.
|
||||
- Bei unterbrochener Echtzeitverbindung werden neue Einträge beim nächsten Abgleich sichtbar.
|
||||
- Ein Nutzer kann normale Kategorien abschalten; kritische Sicherheitsinformationen bleiben sichtbar.
|
||||
- Ein Empfänger kann keine Benachrichtigung oder Zielressource eines anderen Vereins oder Nutzers über manipulierte IDs abrufen.
|
||||
|
||||
## Offene Produktentscheidungen
|
||||
|
||||
1. Welche Rollen sind pro Ereignistyp anfänglich zuständig (z. B. Anfragen: Vorstand, Mitgliederverwaltung oder frei konfigurierbar)?
|
||||
2. Soll E-Mail bereits im MVP enthalten sein oder erst nach dem belastbaren In-App-Posteingang?
|
||||
3. Müssen Mitglieder in der ersten Version individuelle Trainings-/Terminpräferenzen erhalten oder genügt „bei allen für mich relevanten Änderungen“?
|
||||
4. Welche Aufbewahrungsfrist wird mit Datenschutz/Vertrag final festgelegt?
|
||||
Reference in New Issue
Block a user