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).
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 | CANCELLEDmodel GameChallenge(Z. 433-452, Tabellerebreak.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, Tabellerebreak.game_scores): userId PK, playerName, wins, losses, draws, points (3 für Sieg, 1 für Unentschieden).model GameRating(Z. 483) undGameHighScore(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 viaauth.uid())- Spätere Patches haben
gameType,isLive,memoryStatehinzugefü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.tsist byte-identisch mit Nuxt.~/mono/rebreak-monorepo/backend/server/api/games/challenge/[id]/move.post.tsist 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 GameChallengeStatusin~/mono/rebreak-monorepo/backend/prisma/schema.prismaZ. 424 vorhanden.model GameChallenge,GameScore,GameRating,GameHighScorealle vorhanden.
Offene Punkte (klein):
- SQL-Migration unter
backend/prisma/migrations/muss verifiziert werden — sindgameType,isLive,memoryState-Spalten in einer eigenen Migration angelegt? Falls nein: ein konsolidiertesadd_game_challenges.sqlnachziehen. - RLS-Policies und
ALTER PUBLICATION supabase_realtime ADD TABLE rebreak.game_challengesmü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:
-
apps/rebreak-native/app/(app)/game/[challengeId].tsx— Pendant zupages/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
- Loading + Lobby (
-
apps/rebreak-native/components/games/Game1v1Board.tsx(optional, falls zu monolithisch) — Sub-Component für Board-Rendering. -
1v1-Entry-Buttons in
UrgeGames.tsx— analog Vue, pro TicTacToe und Memory Solo-Mode einen "Gegen echten Spieler"-Button hinzu, derPOST /api/games/challenge[-memory]callt und auf/game/[id]navigiert. -
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 alleOPEN-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].tsxanalog 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:
-
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. -
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.
-
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. -
Bonus:
gameTypezum Enum machen stattString @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
-
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.
-
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.
-
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?
- a) in jedes Game integriert (aktueller Nuxt-Stand: Tab im
-
Plan-Tier-Gate? 1v1 für Free-Tier verfügbar oder Pro-Only? (DiGA-Relevanz unklar.)
-
Anonymität: Sollen Gegnernamen anonymisiert sein? Aktuell wird
nickname || username || "Anonym"benutzt. DiGA-Datenschutz? -
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