Profile (3 Iterationen):
- app/profile/index.tsx + components/profile/* (Header, StatsBar, Approved,
Streak, UrgeStats, Demographics, DigaMissionBanner)
- echte Live-Daten via useMe-Hook (Avatar/Nickname/Plan/Email/Provider-Pill)
- Demographics mit echten Inputs (TextInput + Bottom-Sheet-Selects),
debounced auto-save, Pro-Trial-Reward-Banner, Mikro-Why-Texte
- Approved Domains als plain integer (KEIN Plan-Slot/Cap)
- Friendly Hint-Text statt Progress-Bar (alignSelf:'stretch' Pattern)
- StatsBar zentriert mit 3 prominenten Cards (vertikale Dividers)
- Cooldown-Timeline als Liste mit 1px-Rail
- ApprovedDomainsList: Collapse-Chevron rechts in Title-Row (Pattern-Fix)
- Eigene vs fremde Profile-Ansicht streng getrennt (DSGVO/Anonymität)
Header-Dropdown (kein 3-Punkte-Icon):
- Avatar als Trigger im AppHeader (User-Wunsch)
- Custom-Modal beide Plattformen, Card-Style
- SOS prominent oben (nur Wort 'SOS' rot, Tagline 'wir sind für dich da' klein darunter)
- Profile/Settings/Games/Debug(__DEV__)/Logout
- Logout neutral (nicht rot — Recovery-tonal)
- AppHeader: neue showBack + title Props für Sub-Routes
Routes (Stub bis Phase C):
- app/profile/[userId].tsx — anonym (nur public-Stats)
- app/settings.tsx — Coming-Soon-Skeleton
- app/games.tsx — Standalone Games-Page mit GameCard-Grid
- app/debug.tsx — __DEV__-only
Game-Picker (Migration aus Nuxt):
- components/games/{GameCard, StarRating, GameRatingStars}
- 2x2 Grid, 56pt SVG-Icons (inline aus components/urge/gameSvgs.ts)
- Live-Backend /api/games/ratings (silent-fail)
- Re-use UrgeGames.tsx ohne TTS/Cooldown-Loop
UI-Pattern-Fixes (alle aus screenshot-User-Feedback 2026-05-07):
- Snake-Bug (food-pellet React-18-StrictMode-Reducer-double-call) gefixt
- Snake-Buttons platform-native (iOS-blue / Android-ripple)
- Tetris-Margins (16px paddingHorizontal)
- PostCard-Buttons Apple-44pt-Hit-Area (Image-Select, Image-Remove,
Cancel, Share-Pill — via hitSlop)
- ProfileHeader Demographics-Hint: alignSelf:'stretch' Pattern
- ApprovedDomainsList Collapse: Title flex:1 + Chevron rechts
- ProtectionDetailsSheet FAQ-Items: alignSelf:'stretch' defensive
- AppHeader Back-Button: neue showBack-Prop + chevron-back
Memory + Plan-Docs:
- 17 Memory-Files dokumentieren System-Wissen + Patterns
- ops/{CUTOVER, UI_MIGRATION, PROFILE_PAGE, WEBHOOK, GAMES_1V1,
RELEASE_READINESS, TESTING_STATE, MAESTRO_HOSTING}_*.md
Backend bleibt unverändert (Tier-LLM + Nickname + sort:latency
sind seit gestern deployed).
12 KiB
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:
<StreakTab />(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
- Streak:
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 schoneditProfile+settingsItems, beide routen auf/settings. SOS-Button im Dropdown ist auch da.app/urge.tsx— SOS-Page mitKeyboardAvoidingView+<TtsProviderToggle />+<LlmProviderToggle />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].tsxRN)
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.styleo.ä.) - 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):
- Listener auf
keyboardWillShow/keyboardWillHide(iOS) bzw.keyboardDidShow/keyboardDidHide(Android) →keyboardHeight-State. - Container-Padding bottom =
Platform.OS === 'ios' ? keyboardHeight : 0auf der gesamten inneren Inhalts-View. (Android nutztwindowSoftInputMode=adjustResizeund braucht kein Padding.) - Input-Bar paddingBottom =
keyboardHeight > 0 ? 8 : Math.max(12, insets.bottom)— schlankes Padding bei offener Tastatur, sonst Safe-Area. keyboardShouldPersistTaps="handled"auf der scrollbaren Liste.
Was urge.tsx aktuell macht (line 1144)
<KeyboardAvoidingView style={{ flex: 1 }} behavior={Platform.OS === 'ios' ? 'padding' : 'height'} keyboardVerticalOffset={0}>
→ 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):
KeyboardAvoidingViewentfernen.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 : 0anwenden. - 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-keyboardVerticalOffsetaufinsets.top + topBarHeightsetzen statt0— 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):
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.tsumlyraVoiceIderweitern (gated: nur Legend-User dürfen setzen).me.get.tsreturntlyraVoiceIdmit.
(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?: stringerweitern. - 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
useMe()returned bereits Profile incl. zukünftigeslyraVoiceId.- Settings-Tab (Lyra) — Voice-Liste hardcoded zur First-Iteration:
- ElevenLabs: Alexandra (
kdmDKE6EkgrWrrykO9Qt), Rachel, … - Cartesia: Default-DE (
b9de4a89-2257-424b-94c2-db18ba68c81a), …
- ElevenLabs: Alexandra (
- Save →
PATCH /api/auth/me { lyraVoiceId }. lib/sosTtsQueue.ts:208— Body umvoiceId: useMe().lyraVoiceIderweitern.
Voices initial (nice-to-have hardcoded)
- ElevenLabs: 3–5 deutsche female voices auswählen, in
lib/lyraVoices.tsals 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)
- iOS-Keyboard-Fix urge.tsx — kein Server-Change, isolierter UI-Fix → Smoke-Test auf iOS-Device.
- Debug-Tab in settings.tsx — TtsProviderToggle + LlmProviderToggle dort einbauen, aus urge.tsx entfernen.
- Header-Dropdown — Items "Streak" + "Debug" (gated
__DEV__) ergänzen, settings.tsx Tab-Routing implementieren.
Phase 3 — MVP-Cutover (3–5 days)
- Profil-Tab — nickname + Avatar-Picker (preset). Photo-Upload via
expo-image-picker+ Crop später (sehr großes File mitvue-advanced-cropper-Equivalent in RN:react-native-image-crop-pickerwäre Native-Module — später). - Streak-Tab — useStreak-Hook + Badge.
- Einstellungen-Tab — Devices-Liste, Theme/Language-Picker, Notification-Toggles (real persisten), Logout (existiert).
- Subscription-Section — Stripe-Portal-Link via
Linking.openURL.
Phase 4 — Legend Features (2–3 days)
- DB-Migration
lyra_voice_id(Backend-team). - API-Update
speak-*.post.tsbody-param + tier-check. - Voice-Picker UI im Lyra-Tab.
- sosTtsQueue.ts body voiceId.
Phase 5 — Followup
- Community-Domains-Section (mit Blocker-Migration zusammen).
- Profile-View
/profile/[userId].tsx. - Hilfe/FAQ-Section.
- Photo-Upload mit Crop (native module).
7. Top-3 Risiken
- Avatar-Photo-Upload — Nuxt nutzt
vue-advanced-cropper(HTML5-Canvas). RN-Alternative:react-native-image-crop-pickerist ein Native-Module (Expo-Plugin nötig) → Coordination mitzied/backyard. Mitigation: MVP nur HERO_AVATARS preset, photo-upload als Phase-5. - Theme-Switch / ColorMode — Aktuell nutzt rebreak-native NICHT
useColorScheme-getriebenes Theme. Komplette Color-Token-Refactor wäre nötig (lib/theme.tsaktuell hardcoded light). Mitigation: MVP "system" disabled, nur Lock auf light. Dark-Mode Phase-5. - DB-Migration
lyra_voice_id— Schema-Change auf production via Prisma migration → wenn Cutover-Blocker noch nicht resolved (siehefeedback_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__).