New screens (client/app/lib/screens/):
- wahl_admin_screen.dart — per KC: list/create Wahlen; per Wahl: add
workshops, run the assignment (POST /wahl/:id/zuteilung/run), view the
result table.
- teamer_admin_screen.dart — per Gemeinde: list/create local Teamer
accounts, create group-link or per-email invites (shows the token).
- verantwortliche_register_screen.dart — enter a KC invite code
(GET /onboarding/kc/:code), pick a Gemeinde, submit
(POST /onboarding/verantwortliche); shown on the home screen to a
logged-in Authentik user who has no membership yet.
- ui.dart — shared toast / ErrorText / SectionHeader / promptText.
KcDetailScreen now links to Wahl admin and each Gemeinde row opens Teamer
admin.
Backend: widen the wahl + files LT routes to AuthGuard(['authentik','team'])
for consistency with the other LT controllers. Rebrand web/index.html +
manifest from "kc_app" to "KC-App".
Verified against local Postgres with an isLeitungsteam team token: create
KC/Gemeinde/Wahl/Workshop, run Zuteilung, create Teamer + invite, resolve
an invite code. flutter analyze/test/build web green; backend npm test 56.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Backend:
- AuthentikStrategy / TokenVerificationService: normalise the issuer's
trailing slash and accept both `iss` spellings (Authentik's discovery
issuer and token `iss` carry a trailing slash; the JWKS URL must not
double it). Wire the real konfi-castle issuer into .env.example.
- team token path now goes through toAuthenticatedUser too, so a local
account flagged isLeitungsteam gets the synthetic global LT membership
regardless of token kind.
- LT-admin controllers (kc, gemeinde, onboarding, sync, teamer) accept
['authentik','team'] so such an account can use them. RolesGuard still
enforces the actual LT/role check.
- app.module serves the Flutter web build from client/app/build/web (SPA
fallback covers the OIDC redirect path /v1/auth/callback), falling back
to the interim client/web/ if it isn't built.
Client (client/app/):
- oidc.dart: Authorization-Code + PKCE against Authentik (discovery, S256
challenge, state, token exchange, refresh). Browser bits (sessionStorage,
redirect, URL) behind a conditional import so `flutter test` still
compiles on the VM.
- AppState handles the ?code= callback on bootstrap, stores access +
refresh, refreshes an expired token on restart.
- Login screen: "Mit Konfi-Castle-ID anmelden" button (Leitungsteam /
Verantwortliche) alongside the local Teamer password form.
- admin_screen.dart: LT-only "Verwaltung" — list/create KCs, per KC the
Gemeinden (list/create) and pending Verantwortlichen requests
(approve/reject). Verified end to end against local Postgres with an
isLeitungsteam account (create KC/Gemeinde, list + approve a request).
flutter analyze/test/build web green; backend npm test 56.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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>
Single Flutter codebase under client/app/ with web enabled (mobile/desktop
can be added later; lib/ is platform-agnostic). Talks to the NestJS backend
via a thin REST wrapper; API_BASE is a --dart-define (defaults to the local
backend).
Screens:
- Login: Konfi/guest (invite code), local Teamer password login, Teamer
invite redemption. Token persisted in shared_preferences, restored on
start; GET /auth/me drives a role-aware home.
- Workshop-Wahl (guests): loads /wahl/guest/overview, ordered pick of up to
3 workshops, submits to /wahl/:id/teilnehmer.
- Dateien: /files/:kcId list.
- Chat: channel + message list (read-only; WS send is a follow-up).
State: AppState (ChangeNotifier) exposed via an InheritedNotifier
(AppScope) — no third-party state package. flutter analyze clean,
flutter build web --release passes, one widget smoke test.
Also: interim client/web/ HTML placeholder stays as-is (per plan it is
superseded by this Flutter web build).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>