From d5ecdcd3c41c9f3f40d162425c2abcb4fac99fc1 Mon Sep 17 00:00:00 2001 From: linus Date: Thu, 10 Sep 2026 09:03:42 +0200 Subject: [PATCH] docs: Wahl result view, live chat, and the gateway race fix Plan + client README updated for the guest Wahl result endpoint/tab, the WebSocket chat wiring, and the ChatGateway caller-promise fix. Section 8 now scopes the remaining Flutter work to the Authentik-dependent screens. Co-Authored-By: Claude Sonnet 5 --- client/app/README.md | 12 +++++++----- client/app/lib/api.dart | 2 +- plan-kcAppMultiTenantPlatform.prompt.md | 8 +++++--- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/client/app/README.md b/client/app/README.md index a98f1b5..2f486b9 100644 --- a/client/app/README.md +++ b/client/app/README.md @@ -26,13 +26,15 @@ backend, which also serves the interim plain-HTML client at `/`). - The token is stored via `shared_preferences` (localStorage on web) and restored on start; `GET /auth/me` resolves the role for a role-aware home. - **Home** (`lib/screens/home_screen.dart`) — identity card + navigation. -- **Workshop-Wahl** (`lib/screens/wahl_screen.dart`, guests) — loads - `GET /wahl/guest/overview`, tap workshops in order (max 3) to set - priorities, `POST /wahl/:id/teilnehmer`. +- **Workshop-Wahl** (`lib/screens/wahl_screen.dart`, guests) — two tabs: + *Wünsche* (`GET /wahl/guest/overview`, tap workshops in order, max 3, + `POST /wahl/:id/teilnehmer`) and *Ergebnis* (`GET /wahl/guest/results` — + PENDING / ASSIGNED with workshop + wish rank / UNASSIGNED). - **Dateien** (`lib/screens/files_screen.dart`) — `GET /files/:kcId`, filtered server-side by the caller's visibility tier. -- **Chat** (`lib/screens/chat_screen.dart`) — channel + message list - (read-only; sending is a WebSocket path, still to do). +- **Chat** (`lib/screens/chat_screen.dart` + `lib/chat_socket.dart`) — + channel list, REST history, then a live `/chat` WebSocket connection + (`chat:join` / `chat:send` / `chat:message`) with a compose bar. ## Architecture diff --git a/client/app/lib/api.dart b/client/app/lib/api.dart index 6db9483..0d04bcf 100644 --- a/client/app/lib/api.dart +++ b/client/app/lib/api.dart @@ -326,7 +326,7 @@ class Api { ); } - // --- chat (read-only for now; sending is a WebSocket-only path) --- + // --- chat: REST for channels/history; live send/receive is the /chat WS --- Future> channels(String kcId) async { final list = await _get('/chat/$kcId/channels') as List; return list.map((e) => ChatChannel.fromJson(e as Map)).toList(); diff --git a/plan-kcAppMultiTenantPlatform.prompt.md b/plan-kcAppMultiTenantPlatform.prompt.md index 9675fdd..eb0c2b9 100644 --- a/plan-kcAppMultiTenantPlatform.prompt.md +++ b/plan-kcAppMultiTenantPlatform.prompt.md @@ -70,7 +70,7 @@ Vollständiges Schema: [backend/prisma/schema.prisma](backend/prisma/schema.pris | `onboarding/` | Selbstregistrierung Gemeinde Verantwortliche/r: öffentlicher Invite-Lookup, JIT-`User`-Anlage aus Authentik-Claims, `Membership` im Status `PENDING`; LT sieht/genehmigt/lehnt ab | `GET /api/onboarding/kc/:inviteCode`, `POST /api/onboarding/verantwortliche` (Authentik-Bearer), `GET /api/onboarding/requests?kcId=` (LT), `POST /api/onboarding/requests/:id/approve\|reject` (LT) | | `teamer/` | Lokale Gemeinde-Teamer-Accounts + Invites; Direkt-Anlage, Gruppen-Link und E-Mail-Invite (persönliche Invites werden per `MailService` best-effort verschickt, `emailSent` im Response); nutzbar von LT (jede Gemeinde) oder Verantwortliche/r (nur eigene Gemeinde, in `TeamerService` geprüft) | `POST/GET /api/gemeinde/:gemeindeId/teamer`, `DELETE /api/gemeinde/:gemeindeId/teamer/:userId`, `POST/GET /api/gemeinde/:gemeindeId/teamer-invites`, `DELETE .../teamer-invites/:id` | | `mail/` | Globale `MailProvider`-Abstraktion (log-only Default, SMTP via `MAIL_PROVIDER=smtp`); `MailService` baut die Invite-Mail inkl. Link aus `APP_BASE_URL` | – | -| `wahl/` | Wahl-/Workshop-Verwaltung, Force-Zuteilung, Teilnehmer-Einreichung, `ZuteilungService` (Portierung von `kc_run_zuteilung`: Force-Zuteilungen → 3 Wunschrunden → Zufallsfüllung → Konsolidierung unterbesetzter Workshops), CSV-Export | `POST/GET /api/wahl`, `POST/GET /api/wahl/:id/workshops`, `POST /api/wahl/:id/force-zuteilung`, `POST /api/wahl/:id/teilnehmer` (Guest), `POST /api/wahl/:id/zuteilung/run`, `GET /api/wahl/:id/zuteilung(.csv)` | +| `wahl/` | Wahl-/Workshop-Verwaltung, Force-Zuteilung, Teilnehmer-Einreichung, `ZuteilungService` (Portierung von `kc_run_zuteilung`: Force-Zuteilungen → 3 Wunschrunden → Zufallsfüllung → Konsolidierung unterbesetzter Workshops), CSV-Export | `POST/GET /api/wahl`, `POST/GET /api/wahl/:id/workshops`, `POST /api/wahl/:id/force-zuteilung`, `POST /api/wahl/:id/teilnehmer` (Guest), `GET /api/wahl/guest/overview` + `GET /api/wahl/guest/results` (Guest), `POST /api/wahl/:id/zuteilung/run`, `GET /api/wahl/:id/zuteilung(.csv)` | | `files/` | Upload (LT-only, multipart) mit Sichtbarkeitsstufe; Liste/Download für Team oder Guest, gefiltert nach erlaubten Sichtbarkeitsstufen; `StorageProvider`-Abstraktion (WebDAV/Nextcloud Default, S3 optional) | `POST /api/files/:kcId`, `GET /api/files/:kcId`, `GET /api/files/download/:fileId` | | `chat/` | Zentrale Zugriffslogik (`ChatService`) geteilt zwischen REST (`ChatController`) und WS (`ChatGateway`, Pfad `/chat`, eigene Token-Verifikation via `?token=`); Kanaltypen wie oben | `POST /api/chat/:kcId/channels`, `POST /api/chat/direct`, `GET /api/chat/:kcId/channels`, `GET /api/chat/channels/:id/messages`, WS-Events `chat:join`/`chat:send`/`chat:message` | | `sync/` | Append-only Replikationslog + Peer-Sync (lokal ⇄ Cloud), `SyncSchedulerService` (alle 30s, wenn `SYNC_ENABLED=true`) | `POST /api/sync/ingest`, `GET /api/sync/export`, `POST /api/sync/trigger` (LT-only) | @@ -97,7 +97,7 @@ Details, Setup-Anleitung und `.env`-Variablen: [backend/README.md](backend/READM 4. **Dateifreigabe** – Storage-Abstraktion, Sichtbarkeitsstufen. ✅ 5. **Kommunikation** – Chat (Gruppen/DM/LT/Broadcast), WebSocket. ✅ (Push-Integration noch offen) 6. **Hybrid Lokal/Cloud-Server & Sync** – Replikationslog, Scheduler, Shared-Secret-Auth. ✅ -7. **Flutter-Clients** – gemeinsame Codebase (`client/app/`, Web-Target). ✅ Grundgerüst: Login (Guest / lokaler Teamer / Invite-Redemption, Token in `shared_preferences`, `GET /auth/me` für rollenabhängige Startseite), Guest-Wahl-Formular (`/wahl/guest/overview` → geordnete Wunsch-Auswahl → Absenden), Datei-Liste, Chat (nur lesen). 🔜 Mobile/Desktop-Targets, Authentik-Auth-Code-Flow, Wahl-**Ergebnis**-Ansicht, WS-Chat-Senden, LT-/Verantwortlichen-Admin-Screens (KC/Gemeinde/Teamer/Onboarding-Freigaben). +7. **Flutter-Clients** – gemeinsame Codebase (`client/app/`, Web-Target). ✅ Login (Guest / lokaler Teamer / Invite-Redemption, Token in `shared_preferences`, `GET /auth/me` für rollenabhängige Startseite), Guest-Wahl: **Wünsche** (`/wahl/guest/overview` → geordnete Auswahl → Absenden) + **Ergebnis** (`/wahl/guest/results`, PENDING/ASSIGNED/UNASSIGNED), Datei-Liste, **Chat** mit REST-Verlauf + Live-`chat:message` und Senden über das `/chat`-WebSocket (`chat_socket.dart`). 🔜 Mobile/Desktop-Targets, Authentik-Auth-Code-Flow, LT-/Verantwortlichen-Admin-Screens (KC/Gemeinde/Teamer/Onboarding-Freigaben). --- @@ -114,6 +114,8 @@ Details, Setup-Anleitung und `.env`-Variablen: [backend/README.md](backend/READM 3. Web-Client: `GET /` liefert die statische Seite (200), `GET /api/kc` trifft die echte, geschützte API (401 ohne Token). 3a. Gegen lokales **PostgreSQL 16** (Homebrew): `prisma migrate dev --name init` erzeugt/appliziert die erste Migration; Guest-Flow Ende-zu-Ende geprüft (`/auth/guest` → `/auth/me` → `/wahl/guest/overview` → `POST …/teilnehmer` → Re-Fetch zeigt `meinePrioritaeten`). Seed: `prisma/seed-dev.js`. 3b. Flutter-Client (`client/app/`): `flutter analyze` sauber, `flutter build web --release` erfolgreich, `flutter test` grün (Login-Screen-Smoke-Test); CORS-Preflight vom Browser-Origin ok. +3c. Guest-Ergebnis: `/wahl/guest/results` gegen echtes Postgres in beiden Zuständen geprüft (PENDING nach Einreichung, ASSIGNED nach manuell gesetzter `Zuteilung` → Workshop-Name + Wunschrang). +3d. WS-Chat: Zwei-Client-E2E gegen echtes Postgres (zwei lokale Teamer im selben `GEMEINDE_GRUPPE`-Kanal, `chat:send` → der andere empfängt `chat:message`). Dabei behoben: `ChatGateway` speicherte den Caller erst nach dem asynchronen Token-Check, wodurch ein sofortiges `chat:join` mit 4001 abgewiesen wurde — jetzt wartet der Handler auf das Caller-Promise. 4. Jest-Unit-Tests (Prisma/Sync/Mail gemockt, `npm test` grün, 56 Tests): - `src/wahl/zuteilung.service.spec.ts`: Force-Vorrang, Wunschrunden-Fallback bei voller Kapazität, Unzugeteilt-Fall, Konsolidierung unterbesetzter Workshops, Sync-Capture-Anzahl. - `src/auth/team-auth.service.spec.ts`: Invite-Redemption (unbekannt/widerrufen/abgelaufen/aufgebraucht, Gruppen-Link ohne E-Mail, E-Mail-Mismatch, Dublette) + Passwort-Login. @@ -126,7 +128,7 @@ Details, Setup-Anleitung und `.env`-Variablen: [backend/README.md](backend/READM ## 8. Nächste Schritte -1. Flutter-Client ausbauen: Authentik-Authorization-Code-Flow (LT/Verantwortliche), Wahl-Ergebnis-Ansicht, LT-/Verantwortlichen-Admin-Screens (KC/Gemeinde/Teamer anlegen, Onboarding-Anfragen freigeben), WS-Chat-Senden, dann Mobile/Desktop-Targets aktivieren. +1. Flutter-Client ausbauen: **Authentik-Authorization-Code-Flow** (LT/Verantwortliche — braucht eine erreichbare Authentik-Instanz), LT-/Verantwortlichen-Admin-Screens (KC/Gemeinde/Teamer anlegen, Onboarding-Anfragen freigeben), Verantwortlichen-Selbstregistrierung, dann Mobile/Desktop-Targets aktivieren. 2. Authentik-Provider so konfigurieren, dass das Access-Token den `groups`-Claim trägt (Scope „groups"), LT-Gruppe = `AUTHENTIK_LEITUNGSTEAM_GROUP`. (Ops/Config, Code ist fertig.) 3. SMTP konfigurieren (`MAIL_PROVIDER=smtp` + `SMTP_*`/`MAIL_FROM`/`APP_BASE_URL`) und Invite-Mail-Templates finalisieren (aktuell Plain-Text); optional Onboarding-Benachrichtigungen an LT. 4. Push-Benachrichtigungen (FCM/APNs) für Chat/Ankündigungen.