# greenPoker — Umsetzungsstrategie v6

**Erstellt:** 2026-05-21
**Vorgänger:** `umsetzungsStrategie_v5.md` (Stand 2026-05-08, bis I6)
**Status:** Verbindliche Einzelquelle ab dieser Session — alle früheren Strategiepapiere sind archiviert.
**Scope:** Gesamtprojekt — Phasen A bis E, Phase C bis I16 abgeschlossen.

**Selbst-Genügsamkeit:** Dieses Dokument enthält alle Informationen, die für Folge-Sessions benötigt werden. Externe Quellen (`concurrent-tinkering-thompson.md`, `daten_schema-analyse_1.html`, `greenPoker_C2_session_entscheidung.md`, `handhistoryStrategie.html`, `greenPoker_v43_strategie_sqlite.html`, `backend/README.md`) wurden vollständig eingearbeitet und müssen für die weitere Arbeit nicht mehr konsultiert werden.

---

## Inhaltsverzeichnis

0. [Plan-Revisionen v5 → v6](#0--plan-revisionen-v5--v6)
1. [Übergeordneter Rahmen](#1--übergeordneter-rahmen)
2. [Decision Log](#2--decision-log)
3. [Scope und Abgrenzung](#3--scope-und-abgrenzung)
4. [Architektur-Übersicht](#4--architektur-übersicht)
5. [Datenmodell](#5--datenmodell)
6. [Programmstruktur und Modul-Verantwortlichkeiten](#6--programmstruktur-und-modul-verantwortlichkeiten)
7. [API-Spezifikation](#7--api-spezifikation)
8. [Querschnittsthemen](#8--querschnittsthemen)
9. [Schema-Analyse — Risiken SA-1 bis SA-7](#9--schema-analyse--risiken-sa-1-bis-sa-7)
10. [Risiken R1 bis R11](#10--risiken-r1-bis-r11)
11. [Test-Strategie](#11--test-strategie)
12. [Roadmap mit Iterationsschritten](#12--roadmap-mit-iterationsschritten)
13. [Anhang A — Glossar](#anhang-a--glossar)
14. [Anhang B — Datenbank-Indexes](#anhang-b--datenbank-indexes)
15. [Anhang C — Konfigurations-Konstanten](#anhang-c--konfigurations-konstanten)
16. [Anhang D — Frontend-Anpassungspunkte](#anhang-d--frontend-anpassungspunkte)
17. [Anhang E — Übergabe-Logs der abgeschlossenen Iterationen](#anhang-e--übergabe-logs-der-abgeschlossenen-iterationen)

---

## 0 · Plan-Revisionen v5 → v6

### 0.1 Anlass

v5 wurde am 2026-05-08 nach Abschluss von I6 erstellt und nicht weitergepflegt. Der reale Backend-Stand am 2026-05-21 liegt bei I16 (Phase C funktional vollständig für Single-Table; 341 Tests grün). Die Diskrepanz zwischen Plan-Stand und Realstand machte v5 als Steuerungsdokument unbrauchbar. Diese Revision setzt v6 als alleinige Quelle in Kraft.

### 0.2 Inhaltliche Änderungen gegenüber v5

| Bereich | v5 | v6 |
|---|---|---|
| Iterations-Status | I1–I6 abgeschlossen | I1–I16 abgeschlossen |
| Phasen-Reihenfolge | A → C → B → D → E (D4) | A → C[bis I16] → C[I17, I18, I19] → B[Port Phase 1] → C/D[Lobby-Umbau, Cashgame in React] (siehe D24) |
| Rebuy-Mechanik | `rebuy_mode ∈ {none, 1+addon}` (Skizze in I8) | Beide Modi „klassisch" und „re-entry" wählbar, plus „freeze-out" (= heute `none`); Wizard erlaubt Wahl (D23) |
| Übergeordneter Rahmen | Verweis auf `concurrent-tinkering-thompson.md` | Vollständig in Abschnitt 1 eingearbeitet |
| SA-Risiken | Verweis auf `daten_schema-analyse_1.html` | Vollständig in Abschnitt 9 ausformuliert |
| Session-Mechanismus | Verweis auf `greenPoker_C2_session_entscheidung.md` | Vollständig in Abschnitt 1.4 eingearbeitet |
| Hand-History-Schema | Verweis auf `handhistoryStrategie.html` | Begründung in Abschnitt 1.6 eingearbeitet |
| Stack-Begründung | Verweis auf `greenPoker_v43_strategie_sqlite.html` | Vollständig in Abschnitt 1.2 eingearbeitet |
| Übergabe-Logs | Verweis auf `backend/README.md` | Als Anhang E eingearbeitet (soweit dokumentiert) |
| HTML-Stand | v45.72 / v44.22 | v45.82 / v44.26 (Anhang D aktualisiert) |

### 0.3 Neue Entscheidungen

- **D23 — Rebuy-Mechanik:** Wizard-Auswahl zwischen `freeze-out` (keine Rebuys), `classic` (Rebuy-Phase + Addon) und `re-entry` (Bust-Out-Re-Entry).
- **D24 — Phasen-Reihenfolge:** Reihenfolge C ersetzt v5/D4. Begründet in der Einschätzung `Reviews/einschaetzung_reactMigration_vor_backend_2026-05-21.md`.
- **D25 — Lobby-Anzeigeumfang:** Turnier-Lobby muss angemeldete Spieler in einer Liste anzeigen; während des laufenden Turniers ist die Liste nach Stack-Size sortiert; Auszahlungsstruktur wird sowohl schematisch (Prozentsätze) als auch konkret (Chip-Beträge bezogen auf aktuellen Prize Pool) angezeigt.

### 0.4 Beibehaltene Entscheidungen

D1 bis D22 aus v5 bleiben unverändert verbindlich (siehe Abschnitt 2).

---

## 1 · Übergeordneter Rahmen

Dieser Abschnitt ersetzt die früher externe Datei `concurrent-tinkering-thompson.md` und ergänzt sie um die seitdem getroffenen Stack-, Session- und Schema-Entscheidungen.

### 1.1 Projekt-Vision und Phasen-Modell

**Projekt:** greenPoker — webbasiertes Texas-Hold'em-Spielgeld-Pokerportal für einen privaten Club. Single-Node-Deployment, ein zentraler Node-Prozess, mehrere gleichzeitige Tische, Echtzeit-Sync via Socket.IO.

**Ziel-Funktionsumfang:**
- Turniere (Single-Table und Multi-Table) mit Wizard, Anmeldung, Sitzzuteilung, Blind-Levels, Bounties, Payouts, Series-Punkte, Wiederholungen.
- Cashgames (Phase D) — eigenständige Domäne mit Buy-In/Stand-Up, Top-Up-Logik.
- Server-autoritative Hold'em-Logik: Deck-Shuffle aus `crypto.randomBytes`, Hand-Evaluation, Pot/Side-Pot-Verteilung, Action-Validierung.
- Hand-History, Replayer mit Sichtbarkeitsfilter.
- HUD-Stats (VPIP, PFR, 3-Bet, Aggression Factor).
- Chat (Club + Tisch), Moderation, Notifications, Inbox, Club-Board, Bans, Admin-Backoffice.
- React-basiertes Frontend (Phase B) nach Konsolidierung Phase C.

**Phasen-Modell** *(in Reihenfolge ihrer ursprünglichen Konzeption, aktualisiert durch D24 in Abschnitt 2):*

```
Phase A — HTML-Politur                            ✅ abgeschlossen (Stand v45.82 / v44.26)
Phase C — Backend mit HTML-Anbindung              ✅ I1–I16; I17–I19 ausstehend (siehe D24)
Phase B — React-Port                              ausstehend (nach I19, siehe D24)
Phase D — Cashgame-Engine                         ausstehend (verschränkt mit Phase B, siehe D24)
Phase E — Moderator-Rolle, E-Mail-Versand,
           optional PostgreSQL                    ausstehend (Backlog)
```

**Multiplayer-Scope** (verbindlich, unverändert seit v1):
- Ein Node-Prozess, ein SQLite-Writer.
- Mehrere gleichzeitige Tische, mehrere User pro Tisch.
- Socket.IO mit Rooms pro Tisch (`table:<id>`), pro User (`user:<id>`), pro Turnier (`tournament:<id>`), pro Club (`club:global`).
- Kein horizontales Scaling, kein Multi-Region-Setup, keine externe Replikation.
- Reversibilitätspfad nach Postgres (für Phase E) wird offen gehalten, ist aber nicht aktiv geplant.

### 1.2 Tech-Stack — verbindliche Festlegung

Begründungskette für alle Wahlen, damit Folge-Sessions die Trade-offs nachvollziehen können.

| Layer | Wahl | Begründung |
|---|---|---|
| Sprache | TypeScript (strict) | Einheit von Frontend (HTML/JS heute, React/TS später) und Backend; typsichere Schnittstellen reduzieren Integrationsfehler. |
| Runtime | Node.js | Synchrone Event-Loop passt zu Socket.IO; bewährter Multiplayer-Stack. |
| Web-Framework | Express | Minimal-Framework, geringer Lock-in; reicht für REST-Layer. |
| Realtime | Socket.IO | Wiederverbindungsstrategie, Rooms, Browser-Kompatibilität; eingebaute Heartbeat-Logik. |
| Persistenz | SQLite (Datei `data/greenpoker.db`) | Workload-Fit: viele kleine Writes aus genau einem Writer-Prozess. Latenzärmer als Postgres in genau diesem Szenario. Kein separater DB-Prozess, kein Docker-Compose, kein Connection-Pool. Backup = `.db`-Datei kopieren. |
| SQLite-Binding | `better-sqlite3` | Synchron, nativer Treiber, kein async-Dispatch-Overhead. Transaktionen laufen in derselben Tick-Phase wie die eingehende Aktion. |
| ORM | Drizzle ORM + `drizzle-kit` | Typisierte Queries, Migrations als SQL-Files. Keine eigene Engine-IPC wie Prisma. |
| Journal-Modus | WAL (`journal_mode=WAL`) | Concurrent Reads neben einem Writer. `synchronous=NORMAL` als Balance zwischen Durability und Throughput. |
| Foreign Keys | `PRAGMA foreign_keys=ON` | Erzwingt FK-Constraints; in SQLite default off. |
| Busy-Timeout | `busy_timeout=5000` | Defensiv für gelegentliche Read-Lock-Konkurrenz. |
| Auth-Hashing | bcrypt | Verbreitet, geprüft; ausreichend für Spielgeld-Scope. |
| Validierung | zod | Eingangs-Validierung von API-Bodies und JSON-Blobs (SA-3). |
| Tests | vitest | TypeScript-nativ, schneller als Jest bei TS; In-Memory-SQLite-Tests möglich. |

**Verworfene Alternativen (mit Begründung):**

| Alternative | Verworfen, weil |
|---|---|
| Postgres | Workload-Fit-Nachteil bei vielen kleinen Writes aus einem Writer-Prozess; zusätzliche Betriebskomplexität (Connection-Pool, separater Prozess, Docker-Compose). Reversibilitätspfad bleibt offen. |
| Prisma | Eigene Engine-IPC, höhere Latenz, weniger Kontrolle über generiertes SQL. |
| libSQL / Turso | Zusätzliche Abstraktion ohne Replikationsziel. Kein Mehrwert im Single-Node-Scope. |
| JWT-Sessions | Siehe 1.4. Im Single-Node-Scope nur Komplexität ohne Gegenwert. |
| MongoDB / Document-DB | Für ein stark relationales Domänen-Modell (Accounts, Ledger, Hand-History) unpassend. |

### 1.3 Backup-Strategie

- `db.backup('backup/greenpoker-YYYY-MM-DD.db')` über `better-sqlite3`-eigene Online-Backup-Methode — WAL-sicher, kein Server-Stop nötig.
- Worker (`workers/backup.worker.ts`, in I14 geplant aber bis dato nicht implementiert, siehe Anhang E I14) ruft Backup periodisch auf (`BACKUP_INTERVAL_HOURS = 24`).
- Aufbewahrung: `BACKUP_KEEP_DAYS = 7` rollende Backups, ältere werden gelöscht.
- Pfad `backend/data/backups/` (gitignored).
- Backup ist Pflicht-Voraussetzung vor jedem Migrations-Lauf in Produktion.

### 1.4 Session-Mechanismus — Server-Session-ID (Variante A)

**Verbindliche Entscheidung:** Opaque Server-Session-ID im `httpOnly`-Cookie. JWT ist verworfen.

**Funktionsweise:**
1. Beim Login: Server prüft E-Mail + Passwort, erzeugt 32-Byte-Zufalls-ID (`crypto.randomBytes(32).toString('hex')`), speichert `{id, user_id, created_at, expires_at, last_seen, user_agent}` in Tabelle `sessions`, setzt Cookie `session_id=<hex>` mit `httpOnly; Secure; SameSite=Lax; Path=/; Max-Age=<SESSION_LIFETIME_DAYS>`.
2. Bei Folge-Request: Cookie wird vom Browser automatisch gesendet. Middleware liest, schlägt in `sessions` nach (Prepared Statement), hängt `req.user` an.
3. Bei Logout: `DELETE FROM sessions WHERE id = ?` plus Cookie-Clear (`Max-Age=0`).
4. „Alle Geräte abmelden": `DELETE FROM sessions WHERE user_id = ?`.
5. Cleanup-Worker (`cleanup.worker.ts`, alle `CLEANUP_WORKER_INTERVAL_MIN = 60` Min) löscht abgelaufene Sessions.

**Warum nicht JWT:**
- Single-Node-Scope macht JWTs einzigen strukturellen Vorteil (zustandslose Multi-Node-Validierung) gegenstandslos.
- Widerruf bei Ban / Passwort-Reset / „alle Geräte abmelden" ist mit Server-Session-ID eine `DELETE`-Query; bei JWT bräuchte es eine Blocklist plus Refresh-Token-Rotation, was den Stateless-Vorteil aufhebt.
- DB-Lookup pro Request kostet bei WAL-SQLite + Prepared Statement einstellige Mikrosekunden — gegenüber dem Pro-Action-Write vernachlässigbar.
- `httpOnly; Secure; SameSite=Lax` schützt beide Varianten gleich gegen XSS und CSRF.
- Reversibilitätspfad: bei späterer Multi-Node-Umstellung lässt sich `sessions` in einen geteilten Store (Postgres, Redis) verschieben, ohne das Verfahren zu wechseln.

**Konstanten:** `SESSION_LIFETIME_DAYS = 30` (Refresh bei jedem Request über `last_seen`).

### 1.5 Hand-History-Schema — Variante 2 (mit `hand_board_cards`)

**Verbindliche Wahl:** Variante 2 mit zusätzlicher Tabelle `hand_board_cards` und erweiterten Stats-Flags pro Spieler.

**Strukturelle Bestandteile:**

- `hands` — Kopfdaten pro Hand (Tisch, Turnier, Blind-Beträge, Dealer-Sitz, `board` als kompakter String für Replayer, Pot-Total, Rake).
- `hand_board_cards` — pro Karte am Board eine Zeile (`street ∈ {flop, turn, river}`, `position 0/1/2`, `rank 2..14`, `suit ∈ {h,d,c,s}`). Erlaubt analytische Queries auf Board-Texturen, Flush-Boards, gepaarten Boards (D21).
- `hand_players` — pro Spieler einer Hand: Sitz, Start-Stack, Hole-Cards (verschlüsselt persistiert, sichtbar nur für Eigentümer bzw. bei Showdown), `shown_at_showdown`, gewonnener Betrag, sowie pro-Hand-Stats-Flags `vpip`, `pfr`, `three_bet`, `agg_factor_num`, `agg_factor_den`.
- `actions` — pro Action eine Zeile (`seq`, `street`, `action ∈ {fold, check, call, bet, raise, allin}`, `amount`, `at`).

**Begründung gegenüber Variante 1 (Single-Table):**
- Variante 1 hätte das Board als JSON-Blob in `hands.board` gespeichert. Das wäre für Replayer ausreichend, blockiert aber analytische Queries auf Karten-Ebene.
- Variante 2 dupliziert die Board-Karten bewusst (kompakter String in `hands.board` PLUS Tabelle `hand_board_cards`) — beide werden in einer Transaktion geschrieben (D21). Replayer liest String, Analyse liest Tabelle.

**Stats-Definition** (D17b, PokerTracker-4-Standard):
- VPIP = Voluntary Put In Pot — gezählt: jede freiwillige Geldzugabe preflop außer Blind-Posten. BB-Post zählt nicht.
- PFR = Pre-Flop Raise — erste freiwillige Raise preflop.
- 3-Bet = Erste Re-Raise preflop.
- AF = Aggression Factor = (Bet + Raise) / Call. Numerator und Denominator separat gespeichert für korrekte Aggregation über mehrere Hände.

**Sichtbarkeit im Replayer (D7):** Alle Hände eines Tisches sind während eigener Anwesenheit sichtbar; eigene gemuckte Karten immer; fremde Karten nur bei `shown_at_showdown = 1`. Filter erfolgt über JOIN auf `seat_sessions` (Zeit-überlappung mit Hand-Zeitfenster).

### 1.6 Zentrale Spielregel-Konstanten (D17c)

| Konstante | Wert | Bedeutung |
|---|---|---|
| `TIME_PER_ACTION_DEFAULT_SEC` | 30 | Bedenkzeit pro Aktion (Default; im Turnier-Wizard überschreibbar via `speed`). |
| `TIME_BANK_INITIAL_MS` | 90 000 | Time-Bank-Startwert pro Spieler. |
| `TIME_BANK_REFILL_PER_HAND_MS` | 5 000 | Auffüllbetrag pro abgeschlossener Hand. |
| `TIME_BANK_MAX_MS` | 90 000 | Obergrenze für Time-Bank-Auffüllung. |
| `LATE_REG_DEFAULT_MIN` | 30 | Late-Registration-Fenster ab Turnierstart. |
| `MULTI_TABLE_LIMIT` | 4 | Maximalzahl simultaner Tische pro Spieler. |
| `DEFAULT_SEAT_COUNT` | 8 | Sitze pro Tisch. |
| `DEFAULT_START_STACK` | 10 000 | Startstack bei Turnierstart. |
| `MIN_TOURNAMENT_PLAYERS` | 2 | Mindest-Spielerzahl für Turnier-Start. |
| `MAX_TOURNAMENT_PLAYERS` | 8 | Maximal-Spielerzahl pro Turnier (Single-Table-Phase). |

Vollständige Konstanten-Liste siehe Anhang C.

---

## 2 · Decision Log

Verbindliche Entscheidungen. Spätere Abweichungen müssen explizit als Plan-Revision dokumentiert werden.

| Nr | Thema | Entscheidung | Konsequenz |
|---|---|---|---|
| D1 | Geld-Modell | Reines Spielgeld | Kein Payment-Gateway, keine KYC, Cashier-UI ist Mock; „1 Chip = $0,001" kosmetisch. |
| D2 | Rollen | Nur `admin` und `player` | `users.role` als ENUM; Moderator-Rolle ist Phase E. |
| D3 | Tisch-Erstellung | Vollfreigabe für alle Spieler | Spieler dürfen Cashgames, Turniere, Serien anlegen; nur Bounty-Clubkonto bleibt admin-only. |
| D4 | Tournament/Cashgame | Tournament zuerst, Cashgame in Phase D | Phase C liefert Tournament-Engine; Cashgame-UI im Frontend bleibt deaktiviert bis Phase D. |
| D5 | Multi-Table-Balancing | I17 | `TournamentBalancer` als abgrenzbares Modul. |
| D6 | Hand-History-Schema | Variante 2 + erweiterte Flags | Separate `hand_players`-Tabelle mit `vpip/pfr/three_bet/agg_factor_num/den`. |
| D7 | Replayer-Sichtbarkeit | Alle Hände am Tisch während Anwesenheit; eigene gemuckte Karten sichtbar | Filter via `seat_sessions`. |
| D8 | Multi-Table & Time-Bank | Limit 4 Tische; TB kumuliert mit Auffüllung; Auto-Sit-Out bei Ablauf | Pro `tournament_players`-Zeile aktueller TB-Stand. |
| D9 | Tournament-Wiederholung | Hybrid (Schedule + 4-Wochen-Window), In-Process-Scheduler | `tournament_schedules` + Worker-Loop beim Server-Start. |
| D10 | User-Preferences | Serverseitig pro User | Tabelle `user_preferences`, REST `GET/PUT /api/preferences`. |
| D11 | Notifikations-Transport | Socket.IO Rooms + REST | Module emittieren in eigene Rooms, kennen einander nicht. |
| D12 | Client für Phase C | Bestehendes HTML wird angepasst | Mock-Arrays werden nach und nach durch API-Calls ersetzt. |
| D13 | SRP-Granularität | Lesart 3: Module nach Sachgebiet, interne Klassen-Splits wo sinnvoll | Iterativ verfeinert. |
| D14 | Iterations-Größe | Klein, jede Iteration lauffähig | Schema-Änderungen und Spiellogik niemals in derselben Session mischen. |
| D15 | Buchungssystem | Hybrid (doppelte Buchung + Saldo-Cache); Spieler-Chips als Konten; Buchungen final | Stornierung nur per Gegenbuchung. |
| D15a | Initial-Konten | Bounty Pool + Spenden | Strafgelder/Freeroll-Sammler nicht implementiert, UI ausgeblendet. |
| D16 | Bans | 3 Stufen (Verwarnung/Spielsperre/Vollsperre), alle befristet | `bans.expires_at` zwingend; Auto-Sit-Out bei laufendem Turnier. |
| D17a | Series-Punkte | PokerStars-Formel `Buyin × √Spielerzahl × 1/(Platz+1)` | Persistiert in `tournament_players.series_points`. |
| D17b | Stats-Definition | PokerTracker-4-Standard | VPIP zählt nicht BB-Post; PFR erste freiwillige Raise; 3-Bet erste Re-Raise. |
| D17c | Zeitwerte | Bedenkzeit 30 s; TB 90 s + 5 s/Hand bis 90 s; Late-Reg 30 min; Backup 24 h/7 Tage; Session 30 d Refresh | Defaults als Konstanten in `config.ts`. |
| D18 | Login-Identifier | E-Mail-Adresse, lowercase normalisiert | `username` entfällt; Doppelt-Eingabe der E-Mail im Registrierungsformular. |
| D19 | Anzeigename | Pflicht bei Registrierung, eindeutig (COLLATE NOCASE), im Profil mit 30-Tage-Cooldown änderbar | Überall statt `username`. |
| D20 | Game-State-Persistenz | Memory-First für `seats`; DB-Tabelle nur als Snapshot bei Hand-Ende und Sit-Down/Stand-Up | Reduziert Write-Amplifikation (SA-5). |
| D21 | Hand-Board-Speicherung | Hybrid: `hands.board` als String (Replayer) + Tabelle `hand_board_cards` (Karten-Analyse) | Synchron in einer Transaktion. |
| D22 | Sitzzuteilung Turniere | Automatisch beim Turnierstart (Fisher-Yates); manuelles Sitzen für Tournament-Tische serverseitig blockiert | `table.join` mit `seatIndex` gibt Fehler 400 für `kind='tournament'`. Cashgame-Tische nicht betroffen. |
| **D23** | **Rebuy-Mechanik** | **Drei Modi im Wizard wählbar: `freeze-out` (keine Rebuys), `classic` (Rebuy-Phase + optionales Addon), `re-entry` (Bust-Out-Re-Entry im Fenster)** | **Schema-Erweiterung: `tournaments.rebuy_mode ∈ {none, classic, re-entry}`, `rebuy_phase_min`, `rebuy_cost`, `addon_cost`, `addon_chips`; `tournament_players.re_entry_count`. Lifecycle-Erweiterung um Rebuy-Phase-Übergang. UI-Anpassung in Wizard und Turnier-Lobby.** |
| **D24** | **Phasen-Reihenfolge** | **C-Restpunkte (I17–I19) in HTML → React-Port Phase 1 (1:1) → Lobby-Strukturarbeit in React verschränkt mit weiteren Backend-Anpassungen → Cashgame (Phase D) parallel Backend+React → Phase E** | **D4 bleibt sachlich gültig (Cashgame nach Tournament-Engine); die Reihenfolge wird auf Iterations-Ebene verfeinert. Vermeidet Wegwerf-Arbeit in beide Richtungen. Begründung: `Reviews/einschaetzung_reactMigration_vor_backend_2026-05-21.md`.** |
| **D25** | **Lobby-Anzeigeumfang** | **Turnier-Lobby zeigt angemeldete Spieler in Liste; Sortierung bei Status `running` nach Stack-Size absteigend; Auszahlungsstruktur schematisch (Prozente) UND konkret (Chip-Beträge bei aktuellem Prize Pool) sichtbar. Standard-Buttons: Anmelden, Abmelden, Starten, Rebuy/Re-Entry (modusabhängig).** | **Erfordert in I19: API-Erweiterung um Spielerliste-Endpoint mit Stacks (live), Payout-Vorschau, Lobby-Refactoring im HTML. Stack-Live-Werte kommen aus `SeatStateMemory` über Realtime-Bus.** |

---

## 3 · Scope und Abgrenzung

### 3.1 In Scope (gesamtes Projekt)

**Backend** (Node + TypeScript + Express + Socket.IO + better-sqlite3 + Drizzle ORM):

- Authentifizierung mit E-Mail-Login (D18), Sessions (1.4), Registrierungs-Workflow mit Admin-Genehmigung.
- User-Profile mit `display_name` (D19), Cooldown-Endpoint, serverseitige Preferences (D10).
- Buchungssystem (doppelte Buchführung) mit `ConsistencyCheckService` (SA-1/SA-2), zod-Blob-Validation (SA-3), CHECK-Constraints (SA-4).
- Tournament-Engine: Wizard-Erstellung, Registration + automatische Sitzzuteilung (D22), Schedules mit Wiederholung (D9), Lifecycle, Blind-Levels, Bounty-Verteilung, Payout-Berechnung, Single-Table und Multi-Table mit Balancing (I17).
- Rebuy-Modi (D23): freeze-out / classic / re-entry mit jeweils eigener Mechanik.
- Lobby-Anzeigeumfang (D25): Spielerliste mit Live-Stacks, Auszahlungsstruktur.
- Server-autoritative Hold'em-Logik: Deck-Shuffle, Kartenverteilung, Hand-Evaluation, Pot/Side-Pots, Action-Validierung.
- Memory-First-Verwaltung der `seats` (D20) mit periodischem DB-Snapshot.
- Hand-History und Replayer (Variante 2) mit `hand_board_cards` (D21).
- HUD-Stats (VPIP, PFR, 3-Bet, AF).
- Series-Stats und Leaderboards.
- Chat (Club + Tisch) mit Moderation.
- Club Board, Notifications/Inbox, Bans.
- Echtzeit-Sync via Socket.IO Rooms.
- **Cashgame-Engine (Phase D):** Anforderungs-Spezifikation steht aus; Phasen-Skizze in 12.6.

**Frontend:**

- Phase A/C: HTML (`HTML/greenPoker_v45.82.html`, `HTML/greenPokerAdmin_v44.26.html`) inkrementell durch API-Calls angebunden.
- Phase B: vollständige React-Port-Phase (1:1-Migration der heutigen UI-Domänen, danach Lobby-Umbau und Cashgame in React).

### 3.2 Nicht in Scope

- Echte Payment-Anbindung, KYC, E-Mail-Versand (SMTP).
- Moderator-Rolle (Phase E, D2).
- PostgreSQL-Migration (Reversibilitäts-Pfad, nicht aktiv geplant).
- Multi-Node-Skalierung, Multi-Region.
- Mobile native Apps (Web-Frontend ist ausreichend).

### 3.3 Aktualisierte Phasen-Folge (gemäß D24)

```
Phase A — HTML-Politur                                            ✅ abgeschlossen
       ↓
Phase C — Backend mit HTML-Anbindung
   I1–I16 (Auth bis HUD/Stats)                                    ✅ abgeschlossen
   I17 — Multi-Table-Balancer (in HTML)                            ausstehend
   I18 — Rebuy-Vollausbau (in HTML)                                ausstehend
   I19 — Turnier-Lobby-Anzeigeumfang (in HTML)                     ausstehend
       ↓
Phase B — React-Port
   B1 — Stack-Festlegung und Plattform-Setup                       ausstehend
   B2 — 1:1-Migration der stabilen Domänen                         ausstehend
       ↓
Phase B / D verschränkt
   B3/D1 — Cashgame-Backend + React-Cashgame-Komponenten          ausstehend
   B4 — Lobby- und UI-Strukturarbeiten in React                    ausstehend
       ↓
Phase E — Moderator-Rolle, E-Mail-Versand, optional PostgreSQL    Backlog
```

---

## 4 · Architektur-Übersicht

### 4.1 Schichten-Modell

```
Client (Browser)
  HTML + JS (fetch + socket.io-client) — heute
  React + TS (Phase B) — geplant
        ↓
Transport-Schicht
  Express HTTP REST (/api/*)  +  Socket.IO (Rooms: table:/user:/tournament:/club:)
        ↓
Anwendungs-Schicht (Sachgebiet-Module)
  Auth · User · Ban · Accounting · Tournament · Game · HandHistory · Chat · ClubBoard · Notification · Stats · Admin
        ↓
Querschnitt
  RealtimeBus · Logger · Migrator · BackupWorker · ScheduleWorker · CleanupWorker · ConsistencyCheckService · BlobValidator · PolymorphicResolver · Clock
        ↓
Persistenz-Schicht
  Drizzle ORM → better-sqlite3 (synchron, WAL) → greenpoker.db
```

**Modul-Prinzipien (D13, Lesart 3):**
- Ein Modul entspricht einem Sachgebiet (Auth, Accounting, Game, Tournament, …).
- Innerhalb eines Moduls werden Klassen nach SRP gesplittet, wenn nötig (`tournament.repository.ts` getrennt von `tournament.lifecycle.service.ts` getrennt von `payout.calculator.ts`).
- Module kennen einander **nicht direkt**, sondern kommunizieren über:
  - Direkt-Injektion in `server.ts` (DI-Wurzel).
  - Realtime-Bus für asynchrone Events.
  - Callbacks (z. B. `onUserCreated`, `onUserRegistered`, `onRequestLoad`).

### 4.2 Memory-First-Strategie für Spielzustand (D20)

`SeatStateMemory` (in `modules/game/seat-state.memory.ts`) hält In-Memory-Map `Map<tableId, Map<seatIndex, SeatState>>`. DB-Tabelle `seats` ist **Snapshot-Speicher**, kein Action-Log.

**Synchronisations-Punkte:**

| Ereignis | Aktion |
|---|---|
| Tournament-Start (D22) | Memory-Init + DB-Update für alle Sitze (Fisher-Yates) |
| `table.leave` (Stand-Up) | Memory-Delete + DB-Update `user_id=NULL` + `seat_sessions.stood_up_at` |
| Hand-Ende | Snapshot-Write: `UPDATE seats SET stack=?, ...` für alle besetzten Sitze |
| Server-Start | Bootstrap-Recovery aus `seats`-Tabelle |
| Rebuy/Re-Entry (I18) | Memory-Update Stack + DB-Update (gleiche Transaktion wie Buchung) |

### 4.3 Datenfluss einer Spieler-Action (Beispiel: `raise`)

```
Browser: socket.emit('action', { handId, type:'raise', amount: 200 })
  → game.gateway: validiert Inhaber, prüft am-Zug, prüft Stack-Decken
    → action.validator: Min-Raise, Stack-Limit, Pot-Limit (bei PL später)
      → hand.engine: nimmt State, gibt neuen State zurück (pure)
        → hand.repository: persist action in `actions`
        → SeatStateMemory: stack-Adjustierung
        → bei Hand-Ende: snapshot in `seats` + `hand_players.won_amount`
      → realtime.bus.emit('state-update', tableId, newState)
        → io.to(`table:${tableId}`).emit('state-update', newState)
          → Browser empfängt und rendert
```

---

## 5 · Datenmodell

### 5.1 Migrationsreihenfolge

| # | Iteration | Tabellen |
|---|---|---|
| 0001 | I1 | `users` (email, display_name, display_name_changed_at), `sessions` |
| 0002 | I2 | `bans` |
| 0003 | I3 | `accounts`, `transactions`, `ledger_entries` |
| 0004 | I4 | `user_preferences` |
| 0005 | I5 | `tables`, `seats` (snapshot_at), `seat_sessions` |
| 0006 | I6 | `tournaments` (inkl. tableId-FK), `tournament_players`, `blind_levels` |
| 0006b | I7 | `tournament_schedules`, `tournament_series` |
| 0007 | I9 | `hands`, `hand_board_cards`, `hand_players`, `actions` |
| 0008 | I12 | `chat_messages`, `word_filters`, `mutes`, `chat_room_settings` |
| 0009 | I13 | `inbox_items`, `popup_log` |
| 0010 | I14 | `club_board` |
| **0011** | **I18 (geplant)** | **Schema-Erweiterung Rebuy: `tournaments.rebuy_mode` ENUM erweitern; `rebuy_phase_min`, `rebuy_cost`, `addon_cost`, `addon_chips`; `tournament_players.re_entry_count`** |

### 5.2 Vollständiges Schema (Stand I16, aus `backend/src/db/schema.ts`)

#### Identität & Zugang

```
users
├── id INTEGER PK, email TEXT UNIQUE, display_name TEXT (UNIQUE NOCASE)
├── display_name_changed_at INTEGER NULL
├── password_hash TEXT, role ENUM('admin','player') default 'player'
├── status ENUM('pending','active','banned') default 'pending'
├── avatar_path TEXT NULL, created_at INTEGER, last_seen_at INTEGER NULL

sessions
├── id TEXT PK (64 hex), user_id FK → users(id) ON DELETE CASCADE
├── created_at INTEGER, expires_at INTEGER, last_seen INTEGER, user_agent TEXT NULL

bans
├── id INTEGER PK, user_id FK → users ON DELETE CASCADE
├── level ENUM('warning','play_block','full'), reason TEXT NULL
├── issued_by FK → users, issued_at INTEGER, expires_at INTEGER
```

#### Buchungen (doppelte Buchführung, D15)

```
accounts
├── id INTEGER PK, owner_type ENUM('user','club'), owner_id INTEGER NULL
├── name TEXT, balance_cache INTEGER default 0, currency TEXT default 'chips', created_at INTEGER

transactions
├── id INTEGER PK, ts INTEGER
├── from_account FK → accounts, to_account FK → accounts
├── amount INTEGER (> 0 per CHECK in Migration)
├── ref_type ENUM('buyin','rake','bounty','transfer','admin','donation','reverse')
├── ref_id INTEGER NULL, note TEXT NULL
├── created_by FK → users, reversed_by INTEGER NULL

ledger_entries
├── id INTEGER PK, transaction_id FK → transactions
├── account_id FK → accounts, amount INTEGER (signed), ts INTEGER
```

#### User-Konfiguration

```
user_preferences (1:1 zu users)
├── user_id PK FK → users ON DELETE CASCADE
├── table_look_json TEXT, layout_slots_json TEXT, general_json TEXT, gameplay_json TEXT
├── updated_at INTEGER
```

#### Tische & Turniere

```
tables
├── id INTEGER PK, name TEXT, kind ENUM('tournament','cashgame') default 'tournament'
├── tournament_id INTEGER NULL, seat_count INTEGER default 8
├── status ENUM('open','paused','breaking','closed') default 'open'
├── created_at INTEGER, created_by FK → users NULL, closed_at INTEGER NULL

seats  (Memory-First-Snapshot, D20)
├── id INTEGER PK, table_id FK → tables ON DELETE CASCADE
├── seat_index INTEGER, user_id FK → users ON DELETE SET NULL
├── stack INTEGER default 0, is_dealer INTEGER default 0
├── is_sb INTEGER, is_bb INTEGER, is_sit_out INTEGER, time_bank INTEGER default 90000
├── snapshot_at INTEGER NULL

seat_sessions
├── id INTEGER PK, table_id FK, user_id FK
├── sat_down_at INTEGER, stood_up_at INTEGER NULL

tournament_series
├── id INTEGER PK, title TEXT, description TEXT NULL
├── created_by FK → users, created_at INTEGER

tournament_schedules
├── id INTEGER PK, title TEXT
├── recurrence_json TEXT (zod-validiert), template_json TEXT (zod-validiert)
├── series_id FK → tournament_series NULL, is_active INTEGER default 1
├── created_by FK → users, created_at INTEGER

tournaments
├── id INTEGER PK, title TEXT
├── status ENUM('registering','running','paused','finished','cancelled')
├── table_id FK → tables NOT NULL (D22)
├── start_at INTEGER, buyin INTEGER, rake_pct INTEGER
├── speed ENUM('slow','normal','turbo','hyper') default 'normal'
├── payout_curve ENUM('steep','standard','flat') default 'standard'
├── rebuy_mode ENUM('none','1+addon') default 'none'  ← bei I18 zu ENUM('freeze-out','classic','re-entry')
├── max_players INTEGER default 8, prize_pool INTEGER default 0
├── created_by FK → users, created_at INTEGER
├── schedule_id FK → tournament_schedules NULL, series_id FK → tournament_series NULL

tournament_players
├── id INTEGER PK, tournament_id FK ON DELETE CASCADE, user_id FK ON DELETE CASCADE
├── registered_at INTEGER, seated_at INTEGER NULL
├── final_place INTEGER NULL, prize_won INTEGER default 0
├── series_points INTEGER default 0
├── rebuy_count INTEGER default 0, addon_used INTEGER default 0
├── (bei I18) re_entry_count INTEGER default 0

blind_levels
├── id INTEGER PK, tournament_id FK ON DELETE CASCADE
├── level INTEGER, sb INTEGER, bb INTEGER, ante INTEGER default 0, duration_min INTEGER
```

#### Hand-History (D6, D21)

```
hands
├── id INTEGER PK, table_id FK, tournament_id FK NULL
├── started_at INTEGER, ended_at INTEGER NULL
├── sb_amount INTEGER, bb_amount INTEGER, ante INTEGER default 0
├── dealer_seat INTEGER, board TEXT NULL (kompakter String für Replayer)
├── pot_total INTEGER NULL, rake INTEGER default 0

hand_board_cards (D21)
├── id INTEGER PK, hand_id FK ON DELETE CASCADE
├── street ENUM('flop','turn','river'), position INTEGER
├── rank INTEGER (2..14), suit TEXT (h|d|c|s)

hand_players
├── id INTEGER PK, hand_id FK, user_id FK
├── seat INTEGER, stack_start INTEGER, hole_cards TEXT NULL
├── shown_at_showdown INTEGER default 0, won_amount INTEGER default 0
├── vpip, pfr, three_bet, agg_factor_num, agg_factor_den (INTEGER default 0)

actions
├── id INTEGER PK, hand_id FK, user_id FK
├── seq INTEGER, street TEXT
├── action ENUM('fold','check','call','bet','raise','allin'), amount INTEGER default 0
├── at INTEGER
```

#### Chat, Board, Notifications

```
chat_messages
├── id INTEGER PK, room_kind ENUM('table','club'), room_ref INTEGER NULL
├── user_id FK ON DELETE CASCADE, text TEXT
├── at INTEGER, pinned INTEGER default 0, deleted_at INTEGER NULL
├── edited_at INTEGER NULL, original_text TEXT NULL

word_filters
├── id INTEGER PK, word TEXT UNIQUE, created_by FK → users, created_at INTEGER

mutes
├── id INTEGER PK, user_id FK ON DELETE CASCADE, muted_by FK → users
├── expires_at INTEGER, created_at INTEGER

chat_room_settings
├── id INTEGER PK, room_kind ENUM('table','club'), room_ref INTEGER NULL
├── slow_mode_sec INTEGER default 0, updated_at INTEGER, updated_by FK → users

inbox_items
├── id INTEGER PK, user_id FK ON DELETE CASCADE
├── kind TEXT, title TEXT, body TEXT
├── ref_type TEXT NULL, ref_id INTEGER NULL
├── read_at INTEGER NULL, created_at INTEGER, action_required INTEGER default 0

popup_log
├── id INTEGER PK, sent_by FK → users
├── target_type ENUM('all','user'), target_id INTEGER NULL
├── title TEXT, body TEXT, sent_at INTEGER

club_board
├── id INTEGER PK, text TEXT, author_id FK ON DELETE CASCADE
├── created_at INTEGER, edited_at INTEGER NULL, deleted_at INTEGER NULL
```

---

## 6 · Programmstruktur und Modul-Verantwortlichkeiten

### 6.1 Tatsächliche Verzeichnisstruktur (Stand I16)

```
backend/
├── data/                                  (gitignored)
│   ├── greenpoker.db
│   ├── avatars/
│   └── backups/
├── drizzle/                               (SQL-Migrationen 0001–0010)
├── scripts/
│   └── seed.ts                            (Demo-Tische, Test-Accounts)
├── src/
│   ├── config.ts                          (zentrale Konstanten, D17c)
│   ├── types.ts                           (AuthenticatedUser, Express-Augmentation)
│   ├── server.ts                          (Bootstrap, DI-Wurzel)
│   ├── db/
│   │   ├── client.ts                      (PRAGMA-Setup, Drizzle-Wrapper)
│   │   ├── schema.ts                      (alle Tabellen-Definitionen)
│   │   └── migrator.ts
│   ├── core/
│   │   ├── logger.ts
│   │   ├── errors.ts                      (BadRequestError, ConflictError, …)
│   │   ├── time.ts                        (Clock-Interface, MockClock für Tests)
│   │   ├── ids.ts
│   │   ├── realtime.ts                    (RealtimeBus, Socket.IO-Fassade)
│   │   ├── consistency-check.service.ts   (SA-1, SA-2)
│   │   └── polymorphic-resolver.ts        (SA-4)
│   ├── validation/
│   │   ├── prefs.schemas.ts
│   │   ├── auth.schemas.ts
│   │   ├── accounting.schemas.ts
│   │   ├── schedule.schemas.ts
│   │   └── blob-versioning.ts             (SA-3, _v-Feld)
│   ├── modules/
│   │   ├── auth/                          ✅ I1
│   │   ├── ban/                           ✅ I2
│   │   ├── accounting/                    ✅ I3
│   │   ├── user/                          ✅ I4
│   │   ├── game/                          ✅ I5, I9, I10, I11
│   │   │   ├── seat-state.memory.ts
│   │   │   ├── table.repository.ts
│   │   │   ├── game.gateway.ts
│   │   │   ├── game.routes.ts
│   │   │   ├── game.module.ts
│   │   │   ├── deck.ts
│   │   │   ├── hand.engine.ts             (pure, kein I/O)
│   │   │   ├── hand.evaluator.ts
│   │   │   ├── hand.repository.ts
│   │   │   ├── hand.service.ts
│   │   │   ├── hand-state.memory.ts
│   │   │   ├── pot.calculator.ts
│   │   │   ├── action.validator.ts
│   │   │   └── time.bank.service.ts
│   │   ├── tournament/                    ✅ I6, I7, I8, I11
│   │   │   ├── tournament.repository.ts
│   │   │   ├── tournament.routes.ts
│   │   │   ├── tournament.module.ts
│   │   │   ├── tournament.lifecycle.service.ts
│   │   │   ├── buyin.service.ts
│   │   │   ├── payout.calculator.ts
│   │   │   ├── schedule.repository.ts
│   │   │   ├── schedule.service.ts
│   │   │   ├── schedule.routes.ts
│   │   │   └── schedule.module.ts
│   │   ├── handhistory/                   ✅ I15, I16
│   │   │   ├── handhistory.repository.ts
│   │   │   ├── replayer.service.ts
│   │   │   ├── handhistory.routes.ts
│   │   │   ├── handhistory.module.ts
│   │   │   ├── stats.flagger.ts           (I16, VPIP/PFR/3-Bet/AF)
│   │   │   ├── stats.aggregator.ts
│   │   │   ├── stats.routes.ts
│   │   │   └── stats.module.ts
│   │   ├── chat/                          ✅ I12
│   │   ├── clubboard/                     ✅ I14
│   │   └── notification/                  ✅ I13
│   ├── workers/
│   │   ├── cleanup.worker.ts              (Sessions/Bans, 60 min)
│   │   ├── schedule.worker.ts             (Schedule-Window, 60 min)
│   │   └── (geplant: backup.worker.ts, I14-Restpunkt)
│   └── tests/
│       ├── helpers/app.ts                 (Test-App-Builder)
│       └── *.test.ts                      (341 Tests, alle grün)
```

### 6.2 Modul-Verantwortlichkeiten (Kurzfassung)

| Modul | Verantwortet | Wichtige Klassen |
|---|---|---|
| `auth` | Registrierung, Login, Session, Middleware | `auth.service.ts`, `auth.middleware.ts` |
| `ban` | 3-Stufen-Bans (D16), Guards (`requireNotBanned`, `createPlayBlockGuard`) | `ban.service.ts`, `ban.guards.ts` |
| `accounting` | Doppelte Buchführung (D15), Nullsummen-Pattern, Konten | `account.repository.ts`, `transaction.service.ts`, `ledger.repository.ts` |
| `user` | Profile, Anzeigename mit Cooldown (D19), Preferences (D10), Avatar-Upload | `display-name.service.ts`, `preferences.service.ts` |
| `game` | Tische, Sitze, Memory-First-State (D20), Hand-Engine, Action-Loop, Realtime | siehe 6.1 |
| `tournament` | Turnier-Lifecycle, Registration, Sitzzuteilung (D22), Schedules, Buyin/Payout, Bounty | siehe 6.1 |
| `handhistory` | Replayer mit Sichtbarkeitsfilter (D7), HUD-Stats (D17b), Series-Leaderboards | siehe 6.1 |
| `chat` | Club-Chat + Tisch-Chat, Moderation, Wortfilter, Mutes, Slow-Mode | `chat.gateway.ts`, `moderation.service.ts` |
| `clubboard` | Korkbrett (Posts mit Soft-Delete) | `clubboard.repository.ts` |
| `notification` | Inbox-Items, Popups, Realtime-Notification | `notification.service.ts` |

### 6.3 Querschnitts-Komponenten

| Komponente | Aufgabe |
|---|---|
| `RealtimeBus` (`core/realtime.ts`) | Fassade um Socket.IO; Module emittieren über Bus, kennen `io` nicht direkt. |
| `ConsistencyCheckService` (`core/consistency-check.service.ts`) | `verifyAccounts()` (SA-1), `verifyLedgerInvariants()` (SA-2), `verifyPrizePools()` (I8). Bei Bootstrap. |
| `PolymorphicResolver` (`core/polymorphic-resolver.ts`) | Auflösung `ref_type + ref_id` → konkrete Domäne (SA-4). |
| `BlobValidator` (`validation/blob-versioning.ts`) | Validiert JSON-Blobs gegen zod-Schema, prüft `_v ≤ BLOB_SCHEMA_VERSION` (SA-3). |
| `Clock` (`core/time.ts`) | Interface mit `realClock` und `MockClock` für Tests. Alle Zeitlogik geht durch Clock. |
| `Migrator` (`db/migrator.ts`) | Wendet Drizzle-Migrationen beim Server-Start an. |
| `CleanupWorker` (`workers/cleanup.worker.ts`) | Löscht abgelaufene Sessions und Bans. Setzt Status `banned → active` nach Ablauf. |
| `ScheduleWorker` (`workers/schedule.worker.ts`) | Rollender 4-Wochen-Window-Generator für `tournament_schedules`. |

---

## 7 · API-Spezifikation

Konvention: Alle Endpoints unter `/api`. Authentifizierung über Session-Cookie. Antworten als JSON. Bei Fehlern: `{ message: string, code?: string }` mit passendem HTTP-Status.

### 7.1 Implementierte Endpoints (I1–I16)

#### Auth & Session
| Method | Path | Beschreibung |
|---|---|---|
| POST | `/auth/register` | `{email, emailConfirm, displayName, password}` |
| POST | `/auth/login` | `{email, password}` → Cookie |
| POST | `/auth/logout` | Löscht aktuelle Session |
| POST | `/auth/logout-all` | Löscht alle Sessions des Users |
| GET | `/auth/me` | `{id, email, displayName, role, status, avatarPath}` |

#### User & Preferences
| Method | Path | Beschreibung |
|---|---|---|
| GET | `/users/:id/profile` | Öffentliches Profil |
| PUT | `/api/profile/display-name` | `{displayName}` — Cooldown 30 Tage |
| GET | `/api/users/me/preferences` | 4 Blobs (tableLook, layoutSlots, general, gameplay) |
| PUT | `/api/users/me/preferences` | Partial-Update |
| POST | `/api/users/me/avatar` | Multipart, max 200 KB |

#### Accounting
| Method | Path | Rolle |
|---|---|---|
| GET | `/api/accounts/me` | player |
| POST | `/api/accounts/me/transfer` | player |
| POST | `/api/accounts/me/donate` | player |
| POST | `/api/accounts/me/request-load` | player |
| GET | `/api/admin/accounts` | admin |
| POST | `/api/admin/accounts/transfer` | admin |
| POST | `/api/admin/users/:id/chips` | admin |

#### Tables & Seats
| Method | Path | Beschreibung |
|---|---|---|
| GET | `/api/tables` | Alle offenen Tische |
| GET | `/api/tables/:id` | Detail |
| POST | `/api/admin/tables` | Tisch anlegen |
| POST | `/api/admin/tables/:id/close` | Tisch schließen |

#### Tournaments
| Method | Path | Beschreibung |
|---|---|---|
| GET | `/api/tournaments` | Liste (optional `?status=registering`) |
| GET | `/api/tournaments/:id` | Detail + players + blind_levels |
| POST | `/api/tournaments` | Anlegen (alle Spieler, D3) |
| POST | `/api/tournaments/:id/register` | Anmelden |
| POST | `/api/tournaments/:id/unregister` | Abmelden (vor Start) |
| POST | `/api/tournaments/:id/start` | Start + Fisher-Yates (Creator oder Admin) |
| POST | `/api/tournaments/:id/rebuy` | Rebuy (heutige `1+addon`-Variante) |
| POST | `/api/tournaments/:id/addon` | Addon |
| POST | `/api/admin/tournaments/:id/pause` | Admin |
| POST | `/api/admin/tournaments/:id/resume` | Admin |

#### Schedules & Series (I7)
| Method | Path | Beschreibung |
|---|---|---|
| GET | `/api/tournament-schedules` | Liste |
| POST | `/api/tournament-schedules` | Wiederholung anlegen |
| DELETE | `/api/tournament-schedules/:id` | Löschen |
| GET | `/api/tournament-series` | Liste |
| POST | `/api/tournament-series` | Serie anlegen |
| GET | `/api/tournament-series/:id/leaderboard` | Series-Punkte-Rangliste |
| POST | `/api/admin/schedule-worker/run` | Manueller Worker-Trigger |

#### Bans (Admin)
| Method | Path | Beschreibung |
|---|---|---|
| GET | `/api/admin/bans` | Aktive Bans |
| POST | `/api/admin/bans` | Ban setzen |
| DELETE | `/api/admin/bans/:id` | Aufheben |
| POST | `/api/admin/users/:id/kick` | Sessions löschen |
| POST | `/api/admin/users/:id/approve` | pending → active |

#### Hand-History & Stats (I15, I16)
| Method | Path | Beschreibung |
|---|---|---|
| GET | `/api/hands/:id` | Hand-Detail mit D7-Sichtbarkeitsfilter |
| GET | `/api/hands?table=&from=&to=` | Hände-Liste (nur Hände eigener Anwesenheit) |
| GET | `/api/stats/me` | VPIP/PFR/3-Bet/AF aggregiert |
| GET | `/api/stats/users/:id` | Stats anderer User (öffentlich) |

#### Chat & ClubBoard (I12, I14)
| Method | Path | Beschreibung |
|---|---|---|
| GET | `/api/chat/club` | Club-Chat-Verlauf |
| DELETE | `/api/chat/messages/:id` | Eigene/Admin |
| PATCH | `/api/chat/messages/:id` | Eigene |
| POST | `/api/admin/chat/word-filters` | Admin |
| POST | `/api/admin/chat/mutes` | Admin |
| PATCH | `/api/admin/chat/*/settings` | Slow-Mode |
| POST | `/api/admin/chat/*/pinned-message` | Pin |
| GET | `/api/clubboard` | Alle Posts (newest first) |
| POST | `/api/clubboard` | Post anlegen |
| PATCH | `/api/clubboard/:id` | Eigenen Post bearbeiten |
| DELETE | `/api/clubboard/:id` | Soft-Delete (Eigentümer/Admin) |

#### Notifications & Inbox (I13)
| Method | Path | Beschreibung |
|---|---|---|
| GET | `/api/inbox` | Eigener Posteingang (unread zuerst) |
| PATCH | `/api/inbox/:id/read` | Als gelesen markieren |
| POST | `/api/admin/popup` | Popup an alle oder einen User |

### 7.2 Geplante Endpoints (I17–I19, Phase B, Phase D)

| Method | Path | Iter | Zweck |
|---|---|---|---|
| (interne Events) | Balancer-Logik | I17 | `seat.assigned`-Event bei Tischbruch |
| POST | `/api/tournaments/:id/re-entry` | I18 | Re-Entry nach Bust-Out (modusabhängig) |
| GET | `/api/tournaments/:id/lobby-state` | I19 | Erweiterter Lobby-State: Spielerliste mit Stack-Live, Payout-Vorschau |
| GET | `/api/tournaments/:id/payout-structure` | I19 | Schematische + konkrete Auszahlungsstruktur (basierend auf aktuellem Prize Pool) |
| (Phase D, Spezifikation aus) | Cashgame-Endpoints | Phase D | Buy-In, Stand-Up, Top-Up, Rake-Erfassung |

### 7.3 Socket.IO-Events

| Event (Client → Server) | Payload | Seit |
|---|---|---|
| `table.join` | `{tableId, seatIndex?}` — `seatIndex` nur für Cashgame-Tische (D22) | I5 |
| `table.leave` | `{tableId}` | I5 |
| `action` | `{handId, type, amount?}` | I10 |
| `chat.send` | `{roomKind, roomRef?, text}` | I12 |

| Event (Server → Client) | Room | Seit |
|---|---|---|
| `state-update` | `table:<id>` | I5 |
| `balance:update` | `user:<id>` | I3 |
| `chat.new` | `table:<id>` / `club:global` | I12 |
| `notification` | `user:<id>` | I13 |
| `tournament.update` | `tournament:<id>` | I11 |
| (geplant) `lobby.update` | `tournament:<id>` | I19 |
| (geplant) `seat.assigned` | `tournament:<id>` | I17 |

---

## 8 · Querschnittsthemen

### 8.1 Authentifizierung und Session

Siehe 1.4. Middleware in `modules/auth/auth.middleware.ts`. Prepared Statement für Session-Lookup einmalig beim Server-Start gebaut. `req.user` enthält `{ id, role, status, displayName, email }`.

### 8.2 Eingangsvalidierung

Alle eingehenden Bodies werden über zod-Schemas in `validation/` validiert. JSON-Blobs (Preferences, Schedule-Templates) tragen `_v` und werden gegen `BLOB_SCHEMA_VERSION` geprüft. BlobMigrationWorker beim Bootstrap migriert auf aktuelle Version (SA-3).

### 8.3 Logging

`core/logger.ts` — strukturiertes JSON-Logging. Alle Module loggen über das gemeinsame Interface. Wichtige Ereignisse: Login, Logout, Ban, Buyin, Hand-Start, Hand-Ende, Rebuy.

### 8.4 Backup

Siehe 1.3. Implementierung steht aus (offen aus I14, siehe Anhang E I14 Restpunkt).

### 8.5 Cleanup

`cleanup.worker.ts` läuft alle `CLEANUP_WORKER_INTERVAL_MIN = 60` Minuten:
- Löscht Sessions mit `expires_at < now`.
- Setzt User `status='active'` zurück, wenn Vollsperre abgelaufen.
- Löscht abgelaufene Mutes.

### 8.6 Clock

`core/time.ts` exportiert ein `Clock`-Interface mit `now(): number`. Produktion nutzt `realClock`. Tests injizieren `MockClock`, der manuell vorgespult werden kann. Alle Zeitlogik (Cooldown, Session-Ablauf, Late-Reg) geht durch Clock — keine direkten `Date.now()`-Aufrufe in Modulen.

### 8.7 Realtime-Bus

`core/realtime.ts` ist die einzige Stelle, an der Module mit Socket.IO interagieren. Module rufen `bus.emit(room, event, payload)`. Bus kapselt `io.to(room).emit(event, payload)`. Ban-Check beim Verbindungsaufbau.

### 8.8 Konsistenz-Checks (ConsistencyCheckService)

Beim Bootstrap:
- `verifyAccounts()` — repariert inkonsistente `balance_cache`-Werte aus `ledger_entries`-Summe (SA-1).
- `verifyLedgerInvariants()` — prüft Nullsummen-Pattern; protokolliert Verstöße (SA-2).
- `verifyPrizePools()` — vergleicht `tournaments.prize_pool` mit Summe der Buyin-Buchungen (geplant in I8, derzeit implementiert).

### 8.9 Polymorphic Resolver

`core/polymorphic-resolver.ts` löst `ref_type + ref_id` in Domänen-Objekt auf. Verwendet von `transactions` (`ref_type ∈ {buyin, rake, …}`), `inbox_items` (`ref_type`). Mitigiert SA-4.

---

## 9 · Schema-Analyse — Risiken SA-1 bis SA-7

Die folgende Tabelle ist die in v5 nur referenzierte Schema-Analyse, hier ausformuliert. Die Risiken wurden im Rahmen der initialen Schema-Designphase identifiziert.

### 9.1 SA-1 — Cache-Konsistenz von `balance_cache` und `prize_pool`

**Befund:** `accounts.balance_cache` und `tournaments.prize_pool` sind denormalisierte Aggregate, die über Ledger-Einträge bzw. Buyin-Buchungen redundant zur Wahrheit (Summe der Einträge) gehalten werden. Datenbankseitig ist die Konsistenz nicht erzwungen.

**Konsequenz:** Bei einem fehlerhaften Schreibpfad, einem unvollständigen Rollback oder einem unsynchronen Worker kann die Cache-Spalte vom tatsächlichen Saldo abweichen, ohne dass eine Datenbank-Constraint Alarm schlägt.

**Behandlung (I3, abgeschlossen):**
- Alle Schreibwege gehen durch `TransactionService.commit()` (Nullsummen-Pattern in einer Drizzle-Transaktion).
- `ConsistencyCheckService.verifyAccounts()` läuft beim Bootstrap; korrigiert `balance_cache` aus Ledger-Summe und loggt Abweichungen.
- Optionaler Periodic-Check alle `CONSISTENCY_CHECK_INTERVAL_HOURS = 24` Stunden geplant.

### 9.2 SA-2 — Nullsummen-Invariante nur per Test

**Befund:** Die Buchungsregel „Summe aller `ledger_entries.amount` pro `transaction_id` = 0" ist eine Anwendungsinvariante und kein DB-Constraint. Eine fehlerhafte Routine könnte einseitige Einträge schreiben.

**Konsequenz:** Schief gewordene Buchungen würden erst beim nächsten Konsistenz-Check oder bei einer manuellen Saldo-Prüfung auffallen.

**Behandlung (I3, abgeschlossen):**
- `TransactionService.commit()` ist die einzige Eintrittsstelle für Ledger-Schreibungen. Sie validiert Nullsumme in derselben Transaktion und rollt bei Verstoß zurück.
- `ConsistencyCheckService.verifyLedgerInvariants()` beim Bootstrap.
- CHECK-Constraint oder Trigger als Optionsverfeinerung in Phase E.

### 9.3 SA-3 — JSON-Blobs in Preferences und Schedule-Templates

**Befund:** Vier Preferences-Blobs (`table_look_json`, `layout_slots_json`, `general_json`, `gameplay_json`) und `tournament_schedules.template_json`, `tournament_schedules.recurrence_json` entziehen sich dem DB-Schema. Schema-Drift, fehlende Migration und unvalidierter Inhalt sind Risiken.

**Konsequenz:** Frontend könnte Werte schreiben, die das Backend nicht versteht; Schema-Erweiterungen brauchen einen separaten Migrations-Mechanismus außerhalb von SQL.

**Behandlung (I4, abgeschlossen):**
- Jeder Blob trägt ein `_v` Versionsfeld.
- `BlobValidator` (`validation/blob-versioning.ts`) prüft beim Schreiben gegen zod-Schema.
- `BlobMigrationWorker` läuft beim Server-Bootstrap und migriert alte `_v`-Versionen auf `BLOB_SCHEMA_VERSION = 2`.

### 9.4 SA-4 — Polymorphe Referenzen ohne FK-Constraint

**Befund:** `transactions.ref_type + ref_id`, `inbox_items.ref_type + ref_id` und `accounts.owner_type + owner_id` sind polymorphe Referenzen. SQLite kann sie nicht per FK absichern, weil der referenzierte Tabellen-Name dynamisch ist.

**Konsequenz:** Hängende oder falsche Referenzen sind möglich; Abfragen müssen den Typ mit auswerten.

**Behandlung (I3 teilweise, Chat/Notification I12/I13 ergänzt):**
- `PolymorphicResolver` (`core/polymorphic-resolver.ts`) ist die einzige Stelle, die Polymorphismen auflöst.
- CHECK-Constraint in der Migration für `accounts`: `(owner_type='user' AND owner_id IS NOT NULL) OR (owner_type='club' AND owner_id IS NULL)`.
- Bei `transactions` wird `ref_id` defensiv geprüft, bevor referenziert.

### 9.5 SA-5 — Write-Amplifikation durch Game-State in `seats`

**Befund:** Würde der Spielzustand (Stack, isDealer, isSB, isBB, time_bank) bei jeder Action in `seats` persistiert, ergäbe sich eine Write-Last in der Größenordnung „Spieler × Aktionen × Tische". Bei mehreren Tischen ein potenzieller SQLite-Bottleneck.

**Konsequenz:** Eine DB-getriebene Live-Verwaltung des Spielzustands würde die Schreib-Performance unnötig auslasten.

**Behandlung (I5, abgeschlossen — D20):**
- Memory-First-Strategie: `SeatStateMemory` hält den Live-Zustand in einer Map.
- DB-Snapshot nur bei (a) Hand-Ende, (b) Sit-Down/Stand-Up, (c) Tournament-Start, (d) Rebuy/Re-Entry.
- Bootstrap-Recovery lädt den letzten Snapshot aus `seats` in den Memory.

### 9.6 SA-6 — Fehlende Moderator-Rolle

**Befund:** `users.role` kennt nur `admin` und `player`. Eine intermediäre Moderator-Rolle (Chat-Moderation, Tisch-Pause, ohne Vollzugriff auf Buchungen) ist nicht vorgesehen.

**Konsequenz:** Solange der Club klein ist, ist das vertretbar. Bei Wachstum müsste eine Rollen-Erweiterung gemacht werden.

**Behandlung:** Backlog für Phase E. Rollen-Erweiterung als eigene Migration plus Permission-Map in `core/permissions.ts`.

### 9.7 SA-7 — Board als String in `hands.board`

**Befund:** `hands.board` als kompakter String (z. B. `"7h8s9dTcJh"`) ist gut für den Replayer, aber blockiert analytische Queries auf Karten-Ebene.

**Konsequenz:** Boards nach Texturen (Flush-Boards, Paired Boards, Connected Boards) auszuwerten wäre ohne zusätzliche Tabelle aufwändig.

**Behandlung (I9, abgeschlossen — D21):**
- Hybrid-Modell: `hands.board` als String bleibt für Replayer; zusätzlich Tabelle `hand_board_cards` mit pro-Karten-Zeilen (`street`, `position`, `rank`, `suit`).
- Beide in derselben Transaktion geschrieben.
- Index `(rank, suit)` für Karten-Analyse, Index `(hand_id, street, position)` für rekonstruierende Joins.

---

## 10 · Risiken R1 bis R11

Domänen- und Implementierungsrisiken, ergänzend zur Schema-Analyse.

| ID | Risiko | Status nach I16 |
|---|---|---|
| R1 | Tournament-Engine groß, Kontext-Sprengung | Mitigiert — I6–I8 + I11 sauber zerlegt; nur Balancer (I17) bleibt offen. |
| R2 | Hand-Engine Sonderfälle (All-In, Side-Pots, Split, Walk) | Mitigiert — I9 Unit-Test-Abdeckung, I11 End-to-End-Showdown verifiziert. |
| R3 | HTML-Anbindung organisch gewachsen | **Wächst weiter mit I17–I19.** Wird mit Phase B (React-Port) endgültig adressiert. |
| R4 | Replayer-Sichtbarkeitsfilter subtil | Mitigiert — I15 mit D7-Filter implementiert, getestet. |
| R5 | In-Process-Scheduler Neustart-Verhalten | Mitigiert — ScheduleWorker idempotent (`hasInstance`-Guard), läuft beim Bootstrap. |
| R6 | Multi-Table-Balancing vertagt | Offen — I17. |
| R7 | `better-sqlite3` synchron auf Eventloop | Beobachtet — keine Auffälligkeiten in I10/I11. Bei Cashgame (Phase D) erneut prüfen. |
| R8 | Frontend-Mock-Inkonsistenzen | Reduziert — fast alle Mock-Stellen ersetzt; Restpunkte siehe Anhang D. |
| R9 | Memory ↔ DB-Drift bei Crash | Mitigiert — Bootstrap-Recovery; Hand-in-flight wird bei Crash verworfen (Verlust akzeptiert). |
| R10 | Anzeigename-Wechsel während laufender Hand | Mitigiert — `hand_players.user_id` referenziert; Anzeigename wird zur Laufzeit aus `users` aufgelöst. |
| R11 | E-Mail-Tippfehler bei Registrierung | Teil-Mitigation — Doppelt-Eingabe-Validierung im Formular (D18); Admin-Korrektur-Backoffice bleibt Backlog. |

**Neue Risiken in Phase B/D:**

| ID | Risiko | Status |
|---|---|---|
| R12 | React-Stack-Wahl: ungeeignete Library zementiert Probleme | Mitigation: separate Brainstorming-Session vor Phase B (Stack-Festlegung). |
| R13 | Cashgame-Anforderungen unklar (Top-Up, Stand-Up-Politik, Limit-Modi) | Mitigation: separate Brainstorming-Session vor Phase D. |
| R14 | Lobby-Strukturarbeit verändert API → React-Bindings müssen nach | Mitigation: Lobby-Umbau (I19) erfolgt **vor** Phase B Port-Phase 1, damit der Port auf stabiler API arbeitet. |

---

## 11 · Test-Strategie

### 11.1 Pyramide (unverändert)

```
Integration-Tests (HTTP, gegen :memory:-DB)
────────────────────
Modul-Tests (DB, ohne Edge-Layer)
───────────────────────────
Unit-Tests (Engines, Calculator, Validator)
──────────────────────────────────────
```

### 11.2 Status nach I16

| Bereich | Tests | Status |
|---|---|---|
| auth | 18 | ✅ |
| ban | 13 | ✅ |
| accounting | 17 | ✅ |
| user / preferences / display-name | 19 | ✅ |
| game / tables / seat-state | 19 | ✅ |
| tournament | 15 | ✅ |
| schedule | 18 | ✅ |
| buyin / payout | (Teil tournament) | ✅ |
| tournament-lifecycle | inkl. | ✅ |
| game / hand-engine, evaluator, pot, action-validator | (in game) | ✅ |
| chat / moderation | 30 | ✅ |
| notification / inbox | 17 | ✅ |
| clubboard | 16 | ✅ |
| handhistory / replayer | 15 | ✅ |
| stats / aggregator / flagger | (in handhistory) | ✅ |
| **Gesamt** | **341** | **✅ grün** |

### 11.3 Test-Konventionen

- `:memory:`-SQLite-DB pro Test-Datei; vollständige Migration bei Setup.
- `MockClock` für alle Zeit-Abhängigkeiten.
- HTTP-Tests via supertest gegen `app.ts`-Builder.
- Keine echten Sockets in Unit-Tests; RealtimeBus mit `vi.fn()`-Mock.
- Seed-Daten ausschließlich pro Test-Suite, kein globaler State.

### 11.4 Geplante Test-Schwerpunkte

| Iteration | Test-Fokus |
|---|---|
| I17 | Balancer (Tisch-Bruch, Final-Table-Merge, MULTI_TABLE_LIMIT-Check) |
| I18 | Rebuy classic (Multi-Rebuy, Addon-Window), Re-Entry (Bust-Out-Erkennung, neue Sitzzuteilung), Wizard-Validierung |
| I19 | Lobby-State (Stack-Sortierung live, Payout-Berechnung bei verschiedenen `payout_curve`-Werten) |
| Phase B | React-Testing-Library für Komponenten, Playwright für E2E |
| Phase D | Cashgame-Lifecycle (Sit-Down/Stand-Up, Auto-Top-Up, Rake-Erfassung) |

---

## 12 · Roadmap mit Iterationsschritten

### 12.1 Prinzipien (D14)

- Eine Iteration = eine Claude-Code-Session.
- Jede Iteration endet **lauffähig**: Server startet, Tests grün, das adressierte Feature ist im Browser klickbar.
- Schema-Änderungen und Spiellogik niemals in derselben Session mischen.
- Wenn eine Iteration zu groß wird: Splitten. Der Plan bewahrt die **Reihenfolge** und die **Übergabezustände**.

### 12.2 Iterations-Übersicht (gemäß D24)

```
Phase C — Backend Restpunkte
   I17 — Multi-Table-Balancer
   I18 — Rebuy-Vollausbau (D23)
   I19 — Turnier-Lobby-Anzeigeumfang (D25)
       ↓
Phase B — React-Port (Iterations-Detail nach Stack-Entscheid)
   B0 — Stack-Festlegung (separate Brainstorming-Session)
   B1 — Plattform-Setup
   B2.x — Domänen-Migration in mehreren Iterationen
       ↓
Phase B/D verschränkt
   D0 — Cashgame-Anforderungs-Spezifikation (separate Brainstorming-Session)
   D-Iterationen (Schema → Lifecycle → Cashier → React-Komponenten)
       ↓
Phase E — Moderator-Rolle, E-Mail, Postgres-Option
```

### 12.3 I17 — Multi-Table-Balancer

**Eingangsstand:** I16. Phase C bis Single-Table funktional vollständig.

**Lieferumfang:**
- `modules/tournament/balancer.service.ts` — Algorithmus zur Tisch-Auflösung und Sitz-Umverteilung bei ungleicher Tisch-Belegung.
- Erweiterung `tournament.lifecycle.service.ts` um Balancer-Aufruf nach jedem Bust-Out.
- Konstante `MAX_PLAYERS_PER_TABLE` in `config.ts` (Default 8, im Wizard überschreibbar pro Turnier).
- Schema: `tournaments.max_players_per_table INTEGER default 8` (Migration 0010b, klein).
- Socket-Event `seat.assigned` für Spieler, deren Tisch gewechselt wird (in `tournament:<id>`-Room).
- HTML-Anbindung: Toast „Du wurdest an Tisch X umgesetzt", automatischer Wechsel des Tisch-Fensters auf neuen Tisch.

**Verifikation:**
- 10 Spieler in einem Multi-Table-Turnier (2 Tische × 5 Spieler).
- Spieler bustet out → Balancer prüft Differenz → bei `> 1` wird ausgeglichen.
- Bei `count = 8` letzter Tisch → Final-Table-Merge.
- Tests: `balancer.test.ts` mit ≥ 8 Fällen (gleichmäßig, schief, Merge, Single-Table-No-Op).

**Übergabezustand:** Multi-Table-Tournaments funktionieren End-to-End.

### 12.4 I18 — Rebuy-Vollausbau (D23)

**Eingangsstand:** I17.

**Schema-Änderung (Migration 0011):**

```
tournaments
├── rebuy_mode ENUM('freeze-out','classic','re-entry') default 'freeze-out'
├── rebuy_phase_min INTEGER default 60               -- nur classic
├── rebuy_cost INTEGER NULL                          -- classic/re-entry; default = buyin
├── addon_cost INTEGER NULL                          -- nur classic; NULL = kein Addon
├── addon_chips INTEGER NULL                         -- nur classic; Chips beim Addon
├── rebuy_chips INTEGER NULL                         -- nur classic; Chips beim Rebuy (default = Startstack)
├── re_entry_window_min INTEGER default 60           -- nur re-entry; Fenster ab Start
├── rebuy_phase_end_at INTEGER NULL                  -- berechnet bei Tournament-Start

tournament_players
├── re_entry_count INTEGER default 0
```

**Vorhandene Spalten:** `rebuy_count`, `addon_used` existieren bereits (Stand I8).

**Lieferumfang:**

1. **Lifecycle-Erweiterung** (`tournament.lifecycle.service.ts`):
   - Beim `start`: berechne `rebuy_phase_end_at` (= `start_at + rebuy_phase_min * 60_000`) bzw. `re_entry_window_end_at`.
   - Worker oder Timer triggert „Rebuy-Phase Ende" → Broadcast `tournament.rebuy-phase-ended` → ggf. Addon-Fenster öffnen.
   - Re-Entry: bei Bust-Out im Fenster werden Sitz und Stack neu zugewiesen (Fisher-Yates auf freie Sitze).

2. **`buyin.service.ts` erweitern:**
   - `bookRebuy(tournamentId, userId)`: Modus-abhängig:
     - `classic`: erlaubt während Phase, Stack ≤ Startstack, beliebig oft (Default; per Konfig limitierbar).
     - `re-entry`: erlaubt nur nach Bust-Out im Fenster.
     - `freeze-out`: 409 Conflict.
   - `bookAddon(tournamentId, userId)`: nur `classic`, in Addon-Fenster (Ende Rebuy-Phase ± definierte Pause).
   - Alle Buchungen: User-Konto → Prize Pool + Rake (analog Initial-Buyin).

3. **API:**
   - `POST /api/tournaments/:id/rebuy` (Variante anhand `rebuy_mode`).
   - `POST /api/tournaments/:id/re-entry` (neu, nur für `re-entry`-Modus).
   - `POST /api/tournaments/:id/addon` (nur `classic`).
   - GET `/api/tournaments/:id` liefert Modus-Info und aktuelle Fenster-Zustände (`rebuy_phase_active`, `addon_phase_active`).

4. **Wizard (HTML):**
   - Dropdown „Modus" mit drei Optionen.
   - Pro Modus konditional sichtbare Felder (Phase-Min, Kosten, Chips).
   - Validierung: Rebuy-Kosten ≤ Buyin × 5, Addon-Chips > 0, etc.

5. **Turnier-Lobby (HTML, Vorgriff auf I19):**
   - Anzeige „Rebuy-Phase aktiv (bis HH:MM)".
   - Rebuy-Button für eigene Spieler, deren Stack ≤ Startstack (classic).
   - Re-Entry-Button nach Bust-Out im Fenster.

**Verifikation:**
- Classic: 3-Spieler-Turnier mit Rebuy-Phase 5 Min., zwei Spieler kaufen mehrfach nach, einer holt Addon. Prize Pool stimmt mit Buyin + Rebuys + Addons. Stack-Werte korrekt.
- Re-Entry: Spieler bustet out → klickt Re-Entry → erhält neuen Sitz mit vollem Startstack. Außerhalb des Fensters: Re-Entry verweigert.
- Freeze-Out: Rebuy-Endpoint gibt 409 zurück.
- Tests: `rebuy.test.ts` mit ≥ 12 Fällen.

**Übergabezustand:** Alle drei Rebuy-Modi funktional; Wizard zeigt korrekte Feld-Sichtbarkeit; Lobby zeigt Rebuy/Re-Entry-Buttons modus- und zustandsabhängig.

### 12.5 I19 — Turnier-Lobby-Anzeigeumfang (D25)

**Eingangsstand:** I18.

**Lieferumfang:**

1. **API-Erweiterung:**
   - `GET /api/tournaments/:id/lobby-state` — liefert: Turnier-Stamm, angemeldete Spieler mit Live-Stacks (aus `SeatStateMemory`, fallback `seats`-Snapshot), Auszahlungsvorschau (schematisch + konkret).
   - `GET /api/tournaments/:id/payout-structure` — Payout-Curve aus `tournament.payout_curve`, berechnet die Verteilung über aktive Spielerzahl. Liefert:
     - `scheme: { place: 1, share_pct: 50.0 }, …`
     - `concrete: { place: 1, chips: 5000 }, …` (basierend auf aktuellem `prize_pool`).
   - Real-time Update: Socket-Event `lobby.update` im `tournament:<id>`-Room bei: Anmeldung, Abmeldung, Stack-Änderung (gedrosselt), Status-Übergang, Rebuy.

2. **HTML-Anpassung (`HTML/greenPoker_v45.x.html`, Lobby-Tab „Turniere", Detail-Ansicht):**
   - Spielerliste-Sektion: Tabelle mit `displayName`, Status (registriert/sitzend/ausgeschieden), Stack (bei `running`).
   - Sortier-Logik:
     - `registering`: alphabetisch nach Anmelde-Zeit.
     - `running`: nach Stack absteigend.
     - `finished`: nach `final_place` aufsteigend.
   - Auszahlungs-Sektion: zweispaltig „Anteil %" und „Chips bei aktuellem Pool".
   - Standard-Buttons: Anmelden, Abmelden, Starten (Creator/Admin), Rebuy / Re-Entry / Addon (modusabhängig, aus I18).
   - Socket-Handler für `lobby.update` aktualisiert Liste live.

3. **Payout-Curve-Logik** (`payout.calculator.ts` ggf. erweitern):
   - Funktion `previewPayoutStructure(playerCount, prizePool, curve): {place, sharePct, chips}[]`.
   - Wird sowohl vor Start (Spielerzahl = registriert) als auch während (Spielerzahl = noch aktiv) verwendet.

**Verifikation:**
- Im `registering`-Status: Spieler-Anmeldung → Liste aktualisiert via Socket innerhalb < 100 ms.
- Im `running`-Status: Stack-Änderung nach Hand-Ende → Sortierung aktualisiert.
- Auszahlung: bei Curve `standard` und 8 Spielern Anteil-Tabelle korrekt; konkrete Chips = prize_pool × share_pct.
- Tests: `lobby-state.test.ts`, Erweiterung `payout.test.ts`.

**Übergabezustand:** Phase C funktional vollständig auch unter D25. HTML-Plattform damit final aufgerüstet für den nachfolgenden React-Port.

### 12.6 Phase B — React-Port (Iterations-Detail nach Stack-Entscheid)

**Vorbedingung:** Brainstorming-Session zur Stack-Festlegung (Build-Tool, Router, State-Management, Styling, Test-Setup). Bewusst nicht in v6 vorweggenommen.

**Skizze der erwarteten Phasen:**

| Iter | Inhalt | Voraussetzung |
|---|---|---|
| B0 | Stack-Festlegung als eigene Brainstorming-Session — Entscheidung wird als D26 dokumentiert. | I19 abgeschlossen |
| B1 | Plattform-Setup: Repo-Struktur (`frontend/`), Build, Linting, TypeScript strict, Test-Stack, Storybook (optional). | B0 |
| B2.1 | Auth + Profil + Cashier-Domäne (Login, Registrierung, Profil-Tab, Anzeigename-Cooldown, Avatar, Cashier-Verlauf, Transfer, Donate, Request-Load). | B1 |
| B2.2 | Lobby + Tisch-Fenster + Wizard. Inkl. der in I19 etablierten Spielerliste und Auszahlungsstruktur. | B2.1 |
| B2.3 | Replayer + HUD + Admin-Backoffice. | B2.2 |
| B3 | Konzeptionelle UI-Strukturarbeiten in React (Lobby-Refinement nach Praxis-Feedback, Settings-Refactoring). | B2.3 |
| B4 | E2E-Tests (Playwright), Performance-Baseline (Lighthouse), Bundle-Größe dokumentiert. | B3 |

**Anforderungen an den Stack (auch ohne konkrete Produkt-Wahl):**
- Strict-TypeScript mit `noUncheckedIndexedAccess`.
- Komponententest-Library mit JSDOM oder Browser-Modus.
- Routing-Library (kein Hash-Routing) mit Lazy-Loading-Fähigkeit.
- State-Management ohne Globalzustand-Anti-Patterns (kein Singleton-Store mit unkontrolliertem Schreibzugriff).
- Styling: Komponentenscope (CSS-Module, Tailwind oder vergleichbar). Keine globalen Stylesheet-Mutationen.
- Build-Tool mit Hot-Reload-Geschwindigkeit < 1 s für Kleinänderungen.
- Bundle-Größe-Ziel: < 500 KB gzipped für initialen Lobby-Lade-Pfad.

**Definition of Done für B2.3 (Port abgeschlossen):**
- Funktionsparität gegenüber HTML-Stand.
- Storybook-Beispiele für zentrale Bausteine (Tisch, Sitz, Wizard, Lobby-Karte, Chat-Nachricht).
- Mindestens ein E2E-Test pro Hauptdomäne.
- Bundle-Größe und Lighthouse-Score dokumentiert.

### 12.7 Phase D — Cashgame-Engine (Skizze)

**Vorbedingung:** Brainstorming-Session zur Anforderungs-Spezifikation. Bewusst nicht in v6 vorweggenommen.

**Offene Fragen** (für die Brainstorming-Session):
- Limit-Variante: NL Hold'em allein, oder PL/FL als Modi?
- Buy-In-Range pro Tisch (Min/Max BB).
- Auto-Top-Up: User-Preference oder pro Tisch konfigurierbar?
- Stand-Up-Politik: jederzeit, oder nur zwischen Händen?
- Rake-Modell: pro Pot ein fixer Prozentsatz, mit Cap?
- Mehrtisch-Spielen: identisch zu Tournament-MULTI_TABLE_LIMIT?
- Hand-History: separate Tabelle oder gemeinsam mit Tournament-Hands?

**Erwartete Iterations-Skizze:**

| Iter | Inhalt | Stack |
|---|---|---|
| D0 | Anforderungs-Spezifikation (Brainstorming) | — |
| D1 | Schema: Cashgame-Erweiterung in `tables` (`stakes`, `min_buyin`, `max_buyin`, `rake_pct`, `rake_cap`); ggf. `cashgame_sessions`-Tabelle für Sitz-Buy-In-Historie. | Backend |
| D2 | Lifecycle: Sit-Down mit Buy-In-Buchung, Stand-Up mit Auszahlung, Auto-Top-Up-Logik. | Backend |
| D3 | Hand-Engine-Anpassung: Rake-Erfassung pro Pot, kein Tournament-Lifecycle. | Backend |
| D4 | React-Cashgame-Tisch-Komponente (Buy-In-Dialog, Stand-Up-Button, Top-Up-Toggle). | React |
| D5 | Lobby-Integration: Cashgame-Liste mit Stakes, Sitz-Auslastung, Average-Pot. | React + Backend |

### 12.8 Phase E — Moderator-Rolle, E-Mail, Postgres-Option (Backlog)

- Moderator-Rolle (SA-6): neue `users.role`-Option `moderator`, Permission-Map.
- E-Mail-Versand: SMTP-Anbindung für Registrierungsbestätigung, Passwort-Reset.
- Postgres-Migration: nur falls Multi-Node-Bedarf entsteht. Reversibilitätspfad bleibt offen.
- Admin-Korrektur-Backoffice für E-Mail-Tippfehler (R11).

---

## Anhang A — Glossar

| Begriff | Bedeutung |
|---|---|
| Anzeigename (`display_name`) | Pflichtfeld, eindeutig (COLLATE NOCASE), 30-Tage-Cooldown. Überall statt `username`. |
| E-Mail | Lowercase normalisiert, UNIQUE. Login-Identifier. Nicht öffentlich. |
| Snapshot (`seats`) | DB-Persistenz des Seat-Memory-States bei definierten Sync-Punkten (D20). |
| `_v` (Blob-Version) | Versionsfeld in JSON-Blobs (SA-3). |
| Fisher-Yates | Zufälliger Shuffle-Algorithmus für Sitzzuteilung (D22) und Deck. |
| VPIP | Voluntary Put In Pot. BB-Post zählt nicht (D17b). |
| PFR | Pre-Flop Raise. Erste freiwillige Raise (D17b). |
| 3-Bet | Erste Re-Raise preflop (D17b). |
| AF | Aggression Factor = (Bet+Raise)/(Call) (D17b). |
| WAL | Write-Ahead-Log-Modus von SQLite. Erlaubt concurrent reads neben einem writer. |
| Showdown | Karten-Vergleich nach letzter Betting-Runde. |
| Side-Pot | Zusätzlicher Pot bei All-In mit unterschiedlichen Stack-Höhen. |
| Sit-Out | Spieler bleibt am Tisch ohne Karten. |
| Walk | Alle folden zum BB. |
| Time-Bank | Pro Spieler kumulierter Zeitpuffer für längere Bedenkpausen (D8, D17c). |
| Late-Reg | Late-Registration-Fenster ab Tournament-Start, in dem Anmeldung noch möglich (D17c). |
| Memory-First | Strategie: Live-Spielzustand im Anwendungs-Memory, DB nur Snapshot (D20). |
| Bust-Out | Spieler verliert gesamten Stack und scheidet aus Turnier aus. |
| Re-Entry | Wiedereinstieg nach Bust-Out im definierten Fenster mit neuem Buy-In (D23). |
| Classic Rebuy | Mehrfacher Stack-Auffüll-Kauf während Rebuy-Phase (D23). |
| Addon | Einmaliger optionaler Stack-Zukauf am Ende der Rebuy-Phase (D23). |
| Freeze-Out | Turnier ohne Rebuy/Addon — Single-Buyin (D23). |

---

## Anhang B — Datenbank-Indexes

Vollständige Index-Liste (Stand I16):

```sql
INDEX bans(user_id, expires_at)
INDEX ledger_entries(account_id, ts DESC)
INDEX seats(table_id)
INDEX seat_sessions(table_id, user_id, sat_down_at)
INDEX tables(status)
INDEX tournaments(status)
INDEX tournaments(schedule_id)
INDEX tournaments(series_id)
INDEX tournament_players(tournament_id)
INDEX tournament_players(user_id)
INDEX tournament_schedules(is_active)
INDEX blind_levels(tournament_id, level)
INDEX hands(table_id, started_at DESC)
INDEX hand_players(user_id)
INDEX hand_players(hand_id)
INDEX actions(hand_id, seq)
INDEX hand_board_cards(rank, suit)
INDEX hand_board_cards(hand_id, street, position)
INDEX chat_messages(room_kind, room_ref, at DESC)
INDEX inbox_items(user_id, read_at)
INDEX club_board(deleted_at, created_at DESC)
```

**Geplant (I18):** `tournaments(rebuy_mode)`, falls Lobby-Filter nach Modus eingeführt.

---

## Anhang C — Konfigurations-Konstanten

Quelle: `backend/src/config.ts`. Stand I16; ergänzt für I17/I18.

```ts
export const CONFIG = {
  // Gameplay (D17c)
  TIME_PER_ACTION_DEFAULT_SEC:            30,
  TIME_BANK_INITIAL_MS:               90_000,
  TIME_BANK_REFILL_PER_HAND_MS:        5_000,
  TIME_BANK_MAX_MS:                   90_000,
  LATE_REG_DEFAULT_MIN:                   30,
  PAUSE_PATTERN_DEFAULT:            'none' as const,
  MULTI_TABLE_LIMIT:                       4,
  SHUTDOWN_GRACE_MS:                   5_000,
  // Infrastruktur
  BACKUP_INTERVAL_HOURS:                  24,
  BACKUP_KEEP_DAYS:                        7,
  SESSION_LIFETIME_DAYS:                  30,
  SCHEDULE_WINDOW_DAYS:                   28,
  SCHEDULE_WORKER_INTERVAL_MIN:           60,
  CLEANUP_WORKER_INTERVAL_MIN:            60,
  // User / Anzeigename (D19)
  DISPLAY_NAME_COOLDOWN_DAYS:             30,
  DISPLAY_NAME_MIN_LEN:                    3,
  DISPLAY_NAME_MAX_LEN:                   24,
  DISPLAY_NAME_REGEX: /^[A-Za-z0-9_\-äöüÄÖÜß ]+$/,
  // Upload
  AVATAR_MAX_BYTES:                  200_000,
  // Tisch / Sitze (I5)
  DEFAULT_SEAT_COUNT:                      8,
  DEFAULT_START_STACK:                10_000,
  // Turniere (I6 — D22, I17 — Multi-Table)
  MIN_TOURNAMENT_PLAYERS:                  2,
  MAX_TOURNAMENT_PLAYERS:                  8,   // Single-Table; Multi-Table-Turniere via max_players_per_table
  MAX_PLAYERS_PER_TABLE:                   8,   // I17 — Default-Wert für Wizard
  // Rebuy (I18 — D23)
  REBUY_PHASE_DEFAULT_MIN:                60,
  RE_ENTRY_WINDOW_DEFAULT_MIN:            60,
  // Validation + Konsistenz (SA-1/SA-3)
  BLOB_SCHEMA_VERSION:                     2,
  CONSISTENCY_CHECK_INTERVAL_HOURS:       24,
} as const
```

---

## Anhang D — Frontend-Anpassungspunkte

Stand der HTML-Dateien nach I16: `HTML/greenPoker_v45.9.html`, `HTML/greenPokerAdmin_v44.26.html`.

### D.1 — Spieler-Frontend

| Bereich | Status | Beschreibung |
|---|---|---|
| Login-Feld (E-Mail) | ✅ I1 | `type="email"`, Placeholder „E-Mail-Adresse" |
| Registrierungsformular | ✅ I1 | Anzeigename + E-Mail-Bestätigung; kein Username |
| `submitLogin()` / `submitRegister()` | ✅ I1 | Rufen `POST /auth/login` / `/auth/register` |
| Cashier (Balance, Transfer, Donate, Request-Load, Verlauf) | ✅ I3 | Aus API |
| Anzeigename-Änderung im Profil-Tab | ✅ I4 | Cooldown-Anzeige, API |
| Layout-Slots / Tisch-Einstellungen → API | ✅ I4 | |
| Lobby-Tab „Turniere" | ✅ I6 | Aus API mit Anmelden/Starten |
| Turnier-Wizard in Lobby | ✅ I6 | `POST /api/tournaments` |
| Wizard Tab „Wiederholung / Serien-Zuordnung" | ✅ I7 | Schedule + Series-Dropdown |
| Tisch-Fenster (`openLiveTableWin`) | ✅ I5/I6 | Socket.IO; kein manuelles Sitzen bei Tournament-Tischen |
| `demoActionForWin` → echtes `socket.emit('action', ...)` | ✅ I10 | |
| Anzeigename in Sitz-Box (`sname.hn`) | ✅ I4 | `updateHeroName()` nach Login + Namensänderung |
| Chat-Nachrichten zeigen `displayName` | ✅ I12 | |
| HUD zeigt `displayName` | ✅ I16 | |
| Replayer zeigt `displayName` | ✅ I15 | |
| Inbox-Bell + Badge | ✅ I13 | |
| Club Board | ✅ I14 | |
| Rebuy-Button (heute `1+addon`) | ✅ I8 (Teil-Funktion) | Eingeschränkt; voller Ausbau in I18 |
| **Spielerliste in Lobby-Detail mit Live-Stack** | → I19 | |
| **Schematische + konkrete Auszahlungsstruktur** | → I19 | |
| **Re-Entry-Button nach Bust-Out** | → I18 | |
| **Wizard mit Modus-Auswahl (freeze/classic/re-entry)** | → I18 | |
| **Multi-Table-Toast / automatischer Tisch-Wechsel** | → I17 | |

### D.2 — Admin-Frontend

| Bereich | Status | Beschreibung |
|---|---|---|
| Login (E-Mail) | ✅ I2 | |
| Kicks & Bans | ✅ I2 | Aus API |
| Chip-Verwaltung + Club-Konten | ✅ I3 | |
| Tisch-Monitor | ✅ I5/I6 | Aus API; Turnier-Detail + Admin-Start-Button |
| Pause/Resume-Buttons für Turniere | ✅ I14 | |
| Admin-Posteingang (Konto-Anträge) | ✅ I13 | |
| Freischalt-Button für Pending-User | ✅ I13 | |
| Popup-Sender | ✅ I13 | |
| Club Board | ✅ I14 | |
| Spielerliste: `displayName` statt `username` | ✅ I2 | |
| Chat-Moderation (Wortfilter, Mutes, Slow-Mode, Pin) | ✅ I12 | |
| **Wizard mit Rebuy-Modus-Konfiguration** | → I18 | |
| **Balancer-Monitoring (welche Tische, Differenzen)** | → I17 | |

---

## Anhang E — Übergabe-Logs der abgeschlossenen Iterationen

Die folgenden Logs sind aus `backend/README.md` übernommen, soweit dort dokumentiert. Iterationen ohne separates Log in der README (I4, I8, I9, I10, I11, I16) sind im Code nachweisbar implementiert und werden hier in komprimierter Form anhand der vorhandenen Modul-Struktur dokumentiert.

### E.1 — Iteration 1: Bootstrap & Auth (2026-05-03)

**Lieferumfang:** Express + Socket.IO + DB-Init, `users` + `sessions`, Auth-Module (E-Mail-Login, Anzeigename, Doppelt-Eingabe), Session-Middleware, Health-Endpoint, Seed-Script.

**Aktiver Übergabezustand:** Server läuft mit Auth. Sessions per httpOnly-Cookie. Admin-Genehmigung: Registrierung setzt `status='pending'`. 18 Tests.

### E.2 — Iteration 2: Bans und Admin-Skeleton (2026-05-05)

**Lieferumfang:** `bans`-Tabelle, 3-Stufen-Logik (D16), `banGuard`-Middleware, Admin-Endpoints `/admin/bans`, `/admin/users/:id/kick`, `cleanup.worker`-Skeleton, Ban-Check bei Socket.IO-Connect.

**Aktiver Übergabezustand:** Vollsperre invalidiert Sessions sofort. CleanupWorker stellt `status='active'` nach Ablauf automatisch wieder her. 31 Tests.

### E.3 — Iteration 3: Accounting (2026-05-05)

**Lieferumfang:** `accounts`, `transactions`, `ledger_entries`, `TransactionService` (Nullsummen-Pattern), `ConsistencyCheckService`, `PolymorphicResolver`, vollständige Accounting-Endpoints.

**Aktiver Übergabezustand:** Jede Registrierung legt automatisch ein Spieler-Konto an. Club-Konten `Bounty Pool`, `Spenden`, `Admin-Chips` werden beim Server-Start sichergestellt. Nullsummen-Invariante in `TransactionService.commit()` mit Rollback bei Verstoß. 48 Tests.

### E.4 — Iteration 4: Profile + Preferences (2026-05-06)

**Lieferumfang (rekonstruiert aus Code):** `user_preferences`-Tabelle (4 Blobs), `BlobMigrationWorker`, `display-name.service.ts` mit Cooldown, Avatar-Upload (`AVATAR_MAX_BYTES=200_000`), `PUT /api/profile/display-name`, `GET/PUT /api/users/me/preferences`, `POST /api/users/me/avatar`.

**Aktiver Übergabezustand:** Preferences serverseitig in 4 versionierten JSON-Blobs (SA-3). 67 Tests.

### E.5 — Iteration 5: Tische und Sitzplätze (2026-05-08)

**Lieferumfang:** Schema `tables`, `seats` (`snapshot_at`), `seat_sessions`. `SeatStateMemory`-Klasse (D20). Socket.IO `table.join` / `table.leave`. Admin-Endpoints CRUD `tables`, `close`. Bootstrap-Recovery.

**Aktiver Übergabezustand:** Tisch-Modell steht; `bootstrapFromDb()` lädt Sitze beim Start. 86 Tests.

### E.6 — Iteration 6: Tournament-Skeleton + Sitzzuteilung (2026-05-08)

**Planabweichung gegenüber v4:** Registration + automatische Sitzzuteilung (ursprünglich I8) vorgezogen.

**Lieferumfang:** Schema `tournaments` (mit `table_id` FK), `tournament_players`, `blind_levels`. `TournamentRepository.create()` (atomar Tournament + Table + 8 Sitze). Endpoints `POST/GET /tournaments/...`, `register`, `unregister`, `start` mit Fisher-Yates. Manuelles Sitzen für Tournament-Tische serverseitig blockiert (D22).

**Aktiver Übergabezustand:** Tournament-Anmeldung und Sitzzuteilung vollständig. 101 Tests.

### E.7 — Iteration 7: Tournament-Schedules und Serien (2026-05-08)

**Lieferumfang:** Schema `tournament_series`, `tournament_schedules` + `scheduleId`/`seriesId` in `tournaments`. `schedule.service.ts` mit `calculateOccurrences()` (weekly) und `generateInstances()` (rolling 4-Wochen-Window). `schedule.worker.ts` läuft alle 60 min. 7 Endpoints (Series, Schedules, Leaderboard, Admin-Trigger).

**Aktiver Übergabezustand:** `POST /api/tournament-schedules` legt Schedule an + erstellt sofort alle Turnier-Instanzen im 28-Tage-Fenster. ScheduleWorker idempotent (`hasInstance`-Guard). 119 Tests.

### E.8 — Iteration 8: Buyin und Payout (rekonstruiert)

**Lieferumfang (aus Code):** `buyin.service.ts` mit `bookBuyin()`, `bookRebuy()` (heutige `1+addon`-Variante), `bookAddon()`, `bookRefund()` (Storno bei Abmeldung). `payout.calculator.ts` mit Steil/Standard/Flach-Kurven. Endpoints `/api/tournaments/:id/rebuy`, `/addon`, Admin `pause`/`resume`. `createPlayBlockGuard` auf Registration. `verifyPrizePools()` im ConsistencyCheckService.

**Aktiver Übergabezustand:** Buyin-Buchung bei Registration; Rake auf Bounty Pool; Storno bei Abmeldung. **Bekannte Lücken (Anlass für I18):** Rebuy nur einmalig (`1+addon`-Modus), kein Mid-Stack-Rebuy in echter Phase, kein Re-Entry nach Bust-Out, Wizard-Feld `wz-rebuy-phase` wird im Backend nicht ausgewertet.

### E.9 — Iteration 9: Hand-Engine Core (rekonstruiert)

**Lieferumfang (aus Code):** Schema `hands`, `hand_board_cards` (D21), `hand_players`, `actions`. `deck.ts` (`crypto.randomBytes`-Shuffle). `hand.evaluator.ts` (alle Hold'em-Hand-Klassen, Tie-Breaking). `pot.calculator.ts` (Side-Pots). `hand.engine.ts` (pure Funktion, kein I/O). `hand.repository.ts` mit `persistBoard()`.

**Aktiver Übergabezustand:** Tests grün für All-In, Side-Pots, Split, Showdown.

### E.10 — Iteration 10: Action-Loop und Realtime (rekonstruiert)

**Lieferumfang (aus Code):** `game.gateway.ts` Action-Handler, `action.validator.ts` (Min-Raise, Stack-Limit, am-Zug-Check), `time.bank.service.ts`, `hand.service.ts`. Memory-Sync bei Action; Snapshot bei Hand-Ende. Realtime-Broadcast `state-update`.

**Aktiver Übergabezustand:** Action in Tab A erscheint in Tab B < 100 ms.

### E.11 — Iteration 11: Showdown, Payouts, Bounty (rekonstruiert)

**Lieferumfang (aus Code):** Showdown-Pfad in `hand.service.ts`; Hand-Evaluator-Anwendung; Side-Pot-Verteilung. Bounty-Verteilung in `buyin.service.ts`. Tournament-Ende via `tournament.lifecycle.service.ts`: Payout-Curve-Anwendung, Series-Punkte-Berechnung (D17a `Buyin × √Spielerzahl × 1/(Platz+1)`), `final_place`, `prize_won`.

**Aktiver Übergabezustand:** Single-Table-Tournaments funktionieren End-to-End. Bekannte Findings dokumentiert in `Reviews/review_gameplay_I9-I11_2026-05-18.md`.

### E.12 — Iteration 12: Chat und Moderation (2026-05-19)

**Lieferumfang:** `chat_messages`, `word_filters`, `mutes`, `chat_room_settings`. `chat.gateway.ts` (Socket.IO `chat.send`). `moderation.service.ts` (Wortfilter mit Cache, Mute-Check, Slow-Mode-Throttle). 14 REST-Endpoints inkl. Admin-Pinned-Message.

**Aktiver Übergabezustand:** Club-Chat + Tisch-Chat (polymorphes `room_kind`-Modell). Admin: Wortfilter, Mutes, Slow-Mode, Pin via REST; Echtzeit-Chat via Socket.IO. 30 neue Tests.

### E.13 — Iteration 13: Notifications und Inbox (2026-05-19)

**Lieferumfang:** `inbox_items`, `popup_log`. `notification.service.ts` mit `sendToUser`, `notifyAdmins`, `sendPopup`. 4 Endpoints. Callbacks `onUserRegistered`, `onRequestLoad`, `onUserApproved`.

**Aktiver Übergabezustand:** Registrierung → Admin-Inbox-Item (`registration_request`, `action_required: true`). Request-Load → Admin-Inbox-Item. `POST /api/admin/users/:id/approve` setzt pending → active + Inbox-Item an User. Realtime via Socket-Event `notification` im `user:<id>`-Room. 17 neue Tests.

### E.14 — Iteration 14: Club Board und Admin-Polishing (2026-05-19)

**Lieferumfang:** `club_board`-Tabelle. 4 Endpoints (GET/POST/PATCH/DELETE `/api/clubboard`). Tisch-Chat entfernt aus REST (`/api/chat/table/...` gibt 400; `chat.send` mit `roomKind='table'` abgewiesen). Admin: Pause/Resume-Buttons für laufende Turniere.

**Aktiver Übergabezustand:** Club Board als Soft-Delete-Liste; Eigenautor + Admin können löschen/editieren. 16 neue Tests.

**Restpunkt:** `backup.worker.ts` ist als Skeleton geplant, aber nicht implementiert (vgl. 8.4). Bei Bedarf separate Mini-Iteration „I14b — BackupWorker".

### E.15 — Iteration 15: Hand-History und Replayer (2026-05-19)

**Lieferumfang:** `handhistory.repository.ts` mit `findHandById`, `wasUserAtTableDuringHand` (D7), `findHandsForUserAtTable` (D7-Filter via Subquery), `findBoardCards`. `replayer.service.ts` mit `getHandDetail()` (Sichtbarkeit + Maskierung), `getHandsList()`. Endpoints `GET /api/hands/:id`, `GET /api/hands?table=&from=&to=`. Frontend-Replayer API-gesteuert mit `convertApiHandToRpl()`-Konverter.

**Aktiver Übergabezustand:** Eigene Karten immer sichtbar; fremde nur bei `shown_at_showdown=1`. Liste filtert auf Hände, bei denen der User am Tisch war. 15 neue Tests.

### E.16 — Iteration 16: HUD und Series-Leaderboards (rekonstruiert)

**Lieferumfang (aus Code):** `stats.flagger.ts` (VPIP/PFR/3-Bet pro Hand-Action-Sequenz, gemäß D17b), `stats.aggregator.ts` (Aggregation über Hände eines Users), `stats.routes.ts` mit `GET /api/stats/me`, `GET /api/stats/users/:id`. `schedule.service.ts.getLeaderboard()` für Series-Leaderboards (Erweiterung aus I7 mit echten Punkten aus I11).

**Aktiver Übergabezustand:** Phase C funktional vollständig für Single-Table-Tournaments. 341 Tests grün.

---

**Ende der Umsetzungsstrategie v6.**
