rebreak-monorepo/ops/UI_MIGRATION_PLAN.md
chahinebrini e76be7ee78 feat(profile): Profile-Page komplett + Header-Dropdown + UI-Pattern-Fixes
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).
2026-05-07 18:22:58 +02:00

266 lines
12 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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
- `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` + `<TtsProviderToggle />` + `<LlmProviderToggle />` als Floating-Bar (`st.ttsToggleBar`, line 11281131).
- `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:6576`.
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 126139, 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
<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):**
- `KeyboardAvoidingView` **entfernen**.
- `keyboardHeight`-State (existiert schon ab line 92, line 198199 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: 35 deutsche female voices auswählen, in `lib/lyraVoices.ts` als statische Liste.
- Cartesia: 23 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 (12 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 (35 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 (23 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__`).