# 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__`).