0Plan-Revisionen v5 → v6

v6 ist die alleinige Quelle. Alle früheren Strategiepapiere sind archiviert. Externe Quellen wurden vollständig eingearbeitet und müssen nicht mehr konsultiert werden.

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.

0.2 Inhaltliche Änderungen gegenüber v5

Bereichv5v6
Iterations-StatusI1–I6 abgeschlossenI1–I16 abgeschlossen
Phasen-ReihenfolgeA → C → B → D → E (D4)C[I17–I19] → B Port → C/D verschränkt (D24)
Rebuy-Mechanikrebuy_mode ∈ {none, 1+addon} (Skizze)3 Modi: freeze-out / classic / re-entry (D23)
Übergeordneter RahmenVerweis auf externe DateiVollständig in Abschnitt 1 eingearbeitet
SA-RisikenVerweis auf daten_schema-analyse_1.htmlVollständig in Abschnitt 9 ausformuliert
Session-MechanismusVerweis auf externe DateiIn Abschnitt 1.4 eingearbeitet
Hand-History-SchemaVerweis auf handhistoryStrategie.htmlIn Abschnitt 1.5 eingearbeitet
Stack-BegründungVerweis auf externe DateiIn Abschnitt 1.2 eingearbeitet
Übergabe-LogsVerweis auf backend/README.mdAls Anhang E eingearbeitet
HTML-Standv45.72 / v44.22v45.9 / v44.26

0.3 Neue Entscheidungen

D23 Rebuy-Mechanik NEU D24 Phasen-Reihenfolge NEU D25 Lobby-Anzeigeumfang NEU

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:

Phasen-Modell (aktualisiert durch D24):

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

Multiplayer-Scope (verbindlich, unverändert seit v1):

1.2 Tech-Stack — verbindliche Festlegung

LayerWahlBegründung
SpracheTypeScript (strict)Einheit von Frontend und Backend; typsichere Schnittstellen
RuntimeNode.jsSynchrone Event-Loop passt zu Socket.IO
Web-FrameworkExpressMinimal-Framework, geringer Lock-in
RealtimeSocket.IOWiederverbindungsstrategie, Rooms, Browser-Kompatibilität
PersistenzSQLite (data/greenpoker.db)Viele kleine Writes aus genau einem Writer-Prozess. Kein separater DB-Prozess.
SQLite-Bindingbetter-sqlite3Synchron, nativer Treiber, kein async-Dispatch-Overhead
ORMDrizzle ORM + drizzle-kitTypisierte Queries, Migrations als SQL-Files
Journal-ModusWAL (journal_mode=WAL)Concurrent Reads neben einem Writer
Foreign KeysPRAGMA foreign_keys=ONErzwingt FK-Constraints; in SQLite default off
Busy-Timeoutbusy_timeout=5000Defensiv für gelegentliche Read-Lock-Konkurrenz
Auth-HashingbcryptVerbreitet, geprüft
ValidierungzodEingangs-Validierung von API-Bodies und JSON-Blobs (SA-3)
TestsvitestTypeScript-nativ, schnell; In-Memory-SQLite-Tests möglich

Verworfene Alternativen

AlternativeVerworfen, weil
PostgresWorkload-Fit-Nachteil bei vielen kleinen Writes; zusätzliche Betriebskomplexität. Reversibilitätspfad bleibt offen.
PrismaEigene Engine-IPC, höhere Latenz, weniger Kontrolle
libSQL / TursoZusätzliche Abstraktion ohne Replikationsziel
JWT-SessionsSiehe 1.4 — macht strukturellen Vorteil zunichte
MongoDBFür stark relationales Domänenmodell unpassend

1.3 Backup-Strategie

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

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

Funktionsweise:

  1. Login: Server prüft E-Mail + Passwort, erzeugt 32-Byte-Zufalls-ID (crypto.randomBytes(32).toString('hex')), speichert in Tabelle sessions, setzt Cookie session_id mit httpOnly; Secure; SameSite=Lax
  2. Folge-Request: Cookie automatisch gesendet, Middleware liest und schlägt in sessions nach (Prepared Statement), hängt req.user an
  3. Logout: DELETE FROM sessions WHERE id = ? plus Cookie-Clear
  4. „Alle Geräte abmelden": DELETE FROM sessions WHERE user_id = ?
  5. Cleanup-Worker löscht abgelaufene Sessions alle 60 Min

Warum nicht JWT: Single-Node-Scope macht den einzigen JWT-Vorteil (zustandslose Multi-Node-Validierung) gegenstandslos. Widerruf bei Ban/Reset ist mit Server-Session-ID eine DELETE-Query. DB-Lookup kostet bei WAL-SQLite + Prepared Statement einstellige Mikrosekunden.

SESSION_LIFETIME_DAYS = 30 mit Refresh bei jedem Request via 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:

Stats-Definition (D17b, PokerTracker-4-Standard): VPIP = freiwillige Geldzugabe preflop außer Blind-Posten; PFR = erste freiwillige Raise preflop; 3-Bet = erste Re-Raise preflop; AF = (Bet+Raise)/Call mit Numerator und Denominator separat gespeichert.

1.6 Zentrale Spielregel-Konstanten (D17c)

KonstanteWertBedeutung
TIME_PER_ACTION_DEFAULT_SEC30Bedenkzeit pro Aktion
TIME_BANK_INITIAL_MS90 000Time-Bank-Startwert pro Spieler
TIME_BANK_REFILL_PER_HAND_MS5 000Auffüllbetrag pro abgeschlossener Hand
TIME_BANK_MAX_MS90 000Obergrenze für Time-Bank-Auffüllung
LATE_REG_DEFAULT_MIN30Late-Registration-Fenster ab Turnierstart
MULTI_TABLE_LIMIT4Maximalzahl simultaner Tische pro Spieler
DEFAULT_SEAT_COUNT8Sitze pro Tisch
DEFAULT_START_STACK10 000Startstack bei Turnierstart
MIN_TOURNAMENT_PLAYERS2Mindest-Spielerzahl für Turnier-Start
MAX_TOURNAMENT_PLAYERS8Maximal-Spielerzahl pro Turnier (Single-Table)

Vollständige Konstanten-Liste siehe Anhang C.

2Decision Log

Verbindliche Entscheidungen. Spätere Abweichungen müssen explizit als Plan-Revision dokumentiert werden. D23, D24, D25 sind neu in v6.

NrThemaEntscheidungKonsequenz
D1Geld-ModellReines SpielgeldKein Payment-Gateway, keine KYC
D2RollenNur admin und playerModerator-Rolle ist Phase E
D3Tisch-ErstellungVollfreigabe für alle SpielerNur Bounty-Clubkonto bleibt admin-only
D4Tournament/CashgameTournament zuerst, Cashgame in Phase DPhase C liefert Tournament-Engine
D5Multi-Table-BalancingI17TournamentBalancer als abgrenzbares Modul
D6Hand-History-SchemaVariante 2 + erweiterte FlagsSeparate hand_players-Tabelle mit Stats-Flags
D7Replayer-SichtbarkeitAlle Hände am Tisch während Anwesenheit; eigene gemuckte Karten sichtbarFilter via seat_sessions
D8Multi-Table & Time-BankLimit 4 Tische; TB kumuliert mit Auffüllung; Auto-Sit-Out bei AblaufPro tournament_players-Zeile aktueller TB-Stand
D9Tournament-WiederholungHybrid (Schedule + 4-Wochen-Window), In-Process-Schedulertournament_schedules + Worker-Loop
D10User-PreferencesServerseitig pro UserTabelle user_preferences
D11Notifikations-TransportSocket.IO Rooms + RESTModule emittieren in eigene Rooms
D12Client Phase CBestehendes HTML wird angepasstMock-Arrays werden durch API-Calls ersetzt
D13SRP-GranularitätLesart 3: Module nach SachgebietIterativ verfeinert
D14Iterations-GrößeKlein, jede Iteration lauffähigSchema-Änderungen und Spiellogik nie in derselben Session
D15BuchungssystemHybrid (doppelte Buchung + Saldo-Cache); Buchungen finalStornierung nur per Gegenbuchung
D16Bans3 Stufen (Verwarnung/Spielsperre/Vollsperre), alle befristetbans.expires_at zwingend
D17aSeries-PunktePokerStars-Formel Buyin × √Spielerzahl × 1/(Platz+1)In tournament_players.series_points persistiert
D17bStats-DefinitionPokerTracker-4-StandardVPIP zählt nicht BB-Post; PFR erste freiwillige Raise
D17cZeitwerteBedenkzeit 30 s; TB 90 s + 5 s/Hand bis 90 s; Late-Reg 30 minDefaults als Konstanten in config.ts
D18Login-IdentifierE-Mail-Adresse, lowercase normalisiertusername entfällt; Doppelt-Eingabe im Registrierungsformular
D19AnzeigenamePflicht, eindeutig (COLLATE NOCASE), im Profil mit 30-Tage-Cooldown änderbarÜberall statt username
D20Game-State-PersistenzMemory-First für seats; DB-Tabelle nur als SnapshotReduziert Write-Amplifikation (SA-5)
D21Hand-Board-SpeicherungHybrid: hands.board als String + Tabelle hand_board_cardsSynchron in einer Transaktion
D22Sitzzuteilung TurniereAutomatisch beim Turnierstart (Fisher-Yates); manuelles Sitzen blockierttable.join mit seatIndex gibt 400 für Tournament-Tische
D23Rebuy-Mechanik neuDrei Modi im Wizard: freeze-out, classic (Rebuy-Phase + Addon), re-entry (Bust-Out-Re-Entry im Fenster)Schema-Erweiterung Migration 0011; Lifecycle-Erweiterung; UI-Anpassung in Wizard und Lobby
D24Phasen-Reihenfolge neuC-Restpunkte (I17–I19) in HTML → React-Port Phase 1 → Lobby-Strukturarbeit in React verschränkt mit Backend-Anpassungen → Cashgame (Phase D) parallel Backend+ReactD4 bleibt sachlich gültig; Reihenfolge auf Iterations-Ebene verfeinert. Begründung in Reviews/einschaetzung_reactMigration_vor_backend_2026-05-21.md
D25Lobby-Anzeigeumfang neuTurnier-Lobby zeigt angemeldete Spieler in Liste; Sortierung bei running nach Stack-Size absteigend; Auszahlungsstruktur schematisch (Prozente) UND konkret (Chip-Beträge bei aktuellem Prize Pool); Standard-Buttons modus-abhängigAPI-Erweiterung in I19; lobby.update Socket-Event; Stack-Live-Werte aus SeatStateMemory

3Scope und Abgrenzung

3.1 In Scope (gesamtes Projekt)

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

Frontend: Phase A/C: HTML inkrementell durch API-Calls angebunden. Phase B: vollständige React-Port-Phase.

3.2 Nicht in Scope

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

4Architektur-Ü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. Innerhalb eines Moduls werden Klassen nach SRP gesplittet. Module kommunizieren über Direkt-Injektion in server.ts, Realtime-Bus für asynchrone Events oder Callbacks.

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

SeatStateMemory hält In-Memory-Map Map<tableId, Map<seatIndex, SeatState>>. DB-Tabelle seats ist Snapshot-Speicher, kein Action-Log.

EreignisAktion
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-EndeSnapshot-Write: UPDATE seats SET stack=? für alle besetzten Sitze
Server-StartBootstrap-Recovery aus seats-Tabelle
Rebuy/Re-Entry (I18)Memory-Update Stack + DB-Update (gleiche Transaktion wie Buchung)

4.3 Datenfluss einer Spieler-Action

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
      → 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

5Datenmodell

5.1 Migrationsreihenfolge

#IterationTabellen
0001I1users, sessions
0002I2bans
0003I3accounts, transactions, ledger_entries
0004I4user_preferences
0005I5tables, seats, seat_sessions
0006I6tournaments, tournament_players, blind_levels
0006bI7tournament_schedules, tournament_series
0007I9hands, hand_board_cards, hand_players, actions
0008I12chat_messages, word_filters, mutes, chat_room_settings
0009I13inbox_items, popup_log
0010I14club_board
0011I18 (geplant)Rebuy-Erweiterung: tournaments.rebuy_mode, rebuy_phase_min, rebuy_cost, addon_cost, addon_chips, rebuy_chips, re_entry_window_min, tournament_players.re_entry_count

5.2 Vollständiges Schema (Stand I16)

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)
├── 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

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') → I18: 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, series_id FK

tournament_players
├── id INTEGER PK, tournament_id FK, user_id FK
├── 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
├── pot_total INTEGER NULL, rake INTEGER default 0

hand_board_cards
├── 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, text TEXT, at INTEGER
├── pinned INTEGER default 0, deleted_at INTEGER NULL, edited_at INTEGER NULL

word_filters · mutes · chat_room_settings (slow_mode_sec, updated_at)

inbox_items
├── id INTEGER PK, user_id FK, 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, 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
├── created_at INTEGER, edited_at INTEGER NULL, deleted_at INTEGER NULL

6Programmstruktur

6.1 Verzeichnisstruktur (Stand I16)

backend/
├── data/                           (gitignored)
│   ├── greenpoker.db
│   ├── avatars/
│   └── backups/
├── drizzle/                        (SQL-Migrationen 0001–0010)
├── scripts/seed.ts
├── 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
│   │   └── migrator.ts
│   ├── core/
│   │   ├── logger.ts · errors.ts · time.ts · ids.ts
│   │   ├── realtime.ts             (RealtimeBus)
│   │   ├── consistency-check.service.ts
│   │   └── polymorphic-resolver.ts
│   ├── validation/
│   │   ├── prefs.schemas.ts · auth.schemas.ts
│   │   ├── accounting.schemas.ts · schedule.schemas.ts
│   │   └── blob-versioning.ts      (SA-3)
│   ├── 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 · 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.module.ts
│   │   ├── handhistory/            ✅ I15, I16
│   │   │   ├── handhistory.repository.ts · replayer.service.ts
│   │   │   ├── handhistory.routes.ts · handhistory.module.ts
│   │   │   ├── stats.flagger.ts · 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)
│   └── tests/
│       ├── helpers/app.ts
│       └── *.test.ts               (341 Tests, alle grün)

6.2 Modul-Verantwortlichkeiten

ModulVerantwortetWichtige Klassen
authRegistrierung, Login, Session, Middlewareauth.service.ts, auth.middleware.ts
ban3-Stufen-Bans (D16), Guardsban.service.ts, ban.guards.ts
accountingDoppelte Buchführung (D15), Kontenaccount.repository.ts, transaction.service.ts
userProfile, Anzeigename (D19), Preferences (D10), Avatardisplay-name.service.ts, preferences.service.ts
gameTische, Sitze, Memory-First-State (D20), Hand-Engine, Action-Loop, Realtimesiehe 6.1
tournamentTurnier-Lifecycle, Registration, Sitzzuteilung (D22), Schedules, Buyin/Payout, Bountysiehe 6.1
handhistoryReplayer (D7), HUD-Stats (D17b), Series-Leaderboardssiehe 6.1
chatClub-Chat + Tisch-Chat, Moderation, Wortfilter, Mutes, Slow-Modechat.gateway.ts, moderation.service.ts
clubboardKorkbrett (Posts mit Soft-Delete)clubboard.repository.ts
notificationInbox-Items, Popups, Realtime-Notificationnotification.service.ts

7API-Spezifikation

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

7.1 Implementierte Endpoints (I1–I16)

Auth & Session

MethodPathBeschreibung
POST/auth/register{email, emailConfirm, displayName, password}
POST/auth/login{email, password} → Cookie
POST/auth/logoutLöscht aktuelle Session
POST/auth/logout-allLöscht alle Sessions des Users
GET/auth/me{id, email, displayName, role, status, avatarPath}

User & Preferences

MethodPathBeschreibung
GET/users/:id/profileÖffentliches Profil
PUT/api/profile/display-nameCooldown 30 Tage
GET/api/users/me/preferences4 Blobs
PUT/api/users/me/preferencesPartial-Update
POST/api/users/me/avatarMultipart, max 200 KB

Accounting

MethodPathRolle
GET/api/accounts/meplayer
POST/api/accounts/me/transferplayer
POST/api/accounts/me/donateplayer
POST/api/accounts/me/request-loadplayer
GET/api/admin/accountsadmin
POST/api/admin/accounts/transferadmin
POST/api/admin/users/:id/chipsadmin

Tournaments

MethodPathBeschreibung
GET/api/tournamentsListe (?status=registering)
GET/api/tournaments/:idDetail + players + blind_levels
POST/api/tournamentsAnlegen (alle Spieler, D3)
POST/api/tournaments/:id/registerAnmelden
POST/api/tournaments/:id/unregisterAbmelden (vor Start)
POST/api/tournaments/:id/startStart + Fisher-Yates
POST/api/tournaments/:id/rebuyRebuy (heutige 1+addon-Variante)
POST/api/tournaments/:id/addonAddon
POST/api/admin/tournaments/:id/pauseAdmin
POST/api/admin/tournaments/:id/resumeAdmin

Schedules & Series (I7)

MethodPathBeschreibung
GET/api/tournament-schedulesListe
POST/api/tournament-schedulesWiederholung anlegen
DELETE/api/tournament-schedules/:idLöschen
GET/api/tournament-seriesListe
POST/api/tournament-seriesSerie anlegen
GET/api/tournament-series/:id/leaderboardSeries-Punkte-Rangliste

Hand-History & Stats (I15, I16)

MethodPathBeschreibung
GET/api/hands/:idHand-Detail mit D7-Sichtbarkeitsfilter
GET/api/hands?table=&from=&to=Hände-Liste (nur eigene Anwesenheit)
GET/api/stats/meVPIP/PFR/3-Bet/AF aggregiert
GET/api/stats/users/:idStats anderer User (öffentlich)

Chat & ClubBoard

MethodPathBeschreibung
GET/api/chat/clubClub-Chat-Verlauf
DELETE/api/chat/messages/:idEigene/Admin
PATCH/api/chat/messages/:idEigene
GET/api/clubboardAlle Posts (newest first)
POST/api/clubboardPost anlegen
PATCH/api/clubboard/:idEigenen Post bearbeiten
DELETE/api/clubboard/:idSoft-Delete

Notifications & Inbox (I13)

MethodPathBeschreibung
GET/api/inboxEigener Posteingang (unread zuerst)
PATCH/api/inbox/:id/readAls gelesen markieren
POST/api/admin/popupPopup an alle oder einen User

7.2 Geplante Endpoints (I17–I19)

MethodPathIterZweck
Balancer-Logik (interne Events)I17seat.assigned-Event bei Tischbruch
POST/api/tournaments/:id/re-entryI18Re-Entry nach Bust-Out (modusabhängig)
GET/api/tournaments/:id/lobby-stateI19Erweiterter Lobby-State: Spielerliste mit Live-Stack, Payout-Vorschau
GET/api/tournaments/:id/payout-structureI19Schematische + konkrete Auszahlungsstruktur

7.3 Socket.IO-Events

Event (Client → Server)PayloadSeit
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)RoomSeit
state-updatetable:<id>I5
balance:updateuser:<id>I3
chat.newtable:<id> / club:globalI12
notificationuser:<id>I13
tournament.updatetournament:<id>I11
lobby.update I19tournament:<id>geplant
seat.assigned I17tournament:<id>geplant

8Querschnittsthemen

RealtimeBus

core/realtime.ts — Fassade um Socket.IO. Module rufen bus.emit(room, event, payload). Ban-Check beim Verbindungsaufbau. Module kennen io nicht direkt.

ConsistencyCheckService

Beim Bootstrap: verifyAccounts() (SA-1), verifyLedgerInvariants() (SA-2), verifyPrizePools(). Korrigiert balance_cache aus Ledger-Summe und protokolliert Abweichungen.

BlobValidator

validation/blob-versioning.ts — Validiert JSON-Blobs gegen zod-Schema, prüft _v ≤ BLOB_SCHEMA_VERSION (SA-3). BlobMigrationWorker beim Bootstrap migriert auf aktuelle Version.

Clock

core/time.ts — Interface mit realClock und MockClock für Tests. Alle Zeitlogik (Cooldown, Session-Ablauf, Late-Reg) geht durch Clock — keine direkten Date.now()-Aufrufe in Modulen.

CleanupWorker

Läuft alle 60 Min: löscht abgelaufene Sessions, setzt status='active' nach Ablauf von Vollsperren, löscht abgelaufene Mutes.

ScheduleWorker

Rollender 4-Wochen-Window-Generator für tournament_schedules. Idempotent (hasInstance-Guard), läuft beim Bootstrap und alle 60 Min.

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

IDBefundBehandlungStatus
SA-1 Cache-Konsistenz von balance_cache und prize_pool. Denormalisierte Aggregate ohne DB-seitige Konsistenzerzwingung. Alle Schreibwege durch TransactionService.commit() (Nullsummen-Pattern). ConsistencyCheckService.verifyAccounts() beim Bootstrap korrigiert Abweichungen. ✅ I3
SA-2 Nullsummen-Invariante nur per Test. Die Buchungsregel ist eine Anwendungsinvariante, kein DB-Constraint. TransactionService.commit() validiert Nullsumme in derselben Transaktion. verifyLedgerInvariants() beim Bootstrap. ✅ I3
SA-3 JSON-Blobs in Preferences und Schedule-Templates. Entziehen sich DB-Schema; Schema-Drift möglich. Jeder Blob trägt _v. BlobValidator prüft gegen zod-Schema. BlobMigrationWorker beim Bootstrap. ✅ I4
SA-4 Polymorphe Referenzen ohne FK-Constraint. ref_type + ref_id in transactions und inbox_items kann SQLite nicht per FK absichern. PolymorphicResolver als einzige Auflösungsstelle. CHECK-Constraint für accounts.owner_type. ✅ I3+
SA-5 Write-Amplifikation durch Game-State in seats. DB-getriebene Live-Verwaltung wäre SQLite-Bottleneck. Memory-First-Strategie (D20): SeatStateMemory. DB-Snapshot nur bei Hand-Ende, Sit-Down/Stand-Up, Tournament-Start. ✅ I5 (D20)
SA-6 Fehlende Moderator-Rolle. users.role kennt nur admin und player. Backlog für Phase E. Rollen-Erweiterung als eigene Migration plus Permission-Map. Backlog (E)
SA-7 Board als String in hands.board. Kompakter String gut für Replayer, blockiert analytische Queries auf Karten-Ebene. Hybrid-Modell (D21): String bleibt; zusätzlich hand_board_cards mit pro-Karten-Zeilen. Beide in einer Transaktion. ✅ I9 (D21)

10Risiken R1 bis R14

IDRisikoStatus nach I16
R1Tournament-Engine groß, Kontext-SprengungMitigiert — I6–I8 + I11 sauber zerlegt; nur Balancer (I17) bleibt offen
R2Hand-Engine Sonderfälle (All-In, Side-Pots, Split, Walk)Mitigiert — I9 Unit-Test-Abdeckung, I11 End-to-End verifiziert
R3HTML-Anbindung organisch gewachsenWächst weiter mit I17–I19. Wird mit Phase B (React-Port) adressiert.
R4Replayer-Sichtbarkeitsfilter subtilMitigiert — I15 mit D7-Filter implementiert, getestet
R5In-Process-Scheduler Neustart-VerhaltenMitigiert — ScheduleWorker idempotent (hasInstance-Guard)
R6Multi-Table-Balancing vertagtOffen — I17
R7better-sqlite3 synchron auf EventloopBeobachtet — keine Auffälligkeiten in I10/I11. Bei Cashgame erneut prüfen.
R8Frontend-Mock-InkonsistenzenReduziert — fast alle Mock-Stellen ersetzt; Restpunkte siehe Anhang D
R9Memory ↔ DB-Drift bei CrashMitigiert — Bootstrap-Recovery; Hand-in-flight wird bei Crash verworfen (akzeptiert)
R10Anzeigename-Wechsel während laufender HandMitigiert — hand_players.user_id referenziert; Anzeigename zur Laufzeit aufgelöst
R11E-Mail-Tippfehler bei RegistrierungTeil-Mitigation — Doppelt-Eingabe-Validierung. Admin-Korrektur bleibt Backlog.
R12React-Stack-Wahl: ungeeignete Library zementiert ProblemeMitigation: separate Brainstorming-Session vor Phase B (Stack-Festlegung)
R13Cashgame-Anforderungen unklarMitigation: separate Brainstorming-Session vor Phase D
R14Lobby-Strukturarbeit verändert API → React-Bindings müssen nachMitigation: Lobby-Umbau (I19) vor Phase B Port-Phase 1

11Test-Strategie

11.1 Pyramide

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

11.2 Status nach I16

BereichTestsStatus
auth18
ban13
accounting17
user / preferences / display-name19
game / tables / seat-state19
tournament + lifecycle + payout15
schedule18
chat / moderation30
notification / inbox17
clubboard16
handhistory / replayer / stats15
Gesamt341✅ alle grün

11.3 Test-Konventionen

11.4 Geplante Test-Schwerpunkte

IterationTest-Fokus
I17Balancer (Tisch-Bruch, Final-Table-Merge, MULTI_TABLE_LIMIT-Check) — balancer.test.ts, ≥ 8 Fälle
I18Rebuy classic (Multi-Rebuy, Addon-Window), Re-Entry (Bust-Out-Erkennung, neue Sitzzuteilung), Wizard-Validierung — rebuy.test.ts, ≥ 12 Fälle
I19Lobby-State (Stack-Sortierung live, Payout-Berechnung bei verschiedenen Curves) — lobby-state.test.ts
Phase BReact-Testing-Library für Komponenten, Playwright für E2E
Phase DCashgame-Lifecycle (Sit-Down/Stand-Up, Auto-Top-Up, Rake-Erfassung)

12Roadmap mit Iterationsschritten

12.1 Prinzipien (D14)

12.2 Iterations-Übersicht

Phase C — Backend Restpunkte
   I17 — Multi-Table-Balancer
   I18 — Rebuy-Vollausbau (D23)
   I19 — Turnier-Lobby-Anzeigeumfang (D25)
       ↓
Phase B — React-Port
   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

I17 Multi-Table-Balancer ausstehend

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)
  • Schema: tournaments.max_players_per_table INTEGER default 8 (Migration 0010b)
  • Socket-Event seat.assigned für Spieler, deren Tisch gewechselt wird
  • HTML-Anbindung: Toast „Du wurdest an Tisch X umgesetzt", automatischer Tisch-Wechsel

Verifikation: 10 Spieler in einem Multi-Table-Turnier (2 × 5). Spieler bustet out → Balancer prüft Differenz → bei > 1 ausgeglichen. Bei count = 8 Final-Table-Merge. Tests: balancer.test.ts mit ≥ 8 Fällen.

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

12.4 I18 — Rebuy-Vollausbau (D23)

I18 Rebuy-Vollausbau ausstehend

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
├── addon_chips INTEGER NULL                    -- nur classic
├── rebuy_chips INTEGER NULL                    -- nur classic (default = Startstack)
├── re_entry_window_min INTEGER default 60      -- nur re-entry
├── rebuy_phase_end_at INTEGER NULL             -- berechnet bei Tournament-Start

tournament_players
├── re_entry_count INTEGER default 0

Lieferumfang:

  1. Lifecycle-Erweiterung: Bei start berechne rebuy_phase_end_at. Timer triggert „Rebuy-Phase Ende" → Broadcast. Re-Entry bei Bust-Out im Fenster: Fisher-Yates auf freie Sitze.
  2. buyin.service.ts erweitern: bookRebuy() modus-abhängig; bookAddon() nur classic; bookReEntry() nur re-entry nach Bust-Out im Fenster.
  3. API: POST /api/tournaments/:id/rebuy (anhand Modus), /re-entry (neu), /addon.
  4. Wizard (HTML): Dropdown mit drei Optionen; pro Modus konditionale Felder.
  5. Turnier-Lobby (HTML): Rebuy-Phase-Anzeige, Rebuy- und Re-Entry-Button modus-/zustandsabhängig.

Verifikation: Classic: 3-Spieler, Rebuy-Phase 5 Min., mehrfach Rebuy + Addon → Prize Pool stimmt. Re-Entry: Bust-Out → Re-Entry → neuer Sitz; außerhalb Fenster: 409. Freeze-Out: Rebuy-Endpoint gibt 409. Tests: rebuy.test.ts ≥ 12 Fälle.

Übergabezustand: Alle drei Rebuy-Modi funktional; Wizard + Lobby korrekt.

12.5 I19 — Turnier-Lobby-Anzeigeumfang (D25)

I19 Turnier-Lobby-Anzeigeumfang ausstehend

Eingangsstand: I18.

Lieferumfang:

  1. API-Erweiterung: GET /api/tournaments/:id/lobby-state — Turnier-Stamm, Spielerliste mit Live-Stacks (aus SeatStateMemory), Payout-Vorschau. GET .../payout-structure — schematisch + konkret. Socket-Event lobby.update.
  2. HTML-Anpassung: Spielerliste mit displayName, Status, Stack (bei running). Sortierung: registering → Anmeldezeit; running → Stack absteigend; finishedfinal_place. Auszahlungs-Sektion: zweispaltig (Anteil % / Chips).
  3. Payout-Curve-Logik: Funktion previewPayoutStructure(playerCount, prizePool, curve) in payout.calculator.ts.

Verifikation: Anmeldung → Liste aktualisiert < 100 ms. Stack-Änderung → Sortierung aktualisiert. Payout bei standard-Curve + 8 Spielern korrekt.

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

12.6 Phase B — React-Port

Vorbedingung: Brainstorming-Session zur Stack-Festlegung (D26) vor B0. Bewusst nicht in v6 vorweggenommen.
IterInhaltVoraussetzung
B0Stack-Festlegung als eigene Brainstorming-Session — Entscheidung wird als D26 dokumentiertI19
B1Plattform-Setup: frontend/, Build, Linting, TS strict, Test-StackB0
B2.1Auth + Profil + Cashier-DomäneB1
B2.2Lobby + Tisch-Fenster + WizardB2.1
B2.3Replayer + HUD + Admin-BackofficeB2.2
B3UI-Strukturarbeiten in React (Lobby-Refinement, Settings)B2.3
B4E2E-Tests (Playwright), Performance-Baseline (Lighthouse)B3

Stack-Anforderungen (ohne konkrete Produkt-Wahl): Strict-TS mit noUncheckedIndexedAccess, Komponententests mit JSDOM, Routing mit Lazy-Loading, kein Singleton-Store-Anti-Pattern, Komponentenscope-Styling, Hot-Reload < 1 s, Bundle < 500 KB gzipped initial.

12.7 Phase D — Cashgame-Engine

Vorbedingung: Brainstorming-Session zur Anforderungs-Spezifikation (D0). Bewusst nicht in v6 vorweggenommen.

Offene Fragen: NL Hold'em allein oder PL/FL? Buy-In-Range pro Tisch? Auto-Top-Up: User-Preference oder pro Tisch? Stand-Up-Politik? Rake-Modell? Mehrtisch-Limit?

IterInhalt
D0Anforderungs-Spezifikation (Brainstorming)
D1Schema: Cashgame-Erweiterung in tables
D2Lifecycle: Sit-Down mit Buy-In, Stand-Up, Auto-Top-Up
D3Hand-Engine-Anpassung: Rake-Erfassung pro Pot
D4React-Cashgame-Tisch-Komponente
D5Lobby-Integration: Cashgame-Liste mit Stakes, Auslastung, Average-Pot

AAnhang A — Glossar

BegriffBedeutung
Anzeigename (display_name)Pflichtfeld, eindeutig (COLLATE NOCASE), 30-Tage-Cooldown. Überall statt username.
E-MailLowercase 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-YatesZufälliger Shuffle-Algorithmus für Sitzzuteilung (D22) und Deck.
VPIPVoluntary Put In Pot. BB-Post zählt nicht (D17b).
PFRPre-Flop Raise. Erste freiwillige Raise (D17b).
3-BetErste Re-Raise preflop (D17b).
AFAggression Factor = (Bet+Raise)/(Call) (D17b).
WALWrite-Ahead-Log-Modus von SQLite. Erlaubt concurrent reads neben einem writer.
Side-PotZusätzlicher Pot bei All-In mit unterschiedlichen Stack-Höhen.
Time-BankPro Spieler kumulierter Zeitpuffer (D8, D17c).
Memory-FirstLive-Spielzustand im Anwendungs-Memory, DB nur Snapshot (D20).
Bust-OutSpieler verliert gesamten Stack und scheidet aus.
Re-EntryWiedereinstieg nach Bust-Out im definierten Fenster mit neuem Buy-In (D23).
Classic RebuyMehrfacher Stack-Auffüll-Kauf während Rebuy-Phase (D23).
AddonEinmaliger optionaler Stack-Zukauf am Ende der Rebuy-Phase (D23).
Freeze-OutTurnier ohne Rebuy/Addon — Single-Buyin (D23).
Late-RegLate-Registration-Fenster ab Tournament-Start (D17c).

BAnhang B — Datenbank-Indexes

Vollständige Index-Liste (Stand I16):

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 wird.

CAnhang C — Konfigurations-Konstanten

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

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,
  MAX_PLAYERS_PER_TABLE:                   8,   // I17 — Default 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

DAnhang D — Frontend-Anpassungspunkte

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

D.1 — Spieler-Frontend

BereichStatusBeschreibung
Login-Feld (E-Mail)✅ I1type="email", Placeholder „E-Mail-Adresse"
Registrierungsformular✅ I1Anzeigename + E-Mail-Bestätigung; kein Username
submitLogin() / submitRegister()✅ I1Rufen POST /auth/login / /auth/register
Cashier (Balance, Transfer, Donate, Request-Load, Verlauf)✅ I3Aus API
Anzeigename-Änderung im Profil-Tab✅ I4Cooldown-Anzeige, API
Layout-Slots / Tisch-Einstellungen → API✅ I4
Lobby-Tab „Turniere"✅ I6Aus API mit Anmelden/Starten
Turnier-Wizard in Lobby✅ I6POST /api/tournaments
Wizard Tab „Wiederholung / Serien-Zuordnung"✅ I7Schedule + Series-Dropdown
Tisch-Fenster (openLiveTableWin)✅ I5/I6Socket.IO; kein manuelles Sitzen bei Tournament-Tischen
demoActionForWin → echtes socket.emit('action', ...)✅ I10
Inbox-Bell + Badge✅ I13
Club Board✅ I14
HUD zeigt displayName✅ I16
Replayer zeigt displayName✅ I15
Rebuy-Button (heute 1+addon)✅ I8 (Teil)Eingeschränkt; voller Ausbau in I18
Multi-Table-Toast / auto. Tisch-Wechsel→ I17
Wizard mit Modus-Auswahl (freeze/classic/re-entry)→ I18
Re-Entry-Button nach Bust-Out→ I18
Spielerliste in Lobby-Detail mit Live-Stack→ I19
Schematische + konkrete Auszahlungsstruktur→ I19

D.2 — Admin-Frontend

BereichStatusBeschreibung
Login (E-Mail)✅ I2
Kicks & Bans✅ I2Aus API
Chip-Verwaltung + Club-Konten✅ I3
Tisch-Monitor✅ I5/I6Aus 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
Chat-Moderation (Wortfilter, Mutes, Slow-Mode, Pin)✅ I12
Balancer-Monitoring (welche Tische, Differenzen)→ I17
Wizard mit Rebuy-Modus-Konfiguration→ I18

EAnhang E — Übergabe-Logs der abgeschlossenen Iterationen

I1Bootstrap & Auth2026-05-03

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

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

I2Bans und Admin-Skeleton2026-05-05

Lieferumfang: bans-Tabelle, 3-Stufen-Logik (D16), banGuard-Middleware, Admin-Endpoints, cleanup.worker-Skeleton, Ban-Check bei Socket.IO-Connect.

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

I3Accounting2026-05-05

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

Übergabezustand: Jede Registrierung legt automatisch Spieler-Konto an. Club-Konten beim Server-Start sichergestellt. Nullsummen-Invariante mit Rollback. 48 Tests.

I4Profile + Preferences2026-05-06

Lieferumfang: user_preferences-Tabelle (4 Blobs), BlobMigrationWorker, display-name.service.ts mit Cooldown, Avatar-Upload, PUT /api/profile/display-name, GET/PUT /api/users/me/preferences.

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

I5Tische und Sitzplätze2026-05-08

Lieferumfang: Schema tables, seats, seat_sessions. SeatStateMemory-Klasse (D20). Socket.IO table.join / table.leave. Bootstrap-Recovery.

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

I6Tournament-Skeleton + Sitzzuteilung2026-05-08

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

Lieferumfang: Schema tournaments, tournament_players, blind_levels. Endpoints POST/GET /tournaments/..., register, unregister, start mit Fisher-Yates. Manuelles Sitzen für Tournament-Tische blockiert (D22).

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

I7Tournament-Schedules und Serien2026-05-08

Lieferumfang: Schema tournament_series, tournament_schedules. schedule.service.ts mit calculateOccurrences() und generateInstances() (rolling 4-Wochen-Window). schedule.worker.ts. 7 Endpoints.

Übergabezustand: ScheduleWorker idempotent (hasInstance-Guard). 119 Tests.

I8Buyin und Payoutrekonstruiert

Lieferumfang: buyin.service.ts mit bookBuyin(), bookRebuy() (heutige 1+addon-Variante), bookAddon(), bookRefund(). payout.calculator.ts. verifyPrizePools().

Bekannte Lücken (Anlass für I18): Rebuy nur einmalig, kein Mid-Stack-Rebuy in echter Phase, kein Re-Entry nach Bust-Out.

I9Hand-Engine Corerekonstruiert

Lieferumfang: Schema hands, hand_board_cards (D21), hand_players, actions. deck.ts (crypto.randomBytes-Shuffle). hand.evaluator.ts. pot.calculator.ts (Side-Pots). hand.engine.ts (pure).

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

I10Action-Loop und Realtimerekonstruiert

Lieferumfang: game.gateway.ts Action-Handler, action.validator.ts, time.bank.service.ts, hand.service.ts. Memory-Sync bei Action; Snapshot bei Hand-Ende. Realtime-Broadcast state-update.

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

I11Showdown, Payouts, Bountyrekonstruiert

Lieferumfang: Showdown-Pfad, Side-Pot-Verteilung. Bounty-Verteilung. Tournament-Ende: Payout-Curve-Anwendung, Series-Punkte-Berechnung (D17a), final_place, prize_won.

Übergabezustand: Single-Table-Tournaments funktionieren End-to-End.

I12Chat und Moderation2026-05-19

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

Übergabezustand: Club-Chat + Tisch-Chat. Admin: Wortfilter, Mutes, Slow-Mode, Pin. 30 neue Tests.

I13Notifications und Inbox2026-05-19

Lieferumfang: inbox_items, popup_log. notification.service.ts. 4 Endpoints. Callbacks onUserRegistered, onRequestLoad, onUserApproved.

Übergabezustand: Registrierung → Admin-Inbox. Request-Load → Admin-Inbox. Approve → Inbox an User. 17 neue Tests.

I14Club Board und Admin-Polishing2026-05-19

Lieferumfang: club_board. 4 Endpoints. Admin: Pause/Resume-Buttons für Turniere. Tisch-Chat aus REST entfernt.

Übergabezustand: Club Board als Soft-Delete-Liste. 16 neue Tests.

Restpunkt: backup.worker.ts nicht implementiert. Bei Bedarf Mini-Iteration „I14b — BackupWorker".

I15Hand-History und Replayer2026-05-19

Lieferumfang: handhistory.repository.ts mit D7-Filter. replayer.service.ts mit getHandDetail(). Endpoints GET /api/hands/:id, GET /api/hands?table=.... Frontend-Replayer API-gesteuert mit convertApiHandToRpl().

Übergabezustand: Eigene Karten immer sichtbar; fremde nur bei shown_at_showdown=1. 15 neue Tests.

I16HUD und Series-Leaderboards2026-05-21

Lieferumfang: stats.flagger.ts (VPIP/PFR/3-Bet pro Hand-Action-Sequenz, D17b), stats.aggregator.ts, stats.routes.ts mit GET /api/stats/me, GET /api/stats/users/:id. Series-Leaderboard.

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