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

12 KiB
Raw Blame History

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)

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

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)

  1. 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).
  2. Streak-Tab — useStreak-Hook + Badge.
  3. Einstellungen-Tab — Devices-Liste, Theme/Language-Picker, Notification-Toggles (real persisten), Logout (existiert).
  4. Subscription-Section — Stripe-Portal-Link via Linking.openURL.

Phase 4 — Legend Features (23 days)

  1. DB-Migration lyra_voice_id (Backend-team).
  2. API-Update speak-*.post.ts body-param + tier-check.
  3. Voice-Picker UI im Lyra-Tab.
  4. sosTtsQueue.ts body voiceId.

Phase 5 — Followup

  1. Community-Domains-Section (mit Blocker-Migration zusammen).
  2. Profile-View /profile/[userId].tsx.
  3. Hilfe/FAQ-Section.
  4. 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__).