Sheets via neuer KeyboardAwareSheet-Composable (in Modal pattern, auto-grow mit Tastatur, paddingBottom-Lift): EditMail, AddDomain, CreateRoom, ConnectMail. GameOverScreen behält Spring-Slide-In, nutzt RN Keyboard.addListener für Lift. - KeyboardAwareSheet.tsx — universal modal with sheet-grow + keyboard-padding - react-native-keyboard-controller installiert + KeyboardProvider in Root - Snake: time + ScoreProgressBar + useSnakeSounds (haptic, audio TODO) - Tetris: title weg, Buttons zentriert, kein Pressable mit style-fn - DPad-Buttons 60→48, more bg, no scale - useMe: pub-sub listener pattern für app-weite avatar/nickname-Updates - dm.tsx: resolveAvatar wrap (iron.png-Warning) - Mail-error-humanizer + locales Recovery-Doc-Update in docs/internal/RECOVERY_LOG_2026-05-10.md Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
15 KiB
Recovery-Log 2026-05-10 — Lost Work + Workflow-Regeln
Stand: 2026-05-10
Verantwortlich: Chahine
Anlass: verlorene UI-Arbeit nach mehrfachen git stash/cherry-pick-Zyklen am 9. Mai
1. Was passiert ist (Timeline)
1.1 Auslöser — Cutover-Incident 7. Mai 22:17
apps/rebreak/ (Nuxt) → backend/ (Standalone Nitro) Cutover. Force-Push aus dem neuen Mac-Repo zu RaynisDev/rebreak.git triggerte den Server-Webhook, der scheiterte:
cd /srv/rebreak/apps/rebreak-Pfad existierte im neuen Layout nicht- Auth-Middleware crashed mit HTTP 500 (
Cannot read properties of undefined (reading 'url')) weilbackend/nitro.config.ts.runtimeConfigkeinesupabase-Section hatte - ALLE authentifizierten Endpoints kaputt
Rollback: git reset --hard origin/main → HEAD auf 922d5dc. Tag pre-revert-2217 als Sicherung gesetzt.
Siehe ops/CUTOVER_PLAN.md §1.3 für volle Incident-Beschreibung.
1.2 Folgesymptom — Stash-Hopping am 9. Mai
Nach dem Reset arbeitete der User intensiv am Cherry-Pick-Workflow zwischen main und upgrade/sdk-54. Reflog zeigt 10+ Branch-Switches in 4 Stunden (14:51–18:11). Pattern:
commit auf upgrade/sdk-54
→ checkout main
→ cherry-pick (selber Commit, neuer Hash)
→ checkout upgrade/sdk-54
→ uncommitted changes: git stash
→ ... nächster Commit ...
git fsck --no-reflogs --lost-found zeigt 9 dangling WIP-Stash-Commits als Resultat:
wip-pre-cherrypick, wip-pre-daemon-fix, wip-pre-daemon-push, wip-pre-backend-push-2, wip-pre-speak-fix-2, wip-mdm-session, plus 3× wip: sdk-54 ui/backend changes.
1.3 Konkreter Verlust — Commit 35189b9 "wip-pre-cherrypick"
Am 9. Mai 17:57 wurde ein Stash mit gerade fertiggestellter UI-Arbeit angelegt — der Stash-Apply lief nicht sauber zurück (Conflict + git checkout . zum Aufräumen, oder git stash drop ohne saubere pop). Die Arbeit landete als dangling Merge-Commit 35189b9, war aber im Working Tree weg.
Was im Stash war:
| File | Was wäre drin gewesen |
|---|---|
components/games/GameOverScreen.tsx |
256 → 468 Zeilen: StarRating, RiveAvatar, tier-aware Lyra-Messages, Rating-Form, Share-to-Community |
components/urge/UrgeGames.tsx |
Header-Refactor, scoreLabel/goodScore-Props |
app/settings.tsx |
LanguageIcon-Block, dynamic icon-rendering |
locales/de.json + en.json |
gameOver.lyra_title_* / lyra_body_* Keys (record/good/ok/low), Rating-Strings, Share-Strings |
User hat das nach 24h beim Test gemerkt: SOS sah aus „wie alter Stand" — kein neuer GameOverScreen, kein Snake-Score-Dashboard, OpenAI-TTS statt ElevenLabs.
2. Recovery-Aktion 2026-05-10
35189b9 Files via git checkout 35189b9 -- <pfad> ins Working Tree zurückgeholt. Locales chirurgisch gemerged (Python-Script für gameOver-Section only — andere Sections — Mail-Status, Auth-Errors etc. — blieben unangetastet, weil aktuelle de.json/en.json neuere Strings enthielten die in 35189b9 nicht waren).
Plus: urge.tsx + lib/sosTtsQueue.ts umgestellt von endpointForProvider(currentProvider()) (alter TtsProviderToggle-Pfad mit OpenAI-Default) auf /api/coach/speak — der tier-aware Backend-Dispatcher (siehe §5).
Backend (backend/server/api/coach/speak.post.ts) war bereits korrekt fertig (mtime 10. Mai 16:18, Plan-aware: Free→Google / Pro→Cartesia / Legend→ElevenLabs) — kein Touch nötig.
3. Was JETZT NOCH FEHLT (nicht aus 35189b9 wiederhergestellt)
| Feature | Status | Notiz |
|---|---|---|
| Game-Sharing-Post | offen | Aus User-Erinnerung: Game-Result-Sharing zur Community (Post mit Score + Lyra-Caption). Nicht in 35189b9 enthalten — wahrscheinlich anderer dangling stash. TODO separat. |
| TtsProviderToggle Wiring | offen, aber nicht kritisch | Component existiert (components/urge/TtsProviderToggle.tsx), nirgends gerendert. Laut ops/UI_MIGRATION_PLAN.md §3 Tab 5 Debug gehört der in __DEV__-Tab. |
Game-Sharing kommt in nächster Session. Anderen dangling stash-Commits prüfen via git show <sha> aus git fsck --lost-found Output.
4. Workflow-Regeln gegen Wiederholung
4.1 KEIN rapides Stash + Cherry-Pick mehr
Verboten: mehrere git stash hintereinander während Branch-Switching. git stash list darf nie länger als 1 Eintrag werden.
Stattdessen (in Priorität):
-
git worktree add— zweiter Working-Tree für andere Branches:git worktree add ../rebreak-main main # Cherry-pick im 2. Worktree, kein stash nötig cd ../rebreak-main git cherry-pick <sha> git push cd ../rebreak-monorepoBeide Trees sind unabhängig, parallel benutzbar in zwei IDE-Fenstern.
-
Commit-First-Pattern — vor jedem
checkoutimmer committen (auch WIP-commits sind besser als stash):git add -A && git commit -m "wip: in progress" git checkout main # ... arbeit auf main ... git checkout upgrade/sdk-54 # WIP commit unverloren, kann amended werden -
NIE
git stash drop— nurgit stash pop. Wenn pop conflicted: NICHT mitgit checkout .aufräumen, sondern Conflict-Markers manuell auflösen + committen.
4.2 Recovery-Kommandos für die Zukunft
Falls trotzdem mal wieder Arbeit verloren geht:
# Alle dangling commits auflisten:
git fsck --no-reflogs --lost-found
# Inhalt eines Commits inspizieren:
git show <sha> --stat
# Files aus einem dangling commit zurückholen (ohne git history zu touchen):
git checkout <sha> -- <pfad/zur/datei>
# Volles Reflog mit Datum:
git reflog --date=format:"%Y-%m-%d %H:%M"
Tag pre-revert-2217 (vom 7. Mai) bleibt als Notbremse-Anker erhalten.
4.3 Zwei Branches gleichzeitig sind ein Anti-Pattern
Aktuell: main (Production) + upgrade/sdk-54 (Dev). Cherry-Pick-Pflicht zwischen beiden ist die eigentliche Wurzel des Problems.
Empfehlung (User-Decision): sobald upgrade/sdk-54 stabil ist → main durch upgrade/sdk-54 ersetzen (force-push) und nur noch einen Branch fahren. Die GH-Actions-Pipeline deployt von main, also nach Force-Push ist alles auf einem Branch konsolidiert.
5. Tier-Aware TTS-Architektur (jetzt aktiv)
Damit klar ist wie das System nach dem Recovery funktioniert:
User auf SOS-Page (urge.tsx)
→ ttsQueue.endpoint = '/api/coach/speak'
→ POST /api/coach/speak { text, mode: 'sos' }
→ Backend: speak.post.ts
→ requireUser(event)
→ profile.plan aus DB
→ free → speakGoogle() (60s/day quota)
→ pro → speakCartesia() (300s/day quota)
→ legend → speakElevenLabs() (unlimited)
→ Backend liefert raw audio/mpeg stream
→ Client erwartet immer raw audio/mpeg (kein isGoogleCloud-Branch mehr nötig)
Wichtig: Kein User-Toggle. Der Provider hängt ausschließlich an profile.plan. Wenn ein Pro-User Cartesia-Stimme nicht mag → Plan-Tier muss geändert werden, nicht ein Toggle.
TtsProviderToggle.tsx Component bleibt im Repo aber ohne Wiring. Falls Debug-Tab gebaut wird (UI_MIGRATION_PLAN.md §3 Tab 5), kommt der Toggle dort hin (__DEV__-only).
6. Game-Flow in SOS vs Standalone
| Mode | Eintritt | Game-Over-Verhalten |
|---|---|---|
SOS-Mode (urge.tsx) |
aus Lyra-Chip „Spiel" | Game endet → onComplete(score) direkt → SOS-Session läuft weiter, Lyra antwortet auf Score. KEIN GameOverScreen. |
Standalone-Mode (games.tsx) |
aus Header-Dropdown „Games" | Game endet → <GameOverScreen /> rendert mit StarRating + Lyra-Message + Share-to-Community-Button. Retry/Exit drinnen. |
Implementation: mode: 'sos' \| 'standalone'-Prop auf SnakeGame/MemoryGame/TicTacToeGame/TetrisGame. Default = 'standalone'. urge.tsx setzt explizit mode="sos".
7. Keyboard-Overlap — generische Lösung
App-übergreifender Bug: TextInput wird beim Tippen vom Keyboard verdeckt (Mail-Password-Edit, Auth-Forms, Profile-Edit, Demographics, ComposeCard, Chat-Input, etc.).
Aktiver Stack ab 2026-05-10: react-native-keyboard-controller — de-facto Standard seit 2024 für RN-Keyboard-Avoidance. Native Synced (iOS-Curve pixel-genau), kein Driver-Mix, kein Bouncing.
Setup:
- Bereits installiert:
pnpm add react-native-keyboard-controller✓ - Root-Layout wrapped mit
<KeyboardProvider>(apps/rebreak-native/app/_layout.tsx) ✓ - Native-Build nötig nach Install:
cd apps/rebreak-native/ios && pod install(Autolinking macht den Rest), dann frischer Xcode-Build
Migrierte Components (Reference-Beispiele):
EditMailAccountSheet.tsx—useKeyboardAnimation()+Animated.subtract(slideY, height)GameOverScreen.tsx— gleiche Pattern, mit Spring-Slide-In bewahrt
7.1 Wann was nutzen
Empfehlung in dieser Reihenfolge:
| Situation | Lösung |
|---|---|
| Bottom-Sheet mit Form/Input (EditMail, ConnectMail, AddDomain, GameOver, künftig…) | <KeyboardAwareSheet> Composable (components/KeyboardAwareSheet.tsx). Kapselt Modal + Backdrop + Slide-In + Sheet-Grow + Form-an-Bottom-Spacer. Beispiel: EditMailAccountSheet.tsx. |
| Vollbild-Form (Auth, Profile-Edit, Signup) | <KeyboardAvoidingView /> aus react-native-keyboard-controller (NICHT von RN!) als Outermost. Drop-in, funktioniert wie erwartet. |
| Sticky-Bottom-Bar über Tastatur (Send-Button am Screen-Edge, etc.) | <KeyboardStickyView /> aus der Library — sticked automatisch über Tastatur. |
| Chat/SOS (FlatList + Input-Bar) | Wie bisher in PostCommentsSheet.tsx. Funktioniert weiter. |
| Legacy | hooks/useSheetKeyboardLift.ts + hooks/useKeyboardHeight.ts + components/KeyboardAdjustedView.tsx bleiben im Repo aber sollten nicht mehr neu verwendet werden — durch <KeyboardAwareSheet> ersetzt. |
7.1.1 Auto-sized Sheets (kein leerer Platz unterhalb des Inhalts)
Für kompakte Forms (1 Input + Save-Button — z.B. EditMailAccountSheet): KEINE feste height setzen, Sheet auto-sized via position: 'absolute', bottom: 0. useSheetKeyboardLift({ offscreenY: SCREEN_HEIGHT }) für initial-off-screen + Keyboard-Lift. Resultat: Sheet sitzt eng über der Tastatur ohne weißen Leerraum darunter.
Für Sheets mit variablem Listen-Inhalt (Comments, längere Forms): height setzen. ScrollView braucht constrained height zum scrollen.
7.1.2 Library-Migration-Pfad: react-native-keyboard-controller
De-facto-Standard seit 2024 für Keyboard-Avoidance in RN. Löst alle Pain-Points (Driver-Mix, iOS-Modal-Quirks, Sheet-Lifts, smooth Animationen) systemisch über native Module — kein eigener Animated-Code mehr nötig. Kostet:
pnpm add react-native-keyboard-controllernpx expo prebuild+ iOS pod install (= neuer Native-Build nötig)- Wrapper am App-Root:
<KeyboardProvider> - Components ersetzen:
<KeyboardAvoidingView />von der Library statt RN's eigenes - Plus:
useKeyboardAnimation()Hook für custom Animationen
Empfehlung: wenn 2-3 weitere Sheets/Forms Probleme machen → migrieren. Bis dahin: useSheetKeyboardLift() Pattern reicht für die meisten Fälle.
7.2 Anti-Pattern zu vermeiden
<KeyboardAvoidingView behavior="padding">— funktioniert nur in Modals zuverlässig, bricht bei Full-Screens mitpaddingTop: insets.top. Nicht mehr neu nutzen.- Pressable mit style-Funktion für Buttons mit kritischem Visual:
style={({pressed}) => ...}schluckt manchmal Style-Properties (RN-Quirk). Für Buttons mit solidem BG + Border lieber<TouchableWithoutFeedback><View style={...}>Pattern. - Driver-Mix auf einem
<Animated.View>— z.B.height: animatedValue(JS-driver) zusammen mittransform: [{ translateY: animatedValue }](native driver). Crashed mit"Style property 'height' is not supported by native animated module". Lösung:useSheetKeyboardLift()Composable nutzt nur translate (beides native). marginBottom: keyboardHeightals JS-Style + native transform im selben View → Bouncing weil zwei Threads layouten. Lösung: Animated.subtract(slideY, keyboardLift), beides Animated.Values, native driver konsistent.
8. Open Issues (zukünftige Sessions)
8.1 Aus aktueller Session 2026-05-10 verschoben
- Game-Sharing-Post-Render — soll genau wie in
trucko-monorepo/apps/rebreak/app/components/CommunityPostCard.vue(category=game_share) aussehen. Aktuell rendert die Native-App einen generischen Post statt einer Game-Share-Card mit Score-Pill + Lyra-Caption + Challenge-CTA. Source-of-truth: Vue-Pendant. - Mail-Page-Chart —
MailWeeklyChart.tsxist bereits angelegt aber Render-Logic noch nicht 1:1 vom Nuxt-mail-stats-chart.vueportiert. 7-Tage-Bar-Chart mitaccount_*-Stats. - iron.png-Warning vollständig fixen —
dm.tsxist gefixt, aberroom.tsx(3 Stellen: 308, 537, 598) undcomponents/chat/RoomCard.tsx:52nutzen noch rawroom.avatarUrl/m.avatar. Wenn das vom Backend ein Avatar-ID statt URL liefert, gleicher Bug.resolveAvatar()darüber wickeln. - TetrisActionBtn / DPadBtn rollout — falls noch andere Stellen Pressable-mit-style-funktion nutzen für Game-relevante Buttons, gleichen TouchableWithoutFeedback-Pattern anwenden.
8.2 Aus voriger Session
KeyboardAdjustedViewrollout über alle TextInput-Stellen (siehe Liste in §9)- TtsProviderToggle in
__DEV__-Debug-Tab einbauen - Single-Branch-Konsolidierung:
upgrade/sdk-54→mainForce-Push - Andere 8 dangling stashes inspizieren ob noch was Wertvolles drin ist
- expo-av Deprecation Warning — Migration zu
expo-audio+expo-video(SDK 54 Pflicht). Tracker.
8.3 Snake-Sounds — Audio-Files droppen
hooks/useSnakeSounds.ts läuft aktuell im Haptic-only-Mode. Für echten 8-Bit-Retro-Sound:
apps/rebreak-native/assets/sounds/Dir anlegen- 4 kurze Audio-Files reinlegen (Free-Quellen: freesound.org, opengameart.org/content/8-bit-sound-pack, sfxr.me):
snake-eat.mp3~80ms tonale "blip"snake-move.mp3~30ms Tick (optional)snake-gameover.mp3~400ms abfallende Tönesnake-record.mp3~600ms aufsteigender Chime
useSnakeSounds.tsöffnen,require()undAudio.Sound.createAsync()Lines unkommentieren (in der Datei-Doku exakt beschrieben)
Nach Drop fallen die Haptics nicht weg — Audio + Haptic feuern dann beide.
8.4 Cache-Invalidierung — neuer Pattern in useMe.ts
Profile-Avatar-/Nickname-Änderungen sind jetzt app-weit live: nach jedem PATCH /api/auth/me muss invalidateMe() aus hooks/useMe.ts aufgerufen werden (oder reload() einer useMe-Instanz, was intern gleichbedeutend ist). Alle anderen useMe-Konsumenten (AppHeader, ComposeCard, PostCard, NotificationsDropdown, …) re-fetchen via Listener-Subscribe automatisch — kein App-Reload mehr nötig.
Dasselbe Pattern für andere User-Daten (Streak, Demographics, Devices) wenn das gleiche Bug-Symptom auftritt.
9. Files mit TextInput (für KeyboardAdjustedView-Rollout)
app/room.tsx
app/lyra.tsx
app/urge.tsx
app/(auth)/signup.tsx
app/(auth)/signin.tsx
app/(auth)/forgot-password.tsx
app/(auth)/confirm-otp.tsx
app/profile/edit.tsx
components/PostCommentsSheet.tsx ← bereits korrekt (Vorbild-Pattern)
components/ComposeCard.tsx
components/chat/CreateRoomSheet.tsx
components/chat/ChatInput.tsx
components/mail/ConnectMailSheet.tsx
components/mail/EditMailAccountSheet.tsx ← User-explizit gemeldet
components/blocker/AddDomainSheet.tsx
components/urge/InlineRatingDrawer.tsx
components/urge/SosFeedbackModal.tsx
components/urge/ShareSuccessDrawer.tsx
components/games/GameOverScreen.tsx