docs: document chat decisions and update roadmap

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
JannisandClaude Sonnet 5.5 committed 2026-10-10 08:48:07 +02:00
1 parent aa9b5e332a
commit cc4f65b32d
4 files changed
+68 -11

No files matched your search

+4 -4
View File
@@ -85,7 +85,7 @@ Alle Zeitpunkte werden als UTC (`DateTimeOffset`/`DateTime` UTC) gespeichert und
| Essen ✅ | `MealPlanEntry` (Date als `DateOnly`, MealType, DishName, Note, RecipeUrl, Ingredients, WishId, RowVersion, Soft Delete; eindeutig je Tag + Mahlzeit), `Dish` (Name, RecipeUrl, Ingredients, UseCount – für Autovervollständigung), `MealWish` (DishName, Note, Wunschtag/-mahlzeit, Status offen/übernommen/abgelehnt, Soft Delete) |
| Aufgaben ✅ | `TodoTask` (Title, Notes, DueDate + DueTime → DueAt, ReminderMinutesBefore → RemindAt, Priority, Status inkl. „wartet auf Bestätigung“, Category, IsPrivate, IsChore, Points, NeedsApproval, AssignedToFamily, RecurrenceRule + RecurrenceMode, SeriesId/SeriesStart/SeriesIndex, CompletedBy/At, ApprovedBy/At, RowVersion, Soft Delete), `TaskAssignment`, `TaskChecklistItem`, `TaskHistoryEntry` (Verlauf), `PointsLedgerEntry` (Punktebuch). Kategorien kommen aus `FamilySettings.TaskCategories`; „wer/wann/genehmigt“ steht an der Aufgabe und im Verlauf (ADR-0014) |
| Kalender ✅ | `FamilyCalendar` (Name, Color, IsDefault, Soft Delete), `CalendarPermission` (Rolle **oder** Benutzer → None/Read/WriteOwn/Write), `CalendarEvent` (Title, Location, Notes, IsAllDay, StartUtc, EndUtc, TimeZoneId, RecurrenceRule, ExceptionDates, Uid, PersonIds, ForWholeFamily, Sequence, RowVersion, Soft Delete; Overrides als eigene Zeile mit `SeriesMasterId` + `RecurrenceId`), `IcsFeed` (Owner, Scope, CalendarId, TaskMode, TokenHash, TokenPrefix, LastAccessAt, RevokedAt) – ADR-0015/0016 |
| Chat | `ChatChannel` (Family / Direct), `ChatMember` (LastReadAt), `ChatMessage` (Text, Attachment), `ChatReaction` |
| Chat ✅ | `ChatChannel` (Family / Direct), `ChatMember` (LastReadAt), `ChatMessage` (Text, HasImage), `ChatAttachment`, `ChatReaction` |
| Betrieb | `AuditLogEntry` ✅ (ActorName, Action, TargetType/Id/Name, Details, CreatedAt), `PushSubscription` ✅ (UserId, Endpoint + Hash, P256dh, Auth, LastSuccessAt, FailureCount) |
## 4. Rollen- und Berechtigungsmodell
@@ -149,9 +149,9 @@ Das UI bekommt die wirksamen Berechtigungen über `/api/auth/me` und blendet anh
| Kalender-Abos ✅ | `GET/POST /api/calendar/feeds`, `POST /api/calendar/feeds/{id}/regenerate`, `DELETE /api/calendar/feeds/{id}`; Admin: `GET/DELETE /api/admin/calendar-feeds` |
| Admin – Kalender ✅ | `GET/POST /api/admin/calendars`, `PUT/DELETE /api/admin/calendars/{id}` |
| ICS-Feed | `GET /ics/{token}.ics` (öffentlich, Token-gesichert, ETag/Last-Modified) |
| Chat | `GET /api/chat/channels`, `GET /api/chat/channels/{id}/messages?before=`, `POST …/messages`, `POST …/read` |
| Chat ✅ | `GET /api/chat/channels`, `POST /api/chat/direct`, `GET /api/chat/channels/{id}/messages?before=&take=`, `POST …/messages`, `POST …/images` (Multipart), `POST …/read`, `DELETE /api/chat/messages/{id}`, `PUT /api/chat/messages/{id}/reactions`, `GET /api/chat/attachments/{id}` (ADR-0019) |
| Dashboard ✅ | `GET /api/dashboard` (alle Kacheln der Rolle in einem Request, ADR-0018) |
| Hubs | `/hubs/live` (Live-Updates: Einkauf ✅, Essenplan ✅, Aufgaben ✅, Termine ✅; das Dashboard lädt bei diesen Ereignissen neu), `/hubs/chat` |
| Hubs | `/hubs/live` (Live-Updates: Einkauf ✅, Essenplan ✅, Aufgaben ✅, Termine ✅, Chat ✅ inkl. Hub-Methode `Typing`; das Dashboard lädt bei diesen Ereignissen neu) |
| Betrieb | `/health`, `/alive` |
## 7. Frontend
@@ -167,7 +167,7 @@ Das UI bekommt die wirksamen Berechtigungen über `/api/auth/me` und blendet anh
## 8. Echtzeit
SignalR-Hub `/hubs/live` in der Api, vom Web-Host per YARP (WebSockets) durchgereicht, Cookie-Auth. Gruppen: `shopping` und `meals` (alle Rollen),
`family` (ohne Gäste), `user:{id}`; später `channel:{id}` für den Chat. Slices senden nach dem Speichern über `IHubContext<LiveHub>`
`family` (ohne Gäste), `user:{id}`. Der Chat nutzt keine Kanal-Gruppen, sondern sendet an die `user:{id}` der jeweils aktuell Berechtigten (ADR-0019). Slices senden nach dem Speichern über `IHubContext<LiveHub>`
Ereignisse mit dem vollständigen DTO (z. B. `ShoppingItemChanged`), die Clients aktualisieren gezielt ihre Ansicht.
Aufgaben gehen wegen privater Aufgaben nur an `user:{id}` der Benutzer, die sie sehen dürfen (`TaskChanged`/`TaskRemoved`, ADR-0014).
Der Client (`LiveConnection`) verbindet sich mit unbegrenzten Wiederholungen neu und lädt danach den aktuellen Stand.
+11 -6
View File
@@ -98,11 +98,15 @@ Legende: `[x]` erledigt, `[ ]` offen. Nach jeder Phase: Build ohne Warnungen, Te
- [ ] Schnellerfassung Einkauf offline (Outbox) – bewusst nicht umgesetzt, nur online (ADR-0018)
## Phase 8 – Chat
- [ ] Familienchat + Direktnachrichten
- [ ] Gelesen-Status, Ungelesen-Zähler, Tippindikator
- [ ] Paging
- [ ] Bilder (Größenlimit, Validierung), Emoji-Reaktionen
- [ ] Rechte Kind/Gast
- [x] Familienchat (ein geseedeter Kanal) + Direktnachrichten (ein Kanal je Personenpaar, nur Admin/Familie/Kind untereinander, nie mit Gästen); Rechte bei jedem Aufruf gegen die aktuellen Einstellungen/Rollen geprüft (ADR-0019)
- [x] Gelesen-Stand (monoton, „bis zu dieser Nachricht“, nur bei sichtbarer Unterhaltung) mit Haken (Direkt) bzw. „Gelesen von N“ (Familie), Ungelesen-Zähler je Kanal (eine gruppierte Abfrage) in Kanalliste, Navigation (Drawer + Bottom-Navigation) und Dashboard, Tippindikator über Hub-Methode (gedrosselt, kein Echo)
- [x] Paging (30 je Seite, Cursor auf Serverzeit + Id), „Ältere Nachrichten laden“ mit erhaltener Scrollposition, Tageswechsel-Trenner in Europe/Berlin
- [x] Bilder (PNG/JPEG/WebP ≤ 3 MB, Typ aus der Signatur, nur für Kanalmitglieder abrufbar, Bytes nie über SignalR, beim Löschen entfernt), Emoji-Reaktionen (feste Auswahl, idempotent)
- [x] Rechte Kind/Gast: Gast ohne Freigabe sieht nichts (404/403), mit Einstellung nur den Familienkanal; Kinder nur an Familienmitglieder; Löschen: eigene, Admin zusätzlich im Familienkanal; deaktivierte Personen: Verlauf lesbar, Senden gesperrt
- [x] Live über `/hubs/live` an die jeweils berechtigten `user:{id}`-Gruppen, Idempotenz über Client-Ids, Migration `Phase8Chat`
- [x] Tests: Unit (Rechte je Rolle/Einstellung, DM-Regeln, Text/Bild/Reaktion/Löschen, Paging-Grenzen, Drosselung), bUnit (Kanalliste, Verlauf, Paging, Senden/Fehler, Bild, Reaktionen, Löschen, Live-Nachricht sichtbar/versteckt, Tippen, Haken, Gast, nicht erreichbar, neue Unterhaltung, Reconnect), Integration (401, Familienkanal + Ungelesen, Gast-Einstellung, DM-Privatsphäre, Paging, Bilder inkl. gefälschter Typen, Reaktionen, Löschen, deaktivierter Partner, SignalR nur für Mitglieder + Tippen)
- [ ] Chat offline (Outbox) – bewusst nicht umgesetzt, nur online (ADR-0019)
- [ ] Nachrichten bearbeiten, Antworten/Zitate, Suche, frei wählbare Gruppen – bewusst nicht umgesetzt
## Phase 9 – Härtung
- [ ] Content-Security-Policy (Basis-Header X-Content-Type-Options, X-Frame-Options, Referrer-Policy, Permissions-Policy, HSTS sind seit Phase 1 aktiv)
@@ -124,7 +128,8 @@ Legende: `[x]` erledigt, `[ ]` offen. Nach jeder Phase: Build ohne Warnungen, Te
- Einkaufsliste: „Rückgängig“ nach dem Entfernen, Sortierung per Drag & Drop, eigene Kategorien je Familie
- Offline-Cache beim Abmelden leeren (geteilte Geräte)
- Belohnungskatalog für Ämtchen-Punkte
- Web-Push für Termine und Chat
- Web-Push für Termine und Chat (Infrastruktur aus Phase 5 vorhanden)
- Chat: Nachrichten bearbeiten, Antworten, Suche, Aufräumen alter Nachrichten/Bilder
- CalDAV-Server bzw. Microsoft-Graph-Synchronisation (ADR-0017)
- Kalender: Stunden-Raster mit Drag & Drop, „dieser und folgende“, Erinnerungen per Web-Push für Termine
- Rezeptdatenbank
+2 -1
View File
@@ -23,7 +23,8 @@ Kacheln („Gast: eingeschränkte Kacheln“, Familien-/Kinderaufgaben nur für
5. **Schnellerfassung Einkauf** nutzt den vorhandenen Endpunkt `POST /api/shopping/lists/{id}/items` der Standardliste mit Client-Id
(idempotent). Sie funktioniert nur online; die Outbox (ADR-0011) bleibt der Einkaufsseite vorbehalten.
6. **Offline**: Bei Fehlern bleibt der zuletzt geladene Stand mit Hinweis sichtbar; ohne Stand gibt es „Erneut versuchen“.
7. **Chat** (Phase 8): Die Kachel erscheint für Rollen mit Chat-Rechten; der Zähler ist bis zur Chat-Umsetzung 0.
7. **Chat** (Phase 8, ADR-0019): Die Kachel erscheint für Rollen mit Chat-Rechten und zeigt die Summe der ungelesenen Nachrichten über alle sichtbaren Kanäle
(`ChatAccessService.TotalUnreadAsync`); die Seite lädt bei Chat-Ereignissen (neue Nachricht, gelöscht, gelesen) neu.
## Folgen
* Ein Request je Seitenaufruf; Kosten wachsen mit der Familiengröße, nicht mit der Datenmenge (Aufgaben: nicht erledigte, Termine: zwei Tage).
+51
View File
@@ -0,0 +1,51 @@
# ADR-0019: Chat – Familienkanal, Direktnachrichten, Live über den Live-Hub
* Status: akzeptiert
* Datum: 2026-10-10
## Kontext
Phase 8 liefert einen Familienchat und Direktnachrichten mit Gelesen-Status, Ungelesen-Zähler, Tippindikator, Paging, Bildern und
Emoji-Reaktionen. Die Rollenmatrix (ADR-0008) kennt `FamilyChat` (Gast nur mit Einstellung) und `DirectMessages` (Kind „eingeschränkt“).
## Entscheidung
1. **Modell:** `ChatChannel` (Familie – genau einer, geseedet – oder Direkt mit eindeutigem `DirectKey` je Personenpaar),
`ChatMember` (Gelesen-Stand `LastReadAt`), `ChatMessage` (Soft Delete, Text oder Bildunterschrift), `ChatAttachment` (Bild, Id = Nachrichten-Id),
`ChatReaction` (eindeutig je Nachricht/Person/Emoji). Eindeutige, gefilterte Indizes erzwingen „ein Familienkanal“, „ein Kanal je Paar“ und
„eine Reaktion je Person und Emoji“ auch bei gleichzeitigen Requests.
2. **Rechte werden bei jedem Aufruf geprüft** (`ChatAccessService`), nicht über SignalR-Gruppen: Familienkanal nur mit `FamilyChat`
(Gäste nur mit der Einstellung), Direktkanäle nur für Mitglieder mit `DirectMessages`. Ein Wechsel der Einstellung, Rolle oder ein Deaktivieren wirkt
sofort; unberechtigte Zugriffe auf Kanäle liefern 404 (kein Hinweis auf die Existenz), das Starten einer Direktunterhaltung ohne Recht 403.
3. **Direktnachrichten – „Kinder nur an Familienmitglieder“:** gelesen als „beide Seiten dürfen Direktnachrichten nutzen“, also
Admin/Familie/Kind untereinander, nie mit Gästen (`ChatRules.CanDirectMessage`). Ein Direktkanal ohne Nachrichten ist für das Gegenüber verborgen.
Wird die andere Person deaktiviert, gelöscht oder läuft sie ab, bleibt der Verlauf lesbar, Senden liefert 409 (`CanSend = false`).
4. **Live über `/hubs/live`** (kein zweiter Hub `/hubs/chat`): eine Verbindung je Gerät, der BFF leitet `/hubs` bereits weiter. Ereignisse gehen an die
Gruppen `user:{id}` der **jetzt** berechtigten Empfänger (wie bei Aufgaben, ADR-0014), nie an die Sammelgruppe `family`. Ereignisse:
`ChatMessageReceived` (DTO), `ChatMessageRemoved`, `ChatReactionsChanged`, `ChatReadChanged` (auch an den Leser selbst, damit andere Geräte den Zähler
aktualisieren), `ChatTyping`. **Bilddaten werden nie über SignalR verschickt**, nur die URL.
5. **Tippindikator:** Hub-Methode `Typing(channelId)` – prüft den Kanalzugriff, sendet nicht an den Absender zurück und ist je Verbindung und Kanal auf ein
Signal alle 2 s gedrosselt (`ChatTypingThrottle`). Der Client sendet höchstens alle 3 s und zeigt ein Signal 4 s an.
6. **Gelesen-Stand:** monoton (nie zurück), „bis zu dieser Nachricht“ statt „jetzt“ – Nachrichten, die während des Ladens eintreffen, bleiben ungelesen.
Der Client markiert nur, wenn die Unterhaltung wirklich sichtbar ist (Seite offen und Tab im Vordergrund). Ohne Eintrag gilt die Kontoerstellung als Basis
(neue Mitglieder starten ohne Alt-Nachrichten als ungelesen). Eigene und gelöschte Nachrichten zählen nie. Alle Zähler kommen aus **einer** gruppierten Abfrage.
Haken: bei Direktnachrichten „gelesen“, im Familienkanal „Gelesen von N“.
7. **Paging:** neueste zuerst, Cursor ist eine Nachrichten-Id, Reihenfolge `(CreatedAt, Id)` auf Serverzeit – Client-Guids sind geräteübergreifend nicht zeitlich
geordnet. 30 Nachrichten je Seite (höchstens 100).
8. **Bilder:** PNG/JPEG/WebP bis 3 MB, Typ ausschließlich aus der Signatur (nicht Dateiname/Content-Type des Clients), in der Datenbank (ADR-0009).
Upload als Multipart an `POST …/images` mit Größenlimit unterhalb des Standards, ohne Antiforgery-Token, aber mit dem CSRF-Header (ADR-0006).
Abruf nur für Kanalmitglieder, `private` cachebar, `nosniff`. Listenabfragen projizieren auf DTOs und laden nie Bildbytes. Beim Löschen der Nachricht
werden Bild und Reaktionen sofort physisch entfernt.
9. **Reaktionen:** feste Auswahl (👍 ❤️ 😂 😮 😢 🎉), zustandsbasiert (`PUT … {emoji, active}`), damit idempotent.
10. **Löschen:** eigene Nachrichten immer, Administratoren zusätzlich im Familienkanal (Moderation); Direktnachrichten moderiert niemand mit.
11. **Idempotentes Senden:** Client-Id je Nachricht; erneutes Senden liefert die vorhandene Nachricht (200), gelöscht bleibt gelöscht (204),
fremde Id → 409.
12. **Gelöschte Benutzer:** Nachrichten bleiben, der Name wird aus dem anonymisierten Datensatz gelesen (ADR-0010); fehlt er, zeigt die Anzeige „Unbekannt“.
13. **Zeit:** Trenner und Uhrzeiten in Europe/Berlin über die Familienzeitzone, nicht die Browserzone.
## Bewusst nicht umgesetzt
* Offline-Senden/Outbox (Chat nur online, nach Wiederverbindung wird der Verlauf nachgeladen).
* Nachrichten bearbeiten, Antworten/Zitate, Gruppen mit frei wählbaren Mitgliedern, Suche, Anhänge außer Bildern.
* Web-Push für neue Nachrichten (Backlog; Infrastruktur aus ADR-0014 ist vorhanden).
## Folgen
* Der Verlauf wächst unbegrenzt in der Datenbank; Bilder zählen zum Backup. Aufräumen alter Nachrichten ist ein Backlog-Thema.
* Der Familienchat hat genau einen Kanal – mehrere Familienkanäle erfordern später ein Mitgliedschaftsmodell wie bei Direktkanälen.