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

260 lines
18 KiB
Markdown

# 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`*