The Flutter SDK image is ~2.8 GB and filled Docker Desktop's VM disk
("read-only file system" while extracting a layer). Build the web bundle
on the host instead and COPY client/app/build/web into the 2-stage
(NestJS build -> slim runtime) image. .dockerignore keeps the bundle,
drops the platform scaffolding. README documents the host `flutter build
web` prerequisite.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
102 lines
5.3 KiB
Markdown
102 lines
5.3 KiB
Markdown
# KC-APP
|
|
|
|
Multi-tenant event, election and communication platform for Konfi-Castle
|
|
events (KCs), replacing the WordPress plugin "Workshop-Wahlen". See
|
|
[plan-kcAppMultiTenantPlatform.prompt.md](plan-kcAppMultiTenantPlatform.prompt.md)
|
|
for the full architecture and phased roadmap.
|
|
|
|
## Run with Docker
|
|
|
|
```bash
|
|
cp backend/.env.example backend/.env # fill in the secrets
|
|
# optional (push): put the Firebase service account at backend/serviceAccount.json
|
|
(cd client/app && flutter build web --release) # host build — see note below
|
|
docker compose up --build
|
|
```
|
|
|
|
`docker-compose.yml` starts PostgreSQL 16 and one `api` container (2-stage
|
|
`Dockerfile`: NestJS build → slim runtime). The container runs
|
|
`prisma migrate deploy` on start and serves the whole app — Flutter web
|
|
client + REST API — on <http://localhost:3000>. Requires Docker Compose v2.
|
|
|
|
The Flutter web bundle is built **on the host** and copied in (a Flutter
|
|
builder stage would pull a ~2.8 GB SDK image). So `flutter build web` must
|
|
run before `docker compose build`. Secrets come from `backend/.env`
|
|
(unquoted, Compose v2 strips quotes) and the service-account JSON is
|
|
bind-mounted read-only — neither is baked into the image.
|
|
|
|
## Structure
|
|
|
|
- `backend/` — NestJS API (Prisma/PostgreSQL, Authentik OIDC as resource
|
|
server, guest/Konfi local accounts, roles/permissions foundation, file
|
|
sharing, chat, local/cloud sync). See [backend/README.md](backend/README.md)
|
|
for setup. Also serves the web client (see below) directly, so it's the
|
|
single entry point for the web experience.
|
|
- `client/app/` — the Flutter client (single codebase; **web** target
|
|
enabled, mobile/desktop can be added later). Login (guest / local Teamer /
|
|
invite redemption), role-aware home, guest Workshop-Wahl, file list,
|
|
read-only chat. See [client/app/README.md](client/app/README.md).
|
|
- `client/web/` — minimal dependency-free HTML/CSS/JS placeholder web
|
|
client, served by the backend at `/`. Superseded by the Flutter web build;
|
|
kept for now as a zero-dependency fallback.
|
|
|
|
## Status
|
|
|
|
Phase 0/1 foundation implemented: monorepo skeleton, Prisma data model (Kc,
|
|
Gemeinde, User, Membership, GuestAccount, Wahl/Workshop/Teilnehmer/Zuteilung,
|
|
File, Chat), Authentik JWT resource-server strategy, guest invite-code login,
|
|
Role-based guard scoped per KC.
|
|
|
|
Phase 2 (Workshop-Wahl engine) implemented: Wahl/Workshop administration,
|
|
guest Teilnehmer submission, Force-Zuteilung overrides, and the assignment
|
|
algorithm ported from the WP plugin's `kc_run_zuteilung` (force-assignments →
|
|
wish rounds 1-3 → random fill → consolidation of underfilled workshops),
|
|
plus CSV export.
|
|
|
|
Phase 3 (Dateifreigabe) implemented: Leitungsteam-only upload tagged with a
|
|
visibility tier (alle / alle außer Konfis / nur LT), list/download for
|
|
Authentik or guest callers filtered by their allowed tiers, storage behind a
|
|
provider abstraction defaulting to Nextcloud/WebDAV (S3-compatible storage
|
|
as an alternative via `STORAGE_PROVIDER=s3`).
|
|
|
|
Phase 5 (Kommunikation) implemented: Gemeinde-Gruppenchat, 1:1-DMs, LT-
|
|
kanalübergreifende Kanäle, Broadcast (read-only für Konfis); channel/history
|
|
via REST, real-time send/receive via a raw WebSocket gateway authenticated
|
|
with the same Authentik/guest tokens as the REST API.
|
|
|
|
Phase 6 (Hybrid Lokal/Cloud-Server & Sync) implemented: an append-only
|
|
replication log (`SyncLogEntry`) captured by every feature service after its
|
|
writes; the local (on-site) server periodically pushes/pulls against the
|
|
cloud server's `/sync/ingest` + `/sync/export` endpoints (shared-secret
|
|
authenticated, not user auth). No conflict resolution needed by design - the
|
|
local server is the sole source of truth while an event is live.
|
|
|
|
Since then: local (non-Authentik) Gemeinde Teamer accounts + invites
|
|
(`teamer/`, `auth/team-login`), Gemeinde CRUD (`gemeinde/`), Gemeinde
|
|
Verantwortliche self-registration with LT approval (`onboarding/`), JIT
|
|
`User` provisioning on first Authentik login, LEITUNGSTEAM derived from the
|
|
Authentik `groups` claim, and an email module (`mail/`, log-only by default,
|
|
SMTP opt-in) that sends personal Teamer invites. First Prisma migration is
|
|
in (`backend/prisma/migrations/`); the backend has been run end to end
|
|
against a local PostgreSQL 16. `npm test` covers the assignment algorithm
|
|
and the new auth/onboarding services (56 tests).
|
|
|
|
Phase 7 (Flutter client): `client/app/` is a single Flutter codebase with
|
|
the **web** target enabled — guest / local-Teamer / invite login, the
|
|
Authentik Authorization-Code + PKCE flow (`lib/oidc.dart`) for
|
|
Leitungsteam/Verantwortliche, role-aware home, guest Workshop-Wahl (wishes +
|
|
result), file list, live WebSocket chat, FCM web-push registration, and the
|
|
Leitungsteam admin screens: KCs, Gemeinden, onboarding approvals, full
|
|
Workshop-Wahl administration (create/open/close, workshops, Force-Zuteilung,
|
|
run assignment, CSV export), Teamer accounts + invites, LT file upload, plus
|
|
the Verantwortlichen self-registration flow. `flutter build web` /
|
|
`flutter test` pass; the backend serves the build at `/` (SPA fallback
|
|
covers the OIDC redirect `/v1/auth/callback`).
|
|
|
|
Still to do: a live browser test of the OIDC round-trip; mobile/desktop
|
|
targets. Going live needs external config — the Authentik redirect + a test
|
|
account, Nextcloud/S3 credentials, SMTP, and the Firebase push secrets
|
|
(`apiKey`/`appId`/VAPID key + a service-account JSON). See
|
|
`backend/.env.example` and `client/app/web/index.html`.
|
|
|