rebreak-monorepo/docs/internal/RECOVERY_LOG_2026-05-10.md
chahinebrini 5d6c322129 wip: KeyboardAwareSheet migrations + Snake/Tetris UI + iron.png + useMe live-update
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>
2026-05-10 23:59:25 +02:00

15 KiB
Raw Blame History

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')) weil backend/nitro.config.ts.runtimeConfig keine supabase-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:5118: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):

  1. 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-monorepo
    

    Beide Trees sind unabhängig, parallel benutzbar in zwei IDE-Fenstern.

  2. Commit-First-Pattern — vor jedem checkout immer 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
    
  3. NIE git stash drop — nur git stash pop. Wenn pop conflicted: NICHT mit git 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.tsxuseKeyboardAnimation() + 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-controller
  • npx 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 mit paddingTop: 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 mit transform: [{ 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: keyboardHeight als 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-ChartMailWeeklyChart.tsx ist bereits angelegt aber Render-Logic noch nicht 1:1 vom Nuxt-mail-stats-chart.vue portiert. 7-Tage-Bar-Chart mit account_*-Stats.
  • iron.png-Warning vollständig fixendm.tsx ist gefixt, aber room.tsx (3 Stellen: 308, 537, 598) und components/chat/RoomCard.tsx:52 nutzen noch raw room.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

  • KeyboardAdjustedView rollout über alle TextInput-Stellen (siehe Liste in §9)
  • TtsProviderToggle in __DEV__-Debug-Tab einbauen
  • Single-Branch-Konsolidierung: upgrade/sdk-54main Force-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:

  1. apps/rebreak-native/assets/sounds/ Dir anlegen
  2. 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öne
    • snake-record.mp3 ~600ms aufsteigender Chime
  3. useSnakeSounds.ts öffnen, require() und Audio.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