# 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: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):**

1. **`git worktree add`** — zweiter Working-Tree für andere Branches:
   ```bash
   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):
   ```bash
   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:

```bash
# 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`](https://github.com/kirillzyusko/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-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-Chart** — `MailWeeklyChart.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 fixen** — `dm.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-54` → `main` 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
```
