# UI Migration Plan — Settings + Profile (Nuxt → rebreak-native)
Stand: 2026-05-07
Scope: rebreak-native (Expo / RN). Owner: rebreak-native-ui.
Phase 1 = NUR Plan (dieses Dokument). Keine Code-Änderungen in `apps/rebreak-native/`.
---
## 1. Status Quo
### Nuxt-App (`~/mono/trucko-monorepo/apps/rebreak/app/pages/`)
Bereits gebaute Pages, die in rebreak-native fehlen oder nur als Stub existieren:
- `app/settings.vue` (~520 LOC) — 3 UTabs: **Streak**, **Profil**, **Einstellungen**
- Streak: `` (separate Component)
- Profil: nickname-Edit + Avatar (HERO_AVATARS preset ODER Foto-Upload mit Cropper)
- Einstellungen-Tab enthält:
- Hilfe & FAQ (Link)
- Appearance: system / light / dark (UColorMode)
- Language: i18n locale-switch
- Dev-Tools: Keyboard-Resize-Test, Family-Controls Spike
- **Devices** (`useDeviceStore`) — Liste mit Limit-Progressbar, current-device-Badge
- **Community-Domains** (approved + rejected) aus `/api/custom-domains`
- **Subscription** (Stripe Portal — `/api/stripe/portal`) — paid only
- Logout
- `app/profile/[userId].vue` — fremdes Profil ansehen (nickname, tier, posts, follow, recent posts, DM-link)
### rebreak-native — was schon existiert
- `app/settings.tsx` (223 LOC) — Stub-Scaffold. Sections vorhanden, aber Handlers leer (`onPress: () => {}`).
- `components/AppHeader.tsx` — Dropdown-Menu funktioniert, hat schon `editProfile` + `settings` Items, beide routen auf `/settings`. SOS-Button im Dropdown ist auch da.
- `app/urge.tsx` — SOS-Page mit `KeyboardAvoidingView` + `` + `` als Floating-Bar (`st.ttsToggleBar`, line 1128–1131).
- `lib/ttsProvider.ts` — AsyncStorage-Persist, 5 provider (`openai`, `gemini`, `google-cloud`, `elevenlabs`, `cartesia`).
- `lib/llmProvider.ts` — analog für LLM (`auto`, `openrouter-sonnet`, …).
### Was komplett fehlt im rebreak-native
- Profile-Edit-Logik (nickname-Form, Avatar-Picker mit HERO_AVATARS + Photo-Upload)
- StreakTab-Equivalent als RN-Component
- Devices-Section (read aus `/api/devices`)
- Community-Domains-Section
- Stripe-Portal-Button
- Theme/Language-Switcher mit echtem State (i18n + ColorMode)
- Lyra-Voice-Picker (existiert nirgends)
- Debug-Section (TtsProviderToggle + LlmProviderToggle aktuell hardcoded auf urge.tsx)
- Profile-View für fremde User (`/app/profile/[userId]` Nuxt → `/profile/[userId].tsx` RN)
---
## 2. Header-Dropdown-Menu Architektur
Bereits im File: `apps/rebreak-native/components/AppHeader.tsx:65–76`.
Aktuell:
```
[SOS — heart, prominent]
─────
person-outline "Profil bearbeiten" → /settings
settings-outline "Einstellungen" → /settings
─────
log-out-outline "Abmelden"
```
**Empfohlene Items nach Migration:**
- SOS (bleibt prominent)
- Profil bearbeiten → `/settings?tab=profile`
- Einstellungen → `/settings?tab=settings`
- Streak → `/settings?tab=streak`
- (Legend-only) Lyra-Voice → `/settings?tab=settings#lyra-voice`
- (Dev-Builds only / `__DEV__`) Debug → `/settings?tab=debug`
- Logout
→ Settings-Page übernimmt die alte Nuxt-3-Tab-Struktur **plus** Debug-Tab (gated auf `__DEV__` oder Internal-User).
Kein neues Header-Element nötig — Dropdown ist da, nur die `tab`-Query-Param-Logik in `settings.tsx` ergänzen + Items ergänzen.
---
## 3. Settings-Page Struktur (Migration-Ziel)
`app/settings.tsx` umbauen zu Tab-Layout (z.B. via simple Tab-Bar — kein UTabs in RN, custom mit drei `Pressable`-Buttons reicht).
### Tab 1 — Streak (MVP)
- StreakBadge + currentDays/longestDays (read aus `/api/streak`)
- Heatmap der letzten Wochen (StreakEvent-Liste)
- Reset-Button mit Confirm-Modal
### Tab 2 — Profil (MVP)
- Avatar (HERO_AVATARS preset grid + Photo-Upload via `expo-image-picker`)
- Nickname-Input (Save → `PATCH /api/auth/me { nickname }`)
- Username (read-only display, `@username`)
- Member-Since-Date
### Tab 3 — Einstellungen (MVP)
- **Appearance** — system/light/dark (AsyncStorage + theme-Provider) — *requires theme-store create*
- **Language** — de/en (i18n.changeLanguage + AsyncStorage persist)
- **Push-Notifications** Toggle (existiert schon im Stub)
- **Streak-Reminders** Toggle
- **Devices** (read aus `/api/devices`) — Liste, kein Delete (cooldown noch nicht implementiert)
- **Subscription** — Plan-Status + "Manage" → opens Stripe-Portal-URL via `Linking.openURL(url)`
- **Logout**
- **Delete Account** (Danger-Zone)
### Tab 4 — Lyra (Legend-only)
- Voice-Picker (default: Alexandra ElevenLabs / Sonic Cartesia)
- Speed-Slider? (ElevenLabs `voice_settings.style` o.ä.)
- Preview-Button (sample-text TTS)
### Tab 5 — Debug (gated `__DEV__` || internal-user)
- TtsProviderToggle (verschoben aus urge.tsx)
- LlmProviderToggle (verschoben aus urge.tsx)
- Bench-Anzeige (letzte BenchSession-Werte aus `lib/sosTtsBenchmark`)
- Reset-AsyncStorage-Button
### Followup (NICHT MVP)
- Community-Domains-Liste (kann zur Blocker-Page wandern)
- Hilfe & FAQ — Webview oder externe URL erstmal
- Family-Controls Spike — Native-only, kein UI nötig
- Profile-View `/profile/[userId].tsx` — Community-Section, kommt mit Community-Migration
---
## 4. iOS-Keyboard-Fix für SOS-Page
### Pattern aus `components/PostCommentsSheet.tsx`
Funktionierende Bestandteile (line 126–139, 240, 367):
1. **Listener** auf `keyboardWillShow`/`keyboardWillHide` (iOS) bzw. `keyboardDidShow`/`keyboardDidHide` (Android) → `keyboardHeight`-State.
2. **Container-Padding bottom = `Platform.OS === 'ios' ? keyboardHeight : 0`** auf der gesamten inneren Inhalts-View. (Android nutzt `windowSoftInputMode=adjustResize` und braucht kein Padding.)
3. **Input-Bar paddingBottom = `keyboardHeight > 0 ? 8 : Math.max(12, insets.bottom)`** — schlankes Padding bei offener Tastatur, sonst Safe-Area.
4. `keyboardShouldPersistTaps="handled"` auf der scrollbaren Liste.
### Was urge.tsx aktuell macht (line 1144)
```tsx
```
→ funktioniert in Modals manchmal, aber bei Full-Screen-Pages wie urge.tsx (kein Modal!) overlapped der Input bei iOS, weil `behavior="padding"` kämpft mit der SafeAreaView und dem festen `paddingTop: insets.top`. User-Bug ist reproduziert.
### Vorgeschlagener Fix für `app/urge.tsx`
**Option A (empfohlen, 1:1 PostCommentsSheet-Pattern):**
- `KeyboardAvoidingView` **entfernen**.
- `keyboardHeight`-State (existiert schon ab line 92, line 198–199 listener → behalten).
- Auf den äußeren Container (oder den Wrapper um `FlatList + chips + inputBar`) `paddingBottom: Platform.OS === 'ios' ? keyboardHeight : 0` anwenden.
- Input-Bar (line 1230): `paddingBottom: keyboardHeight > 0 ? 8 : Math.max(12, insets.bottom)` ist schon korrekt — bleibt.
- `FlatList`: `keyboardShouldPersistTaps="handled"` hinzufügen (verhindert dass tap-on-chip die Tastatur wegklickt).
**Option B (minimaler Eingriff):**
- `KeyboardAvoidingView`-`keyboardVerticalOffset` auf `insets.top + topBarHeight` setzen statt `0` — hilft bei vielen Layouts, aber fragiler.
→ **A** ist konsistent mit dem bewährten Pattern und schon benutzt für PostComment. Soll der Default sein.
### Files anzupassen
- `apps/rebreak-native/app/urge.tsx` (lines 1144, 1249, ggf. um den top-bar-Container herum)
---
## 5. Lyra-Voice-Feature
### Status backend
`speak-elevenlabs.post.ts:28` und `speak-cartesia.post.ts:23` lesen voiceId NUR aus `runtimeConfig.elevenlabsVoiceId` / `cartesiaVoiceId` oder aus `process.env`. **Kein body-param `voiceId` aktuell.**
Aktuell ist die Voice systemweit fix.
### Frage: DB-Schema-Change nötig?
**JA** — aber minimal. Variante (a) ist **empfohlen**:
**(a) Profile-Field erweitern (1 Spalte):**
```prisma
model Profile {
...
lyraVoiceId String? @map("lyra_voice_id") // null = system-default
...
}
```
+ Migration: `ALTER TABLE rebreak.profiles ADD COLUMN lyra_voice_id text NULL;`
+ `me.patch.ts` um `lyraVoiceId` erweitern (gated: nur Legend-User dürfen setzen).
+ `me.get.ts` returnt `lyraVoiceId` mit.
**(b) Separates `UserPreference`-Modell (overkill für jetzt):**
Nur lohnenswert wenn weitere Prefs (theme, language-override, push-prefs) bald hinzu kommen. Dann lieber zentral.
→ Empfehlung: **(a)** für Phase 2, **(b)** parken bis 3+ Prefs gleichzeitig nötig sind.
### API-Changes
`speak-elevenlabs.post.ts` + `speak-cartesia.post.ts`:
- Body um optional `voiceId?: string` erweitern.
- VoiceId-Resolve-Order: body-param → user.lyraVoiceId (DB) → runtimeConfig → env → FALLBACK.
- Tier-check: wenn body-param gesetzt aber user nicht Legend → 403 oder silent-ignore + use default (silent-ignore safer).
### Frontend-Flow
1. `useMe()` returned bereits Profile incl. zukünftiges `lyraVoiceId`.
2. Settings-Tab (Lyra) — Voice-Liste hardcoded zur First-Iteration:
- ElevenLabs: Alexandra (`kdmDKE6EkgrWrrykO9Qt`), Rachel, …
- Cartesia: Default-DE (`b9de4a89-2257-424b-94c2-db18ba68c81a`), …
3. Save → `PATCH /api/auth/me { lyraVoiceId }`.
4. `lib/sosTtsQueue.ts:208` — Body um `voiceId: useMe().lyraVoiceId` erweitern.
### Voices initial (nice-to-have hardcoded)
- ElevenLabs: 3–5 deutsche female voices auswählen, in `lib/lyraVoices.ts` als statische Liste.
- Cartesia: 2–3 deutsche stimmen.
- Per Provider (TTS-Provider != Voice-Provider), Voice-Picker zeigt nur Voices passend zum gewählten TTS-Provider — oder "auto" mit per-Provider-Default-Map.
→ Erste Iteration: nur ElevenLabs-Voices anbieten (häufigster Provider), andere Provider ignorieren `lyraVoiceId`.
---
## 6. Migration-Reihenfolge (Phase 2/3/4)
### Phase 2 — Quick Wins (1–2 days)
1. **iOS-Keyboard-Fix urge.tsx** — kein Server-Change, isolierter UI-Fix → Smoke-Test auf iOS-Device.
2. **Debug-Tab in settings.tsx** — TtsProviderToggle + LlmProviderToggle dort einbauen, aus urge.tsx entfernen.
3. **Header-Dropdown** — Items "Streak" + "Debug" (gated `__DEV__`) ergänzen, settings.tsx Tab-Routing implementieren.
### Phase 3 — MVP-Cutover (3–5 days)
4. **Profil-Tab** — nickname + Avatar-Picker (preset). Photo-Upload via `expo-image-picker` + Crop später (sehr großes File mit `vue-advanced-cropper`-Equivalent in RN: `react-native-image-crop-picker` wäre Native-Module — später).
5. **Streak-Tab** — useStreak-Hook + Badge.
6. **Einstellungen-Tab** — Devices-Liste, Theme/Language-Picker, Notification-Toggles (real persisten), Logout (existiert).
7. **Subscription-Section** — Stripe-Portal-Link via `Linking.openURL`.
### Phase 4 — Legend Features (2–3 days)
8. **DB-Migration** `lyra_voice_id` (Backend-team).
9. **API-Update** `speak-*.post.ts` body-param + tier-check.
10. **Voice-Picker UI** im Lyra-Tab.
11. **sosTtsQueue.ts** body voiceId.
### Phase 5 — Followup
12. Community-Domains-Section (mit Blocker-Migration zusammen).
13. Profile-View `/profile/[userId].tsx`.
14. Hilfe/FAQ-Section.
15. Photo-Upload mit Crop (native module).
---
## 7. Top-3 Risiken
1. **Avatar-Photo-Upload** — Nuxt nutzt `vue-advanced-cropper` (HTML5-Canvas). RN-Alternative: `react-native-image-crop-picker` ist ein Native-Module (Expo-Plugin nötig) → Coordination mit `zied`/`backyard`. **Mitigation:** MVP nur HERO_AVATARS preset, photo-upload als Phase-5.
2. **Theme-Switch / ColorMode** — Aktuell nutzt rebreak-native NICHT `useColorScheme`-getriebenes Theme. Komplette Color-Token-Refactor wäre nötig (`lib/theme.ts` aktuell hardcoded light). **Mitigation:** MVP "system" disabled, nur Lock auf light. Dark-Mode Phase-5.
3. **DB-Migration `lyra_voice_id`** — Schema-Change auf production via Prisma migration → wenn Cutover-Blocker noch nicht resolved (siehe `feedback_backend_runtime_config.md`), hängt das. **Mitigation:** Voice-Picker erst bauen wenn Cutover stabil; bis dahin client-side AsyncStorage als Mock-Persist (nur lokal, regelt sich nach Login auf neuem Gerät).
---
## 8. Empfehlung — erster Schritt in Phase 2
**Start: iOS-Keyboard-Fix in `app/urge.tsx`.**
Warum:
- Isoliert (1 File, kein Backend, keine Coordination)
- Direkt user-sichtbar als Win
- Pattern (PostCommentsSheet) bereits validiert
- Kein Risiko für andere Features (kein Schema, kein API-Change)
- Sets up den Workflow: small PR, smoke-test, merge — vor den größeren Settings-Migrations.
Danach: Debug-Tab + TtsProviderToggle/LlmProviderToggle aus urge.tsx in settings.tsx wandern (entfernt visuelle Bench-Bar aus Production-Build, optional gated auf `__DEV__`).