- ChatChannelType.GRUPPE: created by Leitungsteam (any KC) or a Gemeinde
Verantwortliche/r (own KC), mixing team users and guests/Konfis as
explicit ChatParticipant rows (unlike GEMEINDE_GRUPPE, membership is
not derived from Gemeinde)
- POST /chat/:kcId/gruppen to create, GET participant-candidates, and
POST/DELETE /chat/gruppen/:channelId/participants to manage membership
(creator, LT, or Verantwortliche/r of that KC)
- ChatGateway broadcasts chat:participants-changed on membership change
- PushService updated for nullable ChatParticipant.userId + new
guestAccountId column
- SyncService now replicates ChatParticipant
- Prisma migration + 14 new unit tests (75/75 passing), tsc clean
- CI: add .gitea/workflows/cybedefend-scan.yml + .cybedefend project config
Backend:
- fix(chat): ChatGateway stored the per-socket caller only after the async
token check resolved, so a client that sent chat:join immediately on open
raced ahead and got 4001. The caller is now stored as a promise that the
message handlers await. Verified with a two-client send/receive E2E test
against local Postgres.
Client (client/app/):
- Wahl screen gains a "Ergebnis" tab backed by GET /wahl/guest/results
(PENDING / ASSIGNED with workshop + wish rank / UNASSIGNED).
- Chat channel view loads history over REST, then connects the /chat
WebSocket (chat_socket.dart): live chat:message stream + a compose bar
that sends chat:send. Shows the socket status.
- web_socket_channel dependency added.
flutter analyze/test/build web all green; backend npm test 56.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>