rebreak-monorepo/ops/GAMES_1V1_MIGRATION_PLAN.md
chahinebrini e76be7ee78 feat(profile): Profile-Page komplett + Header-Dropdown + UI-Pattern-Fixes
Profile (3 Iterationen):
- app/profile/index.tsx + components/profile/* (Header, StatsBar, Approved,
  Streak, UrgeStats, Demographics, DigaMissionBanner)
- echte Live-Daten via useMe-Hook (Avatar/Nickname/Plan/Email/Provider-Pill)
- Demographics mit echten Inputs (TextInput + Bottom-Sheet-Selects),
  debounced auto-save, Pro-Trial-Reward-Banner, Mikro-Why-Texte
- Approved Domains als plain integer (KEIN Plan-Slot/Cap)
- Friendly Hint-Text statt Progress-Bar (alignSelf:'stretch' Pattern)
- StatsBar zentriert mit 3 prominenten Cards (vertikale Dividers)
- Cooldown-Timeline als Liste mit 1px-Rail
- ApprovedDomainsList: Collapse-Chevron rechts in Title-Row (Pattern-Fix)
- Eigene vs fremde Profile-Ansicht streng getrennt (DSGVO/Anonymität)

Header-Dropdown (kein 3-Punkte-Icon):
- Avatar als Trigger im AppHeader (User-Wunsch)
- Custom-Modal beide Plattformen, Card-Style
- SOS prominent oben (nur Wort 'SOS' rot, Tagline 'wir sind für dich da' klein darunter)
- Profile/Settings/Games/Debug(__DEV__)/Logout
- Logout neutral (nicht rot — Recovery-tonal)
- AppHeader: neue showBack + title Props für Sub-Routes

Routes (Stub bis Phase C):
- app/profile/[userId].tsx — anonym (nur public-Stats)
- app/settings.tsx — Coming-Soon-Skeleton
- app/games.tsx — Standalone Games-Page mit GameCard-Grid
- app/debug.tsx — __DEV__-only

Game-Picker (Migration aus Nuxt):
- components/games/{GameCard, StarRating, GameRatingStars}
- 2x2 Grid, 56pt SVG-Icons (inline aus components/urge/gameSvgs.ts)
- Live-Backend /api/games/ratings (silent-fail)
- Re-use UrgeGames.tsx ohne TTS/Cooldown-Loop

UI-Pattern-Fixes (alle aus screenshot-User-Feedback 2026-05-07):
- Snake-Bug (food-pellet React-18-StrictMode-Reducer-double-call) gefixt
- Snake-Buttons platform-native (iOS-blue / Android-ripple)
- Tetris-Margins (16px paddingHorizontal)
- PostCard-Buttons Apple-44pt-Hit-Area (Image-Select, Image-Remove,
  Cancel, Share-Pill — via hitSlop)
- ProfileHeader Demographics-Hint: alignSelf:'stretch' Pattern
- ApprovedDomainsList Collapse: Title flex:1 + Chevron rechts
- ProtectionDetailsSheet FAQ-Items: alignSelf:'stretch' defensive
- AppHeader Back-Button: neue showBack-Prop + chevron-back

Memory + Plan-Docs:
- 17 Memory-Files dokumentieren System-Wissen + Patterns
- ops/{CUTOVER, UI_MIGRATION, PROFILE_PAGE, WEBHOOK, GAMES_1V1,
  RELEASE_READINESS, TESTING_STATE, MAESTRO_HOSTING}_*.md

Backend bleibt unverändert (Tier-LLM + Nickname + sort:latency
sind seit gestern deployed).
2026-05-07 18:22:58 +02:00

18 KiB

1v1 Games Migration Plan (Nuxt → rebreak-native)

Status: Recon abgeschlossen 2026-05-07. Read-only Analyse, kein Code-Touch.

Author scope: Migration der bestehenden Nuxt-1v1-Implementierung (TicTacToe + Memory) aus ~/mono/trucko-monorepo/apps/rebreak/ in die neue React-Native-App ~/mono/rebreak-monorepo/apps/rebreak-native/. Letzter Schritt vor finalem Nuxt-Cutover (DiGA).


1. Status quo Nuxt-Implementierung

1.1 Frontend (Vue/Nuxt)

Datei Zweck
~/mono/trucko-monorepo/apps/rebreak/app/pages/app/game/[challengeId].vue (802 LOC) Haupt-Game-Page. Lobby (Waiting), Live-Board für TicTacToe + Memory, Status, Lyra-Bubble, Tabs (History + Ranking), Rematch, Live-Share-Toggle. Subscribed Supabase-Realtime auf rebreak.game_challenges-Row.
~/mono/trucko-monorepo/apps/rebreak/app/components/sos/GameTicTacToe.vue Solo-Modus mit Lyra-AI. Enthält "Gegen echten Spieler"-Button (Z. 73-76) — POST /api/games/challenge + Redirect.
~/mono/trucko-monorepo/apps/rebreak/app/components/sos/GameMemory.vue Solo-Memory mit "Gegen echten Spieler"-Button (Z. 48-52) — POST /api/games/challenge-memory + Redirect.
~/mono/trucko-monorepo/apps/rebreak/app/components/CommunityPostCard.vue Rendert "Challenge annehmen"-Button für Community-Posts mit category="challenge" (Z. 288, 469-479).
~/mono/trucko-monorepo/apps/rebreak/app/stores/community.ts Pinia-Store, hält challengeId an Posts (Z. 8, 250).

File-Count Frontend: 5 relevante Vue-Files (1 Page + 2 Solo-Game-Components mit 1v1-Hook + 1 PostCard + 1 Store).

1.2 Backend (Nuxt-Server, Nitro)

Backend liegt nicht in einem separaten trucko-backend-Service, sondern im selben Nuxt-Projekt unter apps/rebreak/server/. Endpoints:

Endpoint File
POST /api/games/challenge server/api/games/challenge.post.ts (38 LOC) — TicTacToe-Challenge erzeugen + Community-Post
POST /api/games/challenge-memory server/api/games/challenge-memory.post.ts (62 LOC) — Memory-Challenge erzeugen (16 Karten, shuffled)
GET /api/games/challenge/[id] server/api/games/challenge/[id].get.ts (16 LOC) — Lade Challenge-State
POST /api/games/challenge/[id]/accept server/api/games/challenge/[id]/accept.post.ts (35 LOC) — Gegner tritt bei, Status: OPEN → ACTIVE
POST /api/games/challenge/[id]/move server/api/games/challenge/[id]/move.post.ts (109 LOC) — TicTacToe-Move; Win-Check, Score-Update, Post-Cleanup
POST /api/games/challenge/[id]/memory-move server/api/games/challenge/[id]/memory-move.post.ts (152 LOC) — Memory-Move (Flip/Match/Mismatch)
POST /api/games/challenge/[id]/rematch server/api/games/challenge/[id]/rematch.post.ts (64 LOC) — Neue Challenge mit Gegner pre-set, status=ACTIVE
POST /api/games/challenge/[id]/live-toggle server/api/games/challenge/[id]/live-toggle.post.ts (35 LOC) — isLive-Flag für Spectators
GET /api/games/history server/api/games/history.get.ts (44 LOC) — Spielhistorie (alle, oder vs Gegner)
GET /api/games/ranking server/api/games/ranking.get.ts (15 LOC) — Top-Spieler-Liste

File-Count Backend: 10 Endpoints, ~570 LOC.

1.3 DB-Schema

Aus ~/mono/trucko-monorepo/apps/rebreak/prisma/schema.prisma:

  • enum GameChallengeStatus (Z. 424): OPEN | ACTIVE | FINISHED | CANCELLED
  • model GameChallenge (Z. 433-452, Tabelle rebreak.game_challenges): id, challengerId, challengerName, opponentId, opponentName, status, board (TEXT, default ---------), currentTurn, winner, postId, gameType (default "tictactoe"), isLive, memoryState (Json), timestamps.
  • model GameScore (Z. 470, Tabelle rebreak.game_scores): userId PK, playerName, wins, losses, draws, points (3 für Sieg, 1 für Unentschieden).
  • model GameRating (Z. 483) und GameHighScore (Z. 496) — gehören zum Solo-Mode, irrelevant für 1v1, aber bereits portiert.

Migrations-SQL:

  • ~/mono/trucko-monorepo/apps/rebreak/prisma/migrations/add_game_challenges.sql — Enum, Table, Indexes, ALTER PUBLICATION supabase_realtime ADD TABLE rebreak.game_challenges
  • ~/mono/trucko-monorepo/apps/rebreak/prisma/migrations/add_game_challenges_rls.sql — RLS-Policies (read/insert/update für challenger + opponent via auth.uid())
  • Spätere Patches haben gameType, isLive, memoryState hinzugefügt (in den live-DB Tabelle vorhanden, kein eigenes Migration-File gefunden — Schema-Drift-Verdacht in Nuxt).

1.4 State-Sync-Mechanismus (1 Satz)

Server-authoritative State in Postgres (rebreak.game_challenges-Row); Frontend mutiert via REST-POST und subscribed parallel auf Supabase-Realtime postgres_changes UPDATE-Events der eigenen Row → keine Polling, keine WebSocket-Eigenbau.

1.5 Datenflussdiagramm (ASCII)

        Spieler A (Challenger)                       Spieler B (Opponent)
       ─────────────────────                        ─────────────────────
              │                                              │
   POST /api/games/challenge                                 │
              │                                              │
              ▼                                              │
   ┌──────────────────────┐                                  │
   │  game_challenges     │   communityPost.challengeId      │
   │  status=OPEN         │◀────────────────────────────────┐│
   │  board=---------     │                                 ││
   └──────────┬───────────┘                                 ││
              │                                              ▼
              │                                    GET /api/community/posts
              │                                    (sees challenge card)
              │                                              │
              │                  POST /api/games/challenge/[id]/accept
              │                                              │
              ▼                                              ▼
   ┌────────────────────────────────────────────────────────────┐
   │  game_challenges  status=ACTIVE  opponent_id=B             │
   └──────────────────────────┬─────────────────────────────────┘
                              │
                              │  Supabase Realtime (postgres_changes)
                              │  channel = `game:<id>:<ts>`
                              │  filter = id=eq.<challengeId>
                              ▼
              ┌──────────────────────────────┐
              │ both clients update UI       │
              └──────────┬───────────────────┘
                         │
        loop until FINISHED:
              │
   POST /api/games/challenge/[id]/move (or memory-move)
              │
              ▼
   ┌────────────────────────────────────────────────────────────┐
   │  Server validates turn + writes new board / memoryState    │
   │  on win/draw → upsert game_scores, delete community post  │
   └──────────────────────────┬─────────────────────────────────┘
                              │ Realtime UPDATE → both clients
                              ▼
              ┌──────────────────────────────┐
              │ FINISHED screen + Rematch    │
              └──────────────────────────────┘

2. Migration-Plan

Phase A — Backend-Endpoints in rebreak-monorepo

Status: BEREITS PORTIERT. Verifiziert per diff:

  • ~/mono/rebreak-monorepo/backend/server/api/games/challenge.post.ts ist byte-identisch mit Nuxt.
  • ~/mono/rebreak-monorepo/backend/server/api/games/challenge/[id]/move.post.ts ist byte-identisch.
  • Alle 10 Endpoints existieren bereits unter ~/mono/rebreak-monorepo/backend/server/api/games/.

Aufwand Phase A: 0 h. Nur ein leichter Smoke-Test (curl Request mit Bearer-Token gegen den staging-Nitro) zur Bestätigung dass die Endpoints im Nitro-Prod-Build aktiv sind.

Phase B — DB-Migrations für game_sessions

Status: BEREITS PORTIERT. Schema verifiziert:

  • enum GameChallengeStatus in ~/mono/rebreak-monorepo/backend/prisma/schema.prisma Z. 424 vorhanden.
  • model GameChallenge, GameScore, GameRating, GameHighScore alle vorhanden.

Offene Punkte (klein):

  1. SQL-Migration unter backend/prisma/migrations/ muss verifiziert werden — sind gameType, isLive, memoryState-Spalten in einer eigenen Migration angelegt? Falls nein: ein konsolidiertes add_game_challenges.sql nachziehen.
  2. RLS-Policies und ALTER PUBLICATION supabase_realtime ADD TABLE rebreak.game_challenges müssen am Staging-DB-Cluster bestätigt werden (gleicher DB für Nuxt + RN-Backend, also vermutlich schon aktiv).

Aufwand Phase B: 1-2 h (SQL-Audit + ggf. ein Catch-up-Migration-File).

Phase C — RN-UI-Komponenten

Status: KOMPLETT NEU. RN-App hat aktuell:

  • apps/rebreak-native/components/urge/UrgeGames.tsx (1067 LOC) — Solo-Mode für Memory/TicTacToe/Snake/Tetris.
  • apps/rebreak-native/app/games.tsx — Standalone-Games-Page (Solo).
  • KEIN Community-Komponent, KEIN Game-Page für 1v1.

Zu erstellen:

  1. apps/rebreak-native/app/(app)/game/[challengeId].tsx — Pendant zu pages/app/game/[challengeId].vue. RN-Expo-Router-File. Ports:

    • Loading + Lobby (OPEN-Status, Waiting-Screen mit Cancel-Button)
    • TicTacToe-Board (3x3 Grid, X/O-Marker, WinLine-Highlight) — Pressable-Cells statt <button>
    • Memory-Board (4x4 Grid, Score-Header, Mismatch-Reveal, Progress-Bar)
    • Lyra-Bubble (Avatar + animierter Phrase-Text, optional TTS-Toggle — bereits vorhandene lib/sosTtsQueue.ts-Infra wiederverwendbar)
    • Status/Result-Section + Rematch-Button
    • History-Tab + Ranking-Tab
  2. apps/rebreak-native/components/games/Game1v1Board.tsx (optional, falls zu monolithisch) — Sub-Component für Board-Rendering.

  3. 1v1-Entry-Buttons in UrgeGames.tsx — analog Vue, pro TicTacToe und Memory Solo-Mode einen "Gegen echten Spieler"-Button hinzu, der POST /api/games/challenge[-memory] callt und auf /game/[id] navigiert.

  4. Community-Listing-View — Aktuell hat RN-App keine Community-Tab. Entweder:

    • Option a: Existing community-Page aus Nuxt nach RN portieren (separater großer Task).
    • Option b: Erstmal nur eine "Open Challenges"-Liste unter /game/index.tsx, die alle OPEN-Challenges (eigener Endpoint nötig: GET /api/games/challenges?status=OPEN) listet.
    • Option c (empfohlen): Direkter Invite-Flow per Share-Link /game/[id] (Deep-Link funktioniert bereits in Expo) — kein Community-Browsing nötig für DiGA-Cutover.

Aufwand Phase C: 12-20 h (1 Page mit 2 Game-Modes + Realtime + Lyra + History-Tab + Ranking-Tab + Lobby-Flow). Größte Position.

Phase D — Realtime-Wiring

Status: INFRA VORHANDEN. apps/rebreak-native/lib/supabase.ts hat bereits realtime-Konfig.

Zu tun:

  • Im neuen [challengeId].tsx analog zu Vue: supabase.channel(...).on('postgres_changes', { schema: 'rebreak', table: 'game_challenges', filter: id=eq.${id} }).
  • React-Native-Spezifika: AppState-Listener für Reconnect bei Background → Foreground (Vue-Variante hat nur passive Reconnect bei CHANNEL_ERROR).
  • Auth-Token via supabase.realtime.setAuth(session.access_token) — identisch zu Vue.

Aufwand Phase D: 2-3 h (im Rahmen Phase C).

Phase E — Testing + Deploy

  • Manueller 2-Device-Test (iOS-Simulator + Android-Emulator simultan): Challenge erstellen, accepten, abwechselnd Züge, Win/Draw, Rematch.
  • Disconnect-Resilience: Airplane-Mode toggle während ACTIVE — Realtime muss reconnecten.
  • Deep-Link-Test: rebreaknative://game/<id> aus geteiltem Link.
  • EAS-Preview-Build für Tester.

Aufwand Phase E: 4-6 h (inkl. Bugfixes).

Gesamtaufwand: ~20-30 h Net-Coding.


3. Architektur-Empfehlung — was wir besser machen

Die Nuxt-Implementierung ist solide (server-authoritative + Supabase-Realtime ist die richtige Wahl). Drei Verbesserungen für die RN-Variante:

  1. Kein Community-Post-Coupling. Die Nuxt-Variante erstellt für jede Challenge automatisch einen Community-Post (category="challenge") und löscht ihn beim Spielende. Das verschmiert Game-Lifecycle und Community-Layer. Empfehlung: 1v1-Challenges leben in einer eigenen Tabelle / einem eigenen "Open-Challenges"-Endpoint, ohne Cross-Coupling. Macht Phase-C-Cleanup einfacher und entkoppelt RN-Cutover von Community-Migration.

  2. Optimistic UI mit Rollback. Aktuell wartet Vue auf den POST-Response, bevor das Board updated → 100-300 ms Lag pro Move. RN-Variante: lokal sofort renderen (gleiche Validierungslogik clientseitig spiegeln) und auf Realtime-UPDATE reconcilen. Bei Validation-Error vom Server: rollback + Toast. Macht das Spiel "snappier" auf flackrigen Mobilfunk-Verbindungen.

  3. Heartbeat / Idle-Cancel. Nuxt hat keinen Cleanup für tote OPEN-Challenges. RN-Variante: Cron-Job im Backend (backend/server/api/cron/-Pattern existiert bereits) markiert OPEN-Challenges nach 30 min als CANCELLED, und ACTIVE-Challenges ohne Move seit 10 min ebenfalls. Entlastet die DB und verhindert Geister-Posts.

  4. Bonus: gameType zum Enum machen statt String @default("tictactoe") — kleine Schema-Hygiene.


4. Risk-Assessment

Risk Severity Mitigation
Realtime-Latenz auf Mobilfunk mittel Optimistic UI (siehe oben). 100-500 ms ist für TicTacToe/Memory ok (Async-Style).
Anti-Cheat / Move-Spoofing niedrig Server-authoritative ist bereits implementiert (Server prüft Turn + überprüft Board-State). RLS-Policies erlauben Updates nur für Teilnehmer — aber Updates passieren ohnehin via Service-Role-Backend, RLS ist nur für Realtime-Read.
Disconnect mid-game mittel Aktuell: Spiel "hängt" bis Spieler zurückkommt; kein Auto-Forfeit. Risk: Spieler quittet → Gegner stuck. Mitigation: Heartbeat + Auto-Cancel nach 10 min Inaktivität (Phase E Verbesserung) + UI-"Gegner offline"-Badge.
Plan-Tier-Gate niedrig Aktuell kein Plan-Check in den Endpoints — alle Auth-User können challengen. Falls Pro-Only gewünscht: in challenge.post.ts und challenge-memory.post.ts ein requirePlan('pro') einfügen (User-Decision).
Schema-Drift Nuxt vs Rebreak-Monorepo mittel Beide Projekte teilen physisch dieselbe DB (Schema rebreak). Solange beide Schemas synchron sind, kein Problem. Beim Cutover: Nuxt komplett deaktivieren, sonst race conditions auf game_challenges.
Realtime-Quota-Kosten niedrig Supabase Realtime hat ~200 concurrent connections im Pro-Plan. Bei 1v1 = 2 Subscriber/Match → erst ab 100 parallelen Matches problematisch. Monitoring per Supabase-Dashboard.
Memory-Game state.cards Größe niedrig 16 Cards JSON-Blob ~1 KB pro UPDATE → vernachlässigbar.
iOS Background WebSocket mittel iOS killed Realtime-Channels nach ~30 s im Background. Reconnect-on-resume zwingend (AppState-Listener).

Top-3 Risks: 1) Disconnect mid-game ohne Auto-Forfeit; 2) iOS Background-WebSocket-Drop; 3) Schema-Drift während Übergangsphase Nuxt+RN parallel.


5. Open Questions an User

  1. Live oder Async? Aktuelle Nuxt-Implementierung ist Live (Realtime + Same-Session). Empfehlung für RN: Live beibehalten, weil Netcode steht. Async-Wordle-Style würde komplette Re-Architektur erfordern (Push-Notifications, Move-Queue) und 2-3x Aufwand bedeuten.

  2. Random-Matchmaking vs Friends-only vs Community-Post? Nuxt nutzt Community-Post (jeder kann annehmen, kein Friend-Graph). Optionen für RN:

    • a) Community-Post-Style portieren (braucht Community-View in RN — großer separater Task).
    • b) Public "Open Challenges"-Liste auf /games-Page (klein, schnell).
    • c) Share-Link-Invite (rebreaknative://game/<id> per Native-Share-Sheet — kein Browse nötig).
    • d) Random-Pool: Server-Side-Matchmaking (setzt 2 OPEN-Challenges paarweise zusammen — kein UI-Touch). → User muss entscheiden. Empfehlung: c + b kombiniert für MVP.
  3. Public Leaderboard mit Win-Rate-Stats? GameScore-Tabelle existiert + /api/games/ranking-Endpoint. Frage: soll Leaderboard

    • a) in jedes Game integriert (aktueller Nuxt-Stand: Tab im [challengeId].vue),
    • b) als globale Page /games/leaderboard,
    • c) beides?
  4. Plan-Tier-Gate? 1v1 für Free-Tier verfügbar oder Pro-Only? (DiGA-Relevanz unklar.)

  5. Anonymität: Sollen Gegnernamen anonymisiert sein? Aktuell wird nickname || username || "Anonym" benutzt. DiGA-Datenschutz?

  6. User-Quit-Verhalten: Wenn ein Spieler die App schließt mid-game — Forfeit nach X min, oder Spiel hängt offen? Empfehlung: Auto-Cancel nach 10 min Inaktivität, kein Forfeit (= kein Punkteabzug).


6. Migration-Aufwand-Summary

Phase Aufwand Status
A — Backend-Endpoints 0 h bereits portiert
B — DB-Migrations 1-2 h Schema da, SQL-Audit nötig
C — RN-UI Game-Page + Lobby 12-20 h komplett neu
D — Realtime-Wiring 2-3 h im Rahmen C
E — Testing + Deploy 4-6 h manuelle 2-Device-Tests
Gesamt ~20-30 h

Plus optional: Community-View-Migration (separater Plan) für Community-Post-Style-Matchmaking.


Doc-Version 1.0 — 2026-05-07 — ~/mono/rebreak-monorepo/ops/GAMES_1V1_MIGRATION_PLAN.md