50 Commits

Author SHA1 Message Date
chahinebrini
2c33ba55a4 fix(backend): username (Login-Identifikator) aus öffentlichen Payloads entfernt
community/posts.get.ts + social/profile/[userId].get.ts lieferten neben
nickname auch username an fremde Clients — username ist der Login-
Identifikator ({username}@rebreak.internal) und verletzt die Nickname-
Anonymitäts-Invariante (REQ-COMM-005 / R-DATA-07) + exponiert das halbe
Login-Credential-Paar. Frontend rendert das Feld nirgends (verifiziert);
totes Typ-Feld in stores/community.ts entfernt.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 06:36:16 +02:00
chahinebrini
63fae25531 fix(android-protection): explicit specialUse FGS type — Samsung/Android 16 crash loop
RebreakVpnService.onStartCommand crashed with SecurityException because Android 16's validateForegroundServiceType rejects the implicit 2-arg startForeground(). Now passes FOREGROUND_SERVICE_TYPE_SPECIAL_USE explicitly (Google's documented best practice) and guards the call so a failed foreground promotion stops the service cleanly instead of crashing the app. Verified vs reported Galaxy A54 / Android 16 signature (97% of crash events, 1-user crash loop).

Bundles pending working-tree work across native/marketing/locales/mac + graphify-out rebuild. gitignore: google-services.json + /screenshots/.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 22:33:28 +02:00
chahinebrini
c4fe7d356f fix(rebreak-native): iOS app-lock freeze (Face ID stuck, needs hard-quit)
LockScreen could latch on the locked screen with no working Face ID
prompt until a hard-quit. Two coupled causes:
- inFlight/busy could stay latched if authenticateAsync hung when called
  mid active↔inactive transition → gate tryUnlock on AppState 'active'
  and release the latch on background (iOS tears down the sheet anyway).
- foreground recovery missed background→inactive→active (prev was
  'inactive' at the active event) → track "was backgrounded since last
  active" instead, so re-prompt fires reliably (this is why only
  hard-quit recovered it). The Face ID sheet's own active→inactive→active
  no longer re-triggers (only real 'background' arms the flag).
- appLock.authenticate wraps authenticateAsync in try/catch so a native
  reject always settles (the LockScreen finally relies on it).

cancelAuthenticate() is Android-only (no iOS native impl) — fix works by
prevention + self-heal, not cancellation. Frontend-only; needs a native
rebuild, no deploy.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 15:11:12 +02:00
chahinebrini
ac1d33afb8 fix(native): phantom/zombie incoming calls (iOS) + DM online dot
Calls: an incoming call that ended without the in-app /call screen ever
mounting (iOS shows the native CallKit banner, not our screen) left the
call store stuck in 'ended' forever — the ended→idle reset only lived in
the /call screen. A stuck 'ended' then blocked every subsequent incoming
call (RING + VoIP push were received but dropped by the status!=='idle'
guard), so accepting from the banner produced a phantom CallKit call that
ticked as active with no connection, and the caller saw a missed call.
- store self-heals back to 'idle' after a call ends (teardown fallback)
- receiveIncoming + ring handler tolerate a stale 'ended' state
- onAnswer ends the native CallKit call when store has no incoming call
- RNCallKeep.endAllCalls() on launch clears leftover CallKit zombies

DM online dot: the green avatar dot used follow-gated presence while the
"online" text used raw presence → dot hidden for non-followed partners
even when online. DM header avatar now uses raw presence (rawPresence
prop) → consistent with the text on both platforms.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 10:03:27 +02:00
chahinebrini
5531ef5419 fix(calls): foreground call screen no longer disappears after few seconds
Root cause: iOS CallKit auto-dismisses incoming-call UI after ~5s when the
app is in foreground (because AppDelegate.didReceiveIncomingPush MUST call
reportNewIncomingCall — Apple requirement). That CallKit dismiss fires an
endCall event which our useCallKeepEvents.onEnd translated to declineCall,
unmounting the in-app /call screen before the user could tap accept/decline.

Fixes:
- useCallKeepEvents.onEnd: ignore CallKit endCall when iOS app is foreground
  AND status==='incoming' (in-app UI is authoritative there). Comment with
  big warning not to remove this again.
- call.tsx closeScreen: replace('/') instead of router.back() to avoid
  GO_BACK action errors when navigation stack is inconsistent after long
  calls (manifested as wrap-jsx.js crash in react-native-css-interop).
- useIncomingCalls: log CANCEL receive events for future diagnostics.
- call.ts: clog hangup/declineCall/closeScreen with reason+status for trace.

Verified: foreground call screen stays up the full UNANSWERED_MS (35s) and
caller-side hangup('unanswered') correctly triggers iPhone closeScreen via
cancel-broadcast.
2026-06-04 21:48:34 +02:00
chahinebrini
7fae4539ae diag(calls): add VoIP+push-token+ring-target logs; fix /call mount race
- AppDelegate: NSLog for didUpdate token, didInvalidate, didReceiveIncomingPush
- backend/push: log [push-token] register, [call-ring] receiver token-counts +
  expo-push-fanout for android-fallback
- app/call.tsx: 250ms grace window before closeScreen on initial idle (fixes
  'foreground call flashes briefly then disappears' race when dm.tsx
  startCall set() hasn't propagated through useCallStore selector yet)
2026-06-04 20:37:43 +02:00
chahinebrini
6a907cf89b fix(calls): sandbox/prod VoIP-push failover + foreground CallKit-UI suppress
- voip-push: build both APNs Provider (production+sandbox) and try each per
  token with memoization. Fixes BadDeviceToken on Xcode-Dev-Builds where the
  token is Sandbox-only.
- stores/call: only call callkit.displayIncomingCall when app NOT in foreground
  \u2014 in foreground the /call route handles ringing UI, otherwise double UI
  (system banner + fullscreen).
- patch react-native-callkeep: New-Arch TurboModule compatibility (no overloads,
  no Bundle params in @ReactMethod).
- pushTokenRegistration: more verbose [voip] diagnostics.
2026-06-04 19:42:44 +02:00
chahinebrini
fb2d90b947 fix(calls): no duplicate incoming-call notifications
- backend: skip Expo alert push to iOS devices that already received VoIP push
  (CallKit + banner = double ring)
- native: receiveIncoming no longer triggers InCallManager.startRingtone —
  CallKit/ConnectionService play their own ring. Dedup if same callId
  arrives twice (Realtime + VoIP-Push race).
2026-06-04 18:28:00 +02:00
chahinebrini
822053e11e feat(calls): CallKit/ConnectionService + VoIP-PushKit + EU-Ringback
Caller/Callee UX:
- lib/ringback.ts + assets/sounds/ringback_eu.mp3 (EU 425Hz Festnetz-Tone)
- stores/call.ts: stopRingback bei connected, hangup-reasons, logCallToChat fix
- locales: 'Wird angerufen…' statt 'Ruft an…'

CallKit (iOS) + ConnectionService (Android):
- lib/callkit.ts: setupCallKeep, displayIncomingCall, startOutgoingCall, reportConnected/Ended (appName 'ReBreak-Audio', includesCallsInRecents=false für DSGVO/DiGA)
- hooks/useCallKeepEvents.ts: native answer/end/mute → useCallStore-Actions
- stores/call.ts: CallKit-Aufrufe an allen lifecycle-Punkten
- app.config.ts: @config-plugins/react-native-callkeep + UIBackgroundModes voip/audio + Android-Telecom-Perms

VoIP-PushKit Backend:
- services/voip-push.ts: @parse/node-apn Provider mit .p12 (Topic org.rebreak.app.voip)
- services/push.ts sendCallRingPush: feuert beide Pfade (VoIP iOS + Expo Android/Fallback)
- prisma: push_tokens.voip_token Column + Migration 20260604
- api/users/me/push-token: optional voipToken im Body
- Env (Infisical): APNS_VOIP_P12_PATH/PASSWORD/TOPIC/PRODUCTION

Push-tap routing + cold-start handling:
- app/_layout.tsx: type:'call' Push → useCallStore.receiveIncoming + /call

Docs: ops/CALLKIT_SETUP.md (Apple-Portal-Steps für VoIP-Cert)
2026-06-04 09:27:13 +02:00
chahinebrini
50425a62ee fix(devices): Magic-Hub zeigt jetzt alle Native-Geraete, Native dedupliziert Mac
Magic-Mac-Hub (/api/magic/devices):
- Filter boundToPlan war zu eng \u2014 iPhone/iPad ohne aktiven Plan-Lock
  fielen raus. Jetzt: alle UserDevice-Rows des Users ausser den
  magic-enrolled, plus ProtectedDevice mit Dedupe.

Native /devices Page:
- MacBook erschien doppelt: einmal als UserDevice (registriert via
  Magic-Mac, model=Mac14,9) und einmal als ProtectedDevice (alter
  DNS-Flow). Dedupe per platform-key (mac/ios/android/win):
  wenn UserDevice mit gleicher Plattform existiert, blende
  ProtectedDevice aus.
- Slot-Counter zaehlt jetzt nach dedupe (totalRegistered).
2026-06-03 19:43:33 +02:00
chahinebrini
187a2d8c19 feat(magic): Hub Header mit Avatar+Nickname + iPhone/iPad via UserDevice-Locks + MacBook-Dedupe
- Neuer Endpoint /api/magic/me liefert nickname/avatar/plan fuer
  Hub-Header. Mac-App ruft fetchMe() beim Hub-Load.
- DeviceHubView Header zeigt jetzt Avatar (AsyncImage mit Fallback
  auf Initial-Letter), Nickname + Plan-Badge statt nur 'ReBreak Magic'.
- /api/magic/devices erweitert: listet zusaetzlich UserDevice-Rows mit
  boundToPlan != null (das sind iPhone/iPad aus dem Native-App-Login-
  Flow, Legend-Device-Lock). source='locked'.
- Dedupe: ProtectedDevice wird unterdrueckt wenn bereits ein UserDevice
  mit aehnlichem Namen + gleicher Plattform existiert (fixt doppelten
  MacBook im Hub).
- Helper prettyPlatform() + Normalisierung (platform-key 'mac'/'ios'/
  'android'/'win') fuer robusten Vergleich.
2026-06-03 11:41:06 +02:00
chahinebrini
8670b45351 fix(magic): inline mobileconfig template as TS constant
serverAssets approach didn't bundle the template into the Nitro
output (no .output-staging/server/chunks/raw/ dir, no asset-storage
mount in nitro.mjs). Logs confirm: '[Magic] Profile template missing
in serverAssets'.

Drop serverAssets entirely. Inline the template (~2KB) as a TS
constant in backend/server/utils/magic-profile-template.ts. Build-
robust, no FS/storage dependency at runtime. Canonical source of
truth remains ops/mdm/rebreak-mac-dns-filter.mobileconfig — keep in
sync manually until/unless we add a codegen step.
2026-06-03 09:57:27 +02:00
chahinebrini
c1edef8abd feat(magic): RebreakMagic device-binding + DNS profile
- backend: /api/magic/{register,devices,profile,release} + AdGuard provisioning + 24h cooldown
- prisma: magic_binding_fields migration (additive on UserDevice)
- mac-app: Phase 2 - Login + MacRegistration + Profile install
- marketing: landing section + /download/rebreakmagic + DMG
- lyra: forbidden phrases + RebreakMagic coach guidance
2026-06-02 09:15:19 +02:00
chahinebrini
2e056c7257 feat(devices): Apple-style two-device approval flow + email fallback
iCloud-Sign-In Pattern: wenn ein neues Gerät versucht sich anzumelden
und das Plan-Limit erreicht ist, kann der User auf einem bereits
angemeldeten Gerät bestätigen — Code wird auf BEIDEN Geräten gezeigt
für visuellen Vergleich (verhindert Code-Forwarding-Attacken).

Backend:
- New table device_approval_requests + supabase_realtime + RLS
- POST   /api/devices/approvals               — create (new device)
- GET    /api/devices/approvals               — list pending (existing devices)
- GET    /api/devices/approvals/:id           — status poll (new device)
- POST   /api/devices/approvals/:id/approve   — approve + atomic evict
- POST   /api/devices/approvals/:id/reject    — reject
- POST   /api/devices/approvals/:id/email     — trigger email fallback
- POST   /api/devices/approvals/email/:token  — magic-link approve (no auth)
- Email-Template via Resend (lyra-neutral, security-formal)
- 10min TTL, 6-digit numeric codes (crypto-random)

Frontend (rebreak-native):
- DeviceApprovalIncomingSheet — existing devices: code + device-picker + Allow/Reject
- DeviceApprovalPendingSheet  — new device: code + spinner + 'Send via email'
- useDeviceApprovalRealtime   — postgres_changes subscription
- DeviceLimitReachedSheet     — neues CTA 'Auf anderem Gerät bestätigen'
- i18n DE/EN/FR/AR

Migration läuft automatisch via prisma migrate deploy bei push.
2026-06-01 02:36:28 +02:00
chahinebrini
efca157969 fix(backend): device-mgmt cleanup + stats rejected fallback + realtime refresh
- devices: cleanupStaleDevices() purges phantoms >14d not bound; called from
  GET /api/devices + register limit-check. Deterministic sort
  (lastSeenAt, createdAt, id) so iPad+iPhone see identical order.
  Merge-cutoff tightened 30d -> 7d.
- stats: rejected aggregates from notifications(type='domain_rejected')
  via Math.max — admin reject cascade-deletes submission row.
- blocker.tsx: useDomainSubmissionRealtime triggers blockerStats.refresh()
  directly (not stale-check only) so info-sheet shows fresh rejected count.
2026-06-01 02:23:27 +02:00
chahinebrini
578abfe3bb chore(release): v0.3.13 build 46 / vc36 — DM scroll fix + chat timestamps weekday/days/weeks/months 2026-05-31 07:33:06 +02:00
chahinebrini
b31066a04c feat(chat): native action sheet + Insta-style heart for DM messages
- ChatBubble: useActionSheet replaces custom Modal (native iOS popup, Android bottom sheet)
- DM mode (isDM prop): hides like-count, shows Insta-style heart badge under bubble when liked
- Group chat unchanged
- Cleanup: remove unused Modal/Platform imports, sheet styles, actionsOpen state
- deploy.sh: auto-detect ANDROID_HOME + auto-create local.properties for local Gradle
- NEXT_RELEASE.md: DM reactions release note
- Includes other staged work across binder-mac, marketing, ops/mdm, ios/
2026-05-30 09:14:32 +02:00
chahinebrini
73f70b5e28 fix(language): auto-sync from user_metadata.locale at sign-in
Bug: User mit iOS-Sprache=Arabisch sah App auf Englisch wenn
Localization.getLocales() auf seinem Setup nicht zuverlässig 'ar'
zurückgab (iOS-Region≠Sprache, App-Override etc).

Fix: bei sign-in (init() initial-getSession + onAuthStateChange für
SIGNED_IN events) wird session.user.user_metadata.locale gelesen.
Wenn AsyncStorage @rebreak/language NOCH NICHT gesetzt ist (User hat
keine explicit Choice gemacht) → silent apply der server-locale
(inkl. RTL-flip, KEIN Restart-Alert).

Respektiert User-Choice: wenn AsyncStorage gefüllt ist (z.B. User hat
manuell in Settings gewechselt), bleibt das gewinnen.
2026-05-19 22:02:34 +02:00
chahinebrini
12e140e25b refactor(signup): remove firstName/lastName fields (DSGVO data-minimization)
Felder wurden nirgendwo gelesen/angezeigt (nur in raw_user_meta_data
gespeichert ohne Verwendung). Inkonsistent mit OAuth-Flow der sie
gar nicht erfasst. Entfernt:
- 2 Inputs aus signup.tsx
- firstName/lastName aus signUp metadata-Typ + data
- 8 i18n-keys (de/en/fr/ar)
- DB-Cleanup via SQL für 5 existing User (raw_user_meta_data - 'first_name' - 'last_name')

Art. 5(1)c DSGVO: nur Daten verarbeiten die für Zweck notwendig sind.
Nickname allein reicht — Anonymität-Pattern (memory/feedback_anonymity_nickname.md).
2026-05-19 11:05:18 +02:00
chahinebrini
5434254f74 feat(auth,mail): pw-reset OTP-flow + custom mail templates + account-switch cleanup
- Phase 3 PW-Reset: 3 screens (forgot-password → reset-otp → new-password),
  verifyOtp({type:'recovery'}), new updatePassword() action
- Custom Brevo-Mail templates (backend/public/templates/) — 5 HTMLs with
  go-template i18n (de/en/fr/ar incl. RTL for AR), OTP-only (no link),
  ReBreak branding
- signUp metadata.data.locale aus i18n.language → templates resolven Sprache
- Account-Switch-Bug fix: signOut() resettet alle 10 user-spezifischen stores
  + invalidateMe()
2026-05-19 10:49:23 +02:00
chahinebrini
23cc147231 feat(auth/ios): native Apple Sign-In via expo-apple-authentication
Vorher: stores/auth.ts hatte TODO + fiel auf Supabase-Web-OAuth-Flow zurück,
was fehlschlug mit 400 'Unsupported provider: missing OAuth client ID' weil
der Supabase-Apple-OAuth-Provider nicht konfiguriert ist.

Jetzt: native Flow ohne Supabase-Provider-Config —
- expo-apple-authentication.signInAsync() → identityToken
- supabase.auth.signInWithIdToken({provider:'apple', token}) verifiziert direkt
  gegen Apple's Public-Keys (kein Client-Secret-JWT-Setup nötig)
- User-Cancel (ERR_REQUEST_CANCELED) → leeres Resultat statt Error
- Platform-Guard: Apple-Path nur auf iOS

app.config.ts: ios.usesAppleSignIn=true → Expo prebuild generiert das
com.apple.developer.applesignin-Entitlement in die .entitlements. Beim
ersten EAS-Build wird die Capability auto-registriert im Apple-Developer-
Portal für org.rebreak.app.

signin.tsx + signup.tsx: Apple-Button conditional auf Platform.OS==='ios'
gerendert. Android-User sehen nur Google-Button (auf Android gibt es kein
natives Apple Sign-In).

App-Store-Submission-Pflicht (Apple Guideline 4.8 — wer OAuth-Login mit
3rd-Party-Provider anbietet, muss auch Apple Sign-In bieten).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 00:13:45 +02:00
chahinebrini
2e409efaf0 feat(onboarding/android + backend/lyra-i18n): platform-dispatch + post-catalog scaffold
Android-Onboarding (Platform.OS dispatch in ProtectionSlide):
- Neue Phasen für Android: preexplain_vpn → preexplain_a11y → a11y_pending
- AppState-Listener: nach Settings-Rückkehr auto-poll isAccessibilityEnabled
  → wenn live, armTamperLock + finish (kein Fokus-Klick nötig)
- onboardingAssets: 8 neue Mappings (android_vpn + android_a11y × 4 Locales)
- Screenshots: vpn-permission + a11y-rebreak-row pro Locale
- Locale-Keys: protection_url_android, protection_lock_android, cta_open_a11y,
  cta_check_a11y, dialog_button_vpn_ok, dialog_button_a11y_toggle, tap_marker_hint_*

Lyra-Post i18n Phase 1 (Scaffold, feature-flag OFF by default):
- schema.prisma: CommunityPost.i18nKey String? (nullable)
- migration 20260517_add_lyra_post_i18n_key: ALTER TABLE ADD COLUMN i18n_key
  (NICHT auto-deployed — `prisma migrate deploy` als separater Step)
- server/lib/lyraPostCatalog.ts: 15 Templates skelettiert + pickRandomTemplate
- cron/lyra-post: USE_TEMPLATE_CATALOG=true Branch → speichert i18nKey;
  default false → LLM-Path unverändert (zero-risk-deployment)
- community.createPost: optionaler i18nKey-Parameter
- posts.get: i18nKey in API-Response
- PostCard: 3-Zeilen-Branch — i18nKey ? t('lyra_posts.'+id) : content
- stores/community: i18nKey?: string|null im Interface
- de.json: lyra_posts-Block mit 15 IDs + DE-Texten

Single-Banner-Verhalten auf Android verifiziert:
lockedIn=urlFilter && appDeletionLock funktioniert weiter — auf Android
alias appDeletionLock ← tamperLock; onboarding arms tamperLock, also
nach onboarding-done direkt ProtectionLockedCard sichtbar.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 23:48:25 +02:00
chahinebrini
ac605dce33 feat(onboarding,diga): TTS auto-play preference + 90 more DiGA test codes
## TTS Auto-Play Preference

User-Request: wenn Voice einmal aktiviert, soll Lyra auf jeder Slide
automatisch sprechen — nicht jede Slide extra antippen.

- stores/lyraVoice.ts: zustand-store mit AsyncStorage-Persistence
  (@rebreak/lyraVoiceEnabled). Default OFF.
- LyraBubble auto-plays on text-change wenn enabled
- Audio-Button toggled die Preference + stoppt current playback
- Visuell: Button ist orange-filled wenn voice ON, ghost-bordered wenn OFF
- Icon: volume-mute-outline (OFF) / volume-medium / hourglass / stop
- Cleanup beim Unmount (stopLyraSpeech) + bei text-change

Initialisiert via init() in app/_layout.tsx (analog language/theme/appLock).

Locale-keys: audio_play → "Stimme einschalten", neu audio_disable → "Stimme
ausschalten" in 4 Sprachen.

## DiGA Test Codes 011-100

Aktuell 10 Codes (REBREAK-TEST-001..010), aber 100 Android-Tester kommen
morgen onboarding. Migration 20260518_extend_diga_test_codes seeded 90
zusätzliche Codes via generate_series(11, 100) + LPAD-Padding.

- Label: 'test_batch_2026-05-android' für Auditbarkeit (vs '...2026-05'
  für die ersten 10)
- grants_plan: 'legend' wie die ersten 10
- ON CONFLICT DO NOTHING — idempotent

Distribution-Pattern: Tester N kriegt Code REBREAK-TEST-<NNN-padded>.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 22:39:18 +02:00
chahinebrini
1c9e67c256 feat(onboarding,protection,i18n): spotlight POC, arabic locale, NEFilter recovery
State of work before Duo-style onboarding pivot. Includes work that will be
partly reverted in the next commit (see refactor follow-up).

Onboarding (will be partly reverted):
- Custom Tooltip+Glow spotlight (components/OnboardingHint.tsx)
- Spotlight wiring in app/profile/edit.tsx (nickname-input glow + step-progress
  header, onSubmitEditing auto-save, save-handler routes to /(app)/blocker)
- Spotlight wiring in app/(app)/blocker.tsx (URL-filter LayerSwitchCard wrapped
  + auto-PATCH step='done' when filter activates)
- Routing-gate branches in (app)/_layout.tsx (welcome → /onboarding/welcome,
  nickname → /profile/edit)
- Debug-Reset-Toggle in /debug (welcome|nickname|block|done buttons + redirect)

Will stay (reused in Duo flow):
- Welcome-Screen app/onboarding/welcome.tsx (will become Slide 1)
- Avatar-fix in profile/edit (Dicebear seed stays stable while typing)

i18n + RTL:
- Arabic locale (locales/ar.json, full translation incl. onboarding keys)
- I18nManager.allowRTL(true) + applyRTL helper in stores/language.ts
- Language-Picker option for العربية in settings
- New keys: onboarding.welcome.*, step_progress, nickname_spotlight.*,
  block_spotlight.*, permission_denied.*, language.*, rtl_restart.* (de/en/fr/ar)

NEFilter Permission Recovery (iOS):
- Swift resetUrlFilter() — removeFromPreferences + fresh saveToPreferences to
  bypass iOS's cached denied-state (NEFilterErrorDomain code 5)
- TS module def + lib/protection.ts wrapper
- components/PermissionDeniedSheet.tsx — branded recovery sheet with retry +
  app-settings:// deep-link + fallback hint
- Wired in (app)/blocker.tsx handleActivateUrlFilter (code-5 detection)

Misc:
- Bug fix in onboarding/welcome.tsx: apiFetch body was double-stringified (sent
  as JSON string instead of object → 400 invalid_step)
- Bug fix in profile/edit.tsx: avatar preview Dicebear seed switched from live
  nickname (changed every keystroke) to stable me?.nickname

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 15:44:32 +02:00
chahinebrini
0d073b398f feat(native): DEVICE_LOCKED sign-in handling + DeviceLockedPanel UI
After Supabase auth succeeds the store calls POST /api/devices/check-lock
(x-device-id auto-attached via apiFetch). A 409 DEVICE_LOCKED response
triggers a Supabase sign-out and returns { deviceLocked } instead of
proceeding. The signin screen swaps to DeviceLockedPanel which shows:
- lock icon + headline + explanatory body
- amber countdown badge if a release is already in progress
- grey hint pointing to the email notification
- primary CTA to go back and sign in with the original account

Backend TODO: POST /api/devices/check-lock endpoint — same device-lock
query as login.post.ts but callable with a valid Supabase session token
(for email-login flow that bypasses /api/auth/login).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 00:37:22 +02:00
chahinebrini
edf047eacf feat(native): device-lock i18n keys + devices store type extensions
- Add auth.device_locked_* keys (DE/EN/FR): headline, body, countdown,
  email_hint, use_original CTA, back link
- Add devices.bound_badge + devices.release_* keys (DE/EN/FR) for the
  bound-device / release-flow in the Devices page
- Extend UserDevice interface with boundToPlan and releaseRequestedAt
- Add requestRelease + cancelRelease store actions calling the new
  POST /api/devices/:id/request-release|cancel-release endpoints

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 00:37:12 +02:00
chahinebrini
943afb827b feat(native): settings overhaul — DEV cleanup + notifications section
- Remove __DEV__ debug section from settings.tsx:
  Plan-Override-Row (4 rows: LLM, TTS, plan-override, realtime),
  PlanPickerSheetContent, planSheetRef, DEV_PLANS/DEV_PLAN_ACCENT,
  ActivityIndicator import. The debug.tsx page + realtimeDebug.ts store
  are kept — only UI entry points removed from settings.
- Wire notifications section: pushEnabled/streakReminderEnabled toggles
  + streakReminderTime picker (hour/minute scroll, quarters). State
  persisted in AsyncStorage via new stores/notificationPrefs.ts.
  setPushEnabled calls Notifications.requestPermissionsAsync() — if
  denied, toggle stays off. scheduleNotificationAsync is a TODO (no
  backend endpoint yet).
- Add _layout.tsx Stack.Screen entry for help/* route group.
- i18n: new keys settings.notifications_push_sublabel,
  notifications_streak_time, notifications_hour/minute,
  section_help, help_faq/contact/about/crisis + descs in DE/EN/FR.

TODO: expo-notifications scheduleNotificationAsync wiring once
backend streak-reminder endpoint exists.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 23:42:33 +02:00
chahinebrini
8851f36f65 fix(native): protectedDevices store — unwrap response shape
Backend GET /api/devices/protected returns:
  { success, data: { devices, plan, max, isLegend } }

apiFetch already unwraps `data`, leaving us with the object
{ devices, plan, max, isLegend } — not an array.

Old code did `Array.isArray(res) ? res : []` on that object, which
silently fell through to an empty list. Effect: enrolled protected
devices (Mac/Windows) never appeared in the Geräte screen even though
the DB row existed and the API responded correctly.

Fix: read res.devices instead of assuming the response is the array.
2026-05-15 23:02:26 +02:00
chahinebrini
db377da7ce fix(native): realtime disconnect bug — accessToken callback + AppState handler
Bug (diagnosed by backyard, see project_session_2026-05-15_push.md):
- Manual `supabase.realtime.setAuth()` calls in subscribe-hooks set
  `_manuallySetToken=true` internally, blocking the automatic token-refresh
  on heartbeat. After ~1h the cached access_token expires → Postgres-Changes
  silently stop arriving (channel still shows "joined" but no events).
- Plus: no AppState handler → no Foreground-Reconnect trigger after
  Background-kill of WebSocket.

Fix A — lib/supabase.ts: createClient now passes a `realtime.accessToken`
async callback that returns the current session token. Heartbeat picks
fresh tokens automatically, no manual setAuth needed.

Fix A — all 5 manual `supabase.realtime.setAuth()` calls removed from
useChatRealtime, useCommunityRealtime, useDomainSubmissionRealtime,
stores/notifications. Token is handled by the callback now.

Fix B — _layout.tsx: AppState listener calls
supabase.auth.startAutoRefresh()/stopAutoRefresh() — official Supabase RN
pattern. On Foreground-Return, onAuthStateChange fires TOKEN_REFRESHED →
realtime.setAuth gets called internally.

Required for upcoming Auto-Detect protected-device handshake (Realtime
channel listens on protected_devices status transitions pending→active).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-15 21:48:54 +02:00
chahinebrini
d55cbc11b2 fix(native): mail-sheet modal-conflict + google-oauth picker + feed-bg contrast
- mail/MailAccountSettingsSheet: handleSaveTitle + handleSavePassword now
  dismiss sheet FIRST, then trigger parent SuccessAlert via setTimeout(350ms).
  Fixes iOS "already presenting" crash + page-freeze when editing mailbox name.
  Also fixes double-click-needed UX bug.
- stores/auth: signOut adds WebBrowser.coolDownAsync() to clear OAuth cookies.
  signInWithOAuth for Google adds prompt=select_account — forces account-picker
  on every sign-in attempt instead of auto-reusing previous account.
- app/(app)/index: feed page uses colors.groupedBg instead of colors.bg —
  matches iOS Mail/Messages list-style, post-cards stand out clearer.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-15 21:16:34 +02:00
chahinebrini
804d4a5861 feat(native): device-info api headers + DeviceLimitSheet UI + profile i18n sweep
- lib/api.ts: sends x-device-name + x-device-model + x-device-os headers
  (cached per session, URL-encoded). Backend persists into user_devices for
  visual differentiation in DeviceLimitSheet.
- DeviceLimitReachedSheet: renders name (primary) + model · OS-version
  (secondary), "Dieses Gerät"-Pill on isCurrent. Stale phantoms become
  distinguishable.
- Profile i18n sweep: 8 keys × 3 languages = 24 fixes — all {{var}} placeholders
  switched to %{var} matching i18next config (Vue-i18n leftover from Nuxt-port).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-15 21:16:22 +02:00
chahinebrini
10219e5f68 feat(i18n): add french as 3rd app language
Für Test-Kunden: vollständige fr-Locale mit Tonalität für Recovery-Kontext
(„addiction", „Série", „Période de blocage"). Eigenname „Lyra" und Brand
„Rebreak" bleiben unübersetzt.

- locales/fr.json: 1:1 key parity zu de.json/en.json (UI-Agent-Output)
- lib/i18n.ts: fr in resources + initialLng-Detection (Device-Locale fr → fr)
- stores/language.ts: AppLanguage union ergänzt um 'fr', init-Logic + persistence
- app/settings.tsx: Sprach-Picker mit dritter Option Français
- de.json/en.json: language_fr-Label + language_desc trilingual

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-15 11:03:37 +02:00
chahinebrini
a0d67f33a8 feat(native): realtime debug page + protected-devices array guard
- stores/realtimeDebug.ts: neuer DEV-only Zustand-Store mit connection-state,
  reconnect-counter, token-expiry-countdown, channel-liste, rolling log-buffer
  (last 100 events). Hookt Phoenix-Socket open/close/reconnect + Channel-subscribe.
- _layout.tsx: initRealtimeDebug() im __DEV__-Block beim App-Start.
- debug.tsx: zwei neue Cards (RealtimeStatusCard + RealtimeLogCard) mit
  1s-Tick-Refresh, Copy + Clear Buttons. Settings-Entry 'Realtime connection (DEV)'.
- protectedDevices.ts: Array.isArray-Guard für apiFetch-Response — verhindert
  TypeError 'devices.filter is not a function' wenn Backend Non-Array zurückgibt.

Diagnostik-Tool für Realtime-Disconnect-Bug bei lange eingeloggten Usern.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 22:15:57 +02:00
chahinebrini
fc69a14f25 feat(mail): outlook oauth — full end-to-end (backend + daemon + frontend)
Microsoft hat App-Passwords für consumer-Outlook im September 2024 abgeschaltet.
Diese Welle bringt OAuth2/XOAUTH2-Support als zweiten AuthMethod-Pfad — Gmail/
iCloud/GMX/Yahoo bleiben unangetastet auf App-Password.

Backend (rebreak-backend):

- POST /api/mail/oauth/microsoft/init: PKCE-Flow-Start, generiert
  code_verifier + Authorization-URL, persistiert pending state mit TTL
- POST /api/mail/oauth/microsoft/callback: Token-Exchange (PKCE, kein
  client_secret weil Public Client), id_token-Decode für Email, MailConnection
  upsert mit auth_method='oauth2_microsoft' + encrypted Tokens
- Token-Refresh-Util backend/server/utils/ms-oauth.ts + DB-Function
  refreshAndSaveTokens(connectionId, clientId) mit optimistic-concurrency-
  Race-Condition-Schutz (UPDATE WHERE oauth_token_expiry = <gelesener-wert>,
  bei affected_rows=0 → frischen Wert lesen statt nochmal refreshen sonst
  invalid_grant via Token-Rotation)
- Neue Tabelle oauth_pending_states (TTL via createdAt + Cleanup-Job-TODO)
- [id].delete.ts: echter OAuth-Disconnect — DB-Token-Löschung + Audit-Log
  (MS hat keinen Drittanbieter-Revoke-Endpoint, daher User-Information-Pflicht
  per Frontend-Modal, siehe DSB-Memo Section 5.1)
- Consent-Gate auch in scan.post.ts + scan-internal.post.ts (Cron-Trigger
  war ohne Consent-Check = DSGVO-Lücke, jetzt geschlossen mit
  skippedNoConsent-Field in Response)

IDLE-Daemon (backend/imap-idle/index.mjs, mo):

- XOAUTH2-Auth-Branch via getCredentialsForConnection() — wenn
  auth_method='oauth2_microsoft', Token-Expiry-Check (<5min remaining →
  proaktiver Refresh), sonst decrypted accessToken zu ImapFlow
- AUTHENTICATIONFAILED-Recovery: bis 3× reaktiv refresh + reconnect, danach
  last_connect_error='auth_revoked' (kein Endlos-Loop)
- IDLE_RENEW_INTERVAL_MS = 10min — passt für MS 29min-Timeout (gleich wie
  Gmail/iCloud)
- Consent-Pause: Connections mit consent_at=null laufen IDLE weiter (für
  exists-Event-Wiederaufnahme), aber triggerScan() ist deaktiviert bis
  consent erteilt
- start-idle-staging.sh: MS_OAUTH_CLIENT_ID explizit weiterleiten in den
  inneren bash -c-Block (war Infisical-Var, ging aber durch strict-mode
  verloren)

Frontend (rebreak-native-ui):

- Outlook-Tile re-aktiviert (war disabled mit "Kommt bald" seit Sept-2024-
  Awareness), authMethod-Discriminator löst statt Email+Pw-Form den
  OAuth-Flow aus
- ConnectMailSheet: neuer view-State 'oauth_warning' (Outing-Effekt-Hinweis
  per Hans-Müller-Memo Section 6.1) + 'oauth_pending' (Browser-Step-Spinner)
- Deep-Link-Handler app/auth/mail-oauth-callback.tsx — auto-registriert
  durch expo-router-File-Routing, kein Native-Rebuild (scheme 'rebreak'
  schon im app.config.ts)
- mailConnectDraft-Store: pendingOAuthConnectionId für Title-Edit-Sheet
  direkt nach Connect
- MailAccountCard: Password-Row hidden für OAuth-Connections, Post-Disconnect-
  Modal mit MS-Account-Anleitung (DSB-konform — kompensiert fehlenden
  Drittanbieter-Revoke-Endpoint mit User-Information)

Hans-Müller-DSB-Memo (mail-outlook-oauth-dsgvo-review.md):

- Section 4.1 Datenschutzerklärung-Textbaustein: "Wir widerrufen den Token
  aktiv bei Microsoft"-Satz raus (war faktisch falsch — MS hat keinen
  Drittanbieter-Revoke). Neuer Wortlaut: DB-Löschung + User-Anleitung
  account.microsoft.com → Sicherheit → App-Berechtigungen
- Section 4.1: User.Read-Scope offen dokumentiert mit Datenminimierungs-
  Klausel (Scope breiter, wir nutzen NUR Display-Name + Email-Claim)
- Section 5.1: ehrliche Doku dass MS keinen RFC-7009-Revoke hat
- Section 9 Anwalts-Themen: neue Frage 5 zur Art. 17-Erfüllung trotz
  fehlendem MS-Revoke

Architektur-Eigenschaften:

- Generisches AuthMethod-Framework — Gmail/iCloud/Yahoo können später als
  reine Config-Erweiterung OAuth bekommen, kein Refactor nötig
- Token-Encryption via bestehendes crypto.ts (AES-256-GCM, Key aus
  Infisical)
- Consent-Gate konsistent: ConnectMailSheet-Consent-Step VOR Provider-
  Auswahl (Frontend), backend-Endpoint 412 wenn consent fehlt, Daemon +
  Scan-Endpoints pausieren bei consent_at=null

Open follow-ups:

- oauth_pending_states-Cleanup-Cron für abgelaufene Entries (TODO im
  Backend-Code dokumentiert)
- Anwalts-Klärung Hans-Müller Section 9 (DPA-Anspruch ohne MS-Lizenz +
  Art. 17 mit User-Information statt Revoke-Endpoint)
- TIA (Transfer Impact Assessment) für MS-Sub-AV — Hans-Müller-Draft-Aufgabe
- Outlook-Tile-Wieder-Aktivierung ist live, aber Phase-1-Production-Test
  steht aus (User Test auf iPhone nach Pipeline-Deploy)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 21:04:14 +02:00
chahinebrini
b7909d77e4 feat(mail): custom title + settings collapsible + stats charts + provider filter
Mail-Page-Refactor — Privacy-friendly + DiGA-tauglich:

- Custom title pro mail-connection (z.B. "Privat-Gmail" statt voller E-Mail).
  Memory-Pattern: Anonymität via Nickname jetzt auch für Mail-Adressen
  sichtbar, Datenminimierung. Title nullable, Fallback auf Email-Domain.
- Schema-Migration mail_connection_title (additiv, NULL default für Bestand)
- Endpoint PATCH /api/mail-connections/:id mit title-Validation (max 60,
  trim, leerer String → NULL)
- "Passwort ändern"-Collapsible → vollwertige "Einstellungen"-Sektion:
  Title editieren · Email read-only · Passwort neu setzen · Verbindung
  trennen (mit Confirm-Dialog)
- EditMailTitleSheet als FormSheet-Pattern für Title-Edit
- mailConnectDraft-Store kriegt Title-Feld für Pre-Fill bei Re-Open

Zwei neue Stats-Charts auf der Mail-Page:

- MailBlockedByDayChart — 30-Tage-Bar-Chart, Plain-View-Bars (Pattern wie
  Sparkline-Profile), Empty-State bei 0 Cooldowns
  · Backend: GET /api/mail/stats/blocked-by-day?days=30
- MailDistributionChart — Half-Donut via react-native-svg, Top-5 Connections
  + "Sonstige", rendert nicht bei ≤1 Connection
  · Backend: GET /api/mail/stats/blocked-by-connection

Activity-Log mit Provider-Filter:

- Filter-Chips Mo Gmail/Outlook/iCloud/etc. über bestehendem Activity-Log
- GET /api/mail/results?provider=X (war vorher hardcoded all)
- Endpoint-Naming-Fix in useMailResults (war /api/mail/blocked, jetzt
  korrekt /api/mail/results — UI-Agent hatte falschen Path geraten)

Backend-Side-Effects:

- imap-providers util resolveProviderMeta(host) — gibt {provider, label,
  isCustomDomain} zurück, von 3 Endpoints konsumiert
- /api/mail/status erweitert: title, provider, providerLabel,
  isCustomDomain im Account-Shape
- /api/mail/results erweitert: connection-Sub-Objekt pro Entry +
  provider-Filter-Query

Open follow-ups (TODOs):

- deleteOldMailBlocked-Cron löscht <24h → Bar-Chart-Daten weg. Retention
  auf 90 Tage hochsetzen oder Cron stoppen.
- POST /api/mail/connect könnte die neue connection.id im Response
  mitliefern → Title-PATCH direkt ohne Extra-GET (UI-Agent-Empfehlung).
- /api/mail/status zeigt nur active Connections — paused mit Title wären
  unsichtbar. Entscheiden.

18 neue i18n-Keys (mail.title_*, mail.settings_*, mail.row_*,
mail.disconnect_confirm_*, mail.stats.*, mail.filter.all) in DE + EN.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 19:06:01 +02:00
chahinebrini
0ab635c74a feat: art-9 consent flow + outlook-oauth schema + cooldown patterns + mail draft persist
DSGVO Art. 9 — Compliance-Gap im Mail-Connect-Flow geschlossen (Hans-Müller-DSB
hat den Gap für Gmail/iCloud/GMX identifiziert, schon vor Outlook-OAuth-Pflicht):

- Schema: mail_connections.consent_at + consent_version + consent_ip_address;
  neue consent_logs-Tabelle für Audit (grant + revoke append-only)
- Endpoints:
  - POST /api/mail-connections/consent (Bulk-Array für Re-Consent, partial-fail
    wirft sofort = DSGVO-sicher gegen silent-skip fremder IDs)
  - POST /api/mail-connections/:id mit consent-gate (412 wenn consentVersion fehlt)
  - DELETE /api/mail-connections/:id mit Widerruf-Log (OAuth-Token-Revoke als
    TODO für mo Phase 2)
  - GET /api/mail-connections/pending-consent — listet Bestands-Connections
    mit consent_at=NULL für Re-Consent-Modal
- Account-Lösch-Bug fix: deleteAllMailConnections() war in user/delete nicht
  eingebunden — Verbindungen blieben als Waisen
- Frontend:
  - ConnectMailSheet: neuer Consent-Step VOR Provider-Grid (view-Machine
    consent → grid → form), exakter Hans-Müller-Wortlaut für Art. 9 Abs. 2
    lit. a Einwilligung
  - MailConsentReminderSheet: Re-Consent-Modal beim App-Open für Bestands-User
  - Stores mailConsent + mailConnectDraft (letzterer fixt Bug: Email/Provider
    ging verloren wenn User Browser für App-Pw-Generierung öffnete)
  - 12 neue i18n-Keys mail.consent.* in DE + EN
- Versionierter Consent-Text: art9-mail-v1-2026-05-13 (Bump bei Text-Änderung
  triggert Re-Consent für alle)

Outlook-OAuth Schema (Phase 0 — additiv, Endpoints kommen später):

- mail_connections: auth_method (default 'app_password' → keine Bestands-
  Connection bricht), oauth_access_token, oauth_refresh_token,
  oauth_token_expiry, oauth_scope
- Encryption via bestehendes server/utils/crypto.ts (AES-256-GCM, Key aus
  Infisical)
- Plan-Doc backend/docs/mail-outlook-oauth-plan.md (mo)
- DSB-Review backend/docs/mail-outlook-oauth-dsgvo-review.md (Hans-Müller):
  MS als Sub-AV via DPA Sep 2025, EU Data Boundary seit Feb 2025; 5 Pflicht-
  Aufgaben + Anwalts-Klärung zu DPA-Anspruch ohne MS-Lizenz

Profile — Cooldown-Pattern-Analysis als Collapsible:

- CooldownPatternAnalysis: 24h-Uhrzeit-Heatmap, Mo–So-Wochentag-Histogramm,
  Top-5-Reason-Wortcloud mit Stop-Words-Filter, Cancel-Rate-Anzeige
- DiGA-relevant: NLP läuft client-side, reason-Texte verlassen das Device
  nicht (gut für DSB-Akte)
- useProfileData: useCooldownHistoryFull (limit=100) für Pattern-Analyse
- Neutral formuliert, kein Stigma, alle Headings als Frage

Plan-Docs (kein Code):

- backend/docs/mail-custom-keywords-plan.md — Pro/Legend Custom-Keyword-Filter
  (3.25 PT MVP, user-scoped, Body-Match in Phase 2)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 16:35:18 +02:00
chahinebrini
01d515d137 feat(rebreak-native): persistent FaceID-sign-in + iOS-grouped UI + Outlook guard + sparkline cooldowns
Auth / FaceID — eingeloggt bleiben funktioniert jetzt:
- AppLock-Init idempotent: late re-init durch router.replace-Re-Mount behält
  locked-State (fixt Endlosschleife: unlock → re-mount → init reset → lock)
- LockScreen-Auto-Prompt nur wenn AppState=active (verhindert silent FaceID-
  Fail wenn LockScreen während background-Event mountet — User sah dann nur
  Fallback-Button)
- index.tsx: wenn Session schon in AsyncStorage liegt → router.replace zu /(app),
  Landing wird übersprungen; early-return nach allen Hooks (Rules of Hooks)
- WebBrowser.dismissAuthSession vor openAuthSessionAsync (verhindert
  "Another web browser is already open" nach abgebrochenen OAuth-Flows)

UI — iOS-Grouped-Look auf Settings + Profile:
- Neue Theme-Tokens groupedBg (#F2F2F7 / #000) + card (#fff / #1c1c1e),
  identisch zu Apples systemGroupedBackground / secondarySystemGroupedBackground
- settings.tsx + profile/index.tsx + profile/[userId].tsx: Page-BG → groupedBg
- StreakSection / UrgeStatsCard / DemographicsAccordion / StatsBar /
  ApprovedDomainsList: Card-BG colors.surface → colors.card

Mail-Connect — Outlook-Tile entschärft:
- Microsoft hat App-Passwords für consumer-Outlook (.com/hotmail/live/msn) im
  September 2024 abgeschaltet, der bisherige Guide-Flow ist seit ~8 Monaten
  wirkungslos → AUTHENTICATIONFAILED
- Tile bleibt sichtbar mit opacity 0.45, "Kommt bald"-Sub-Label, disabled=true
- Provider-Typ um disabled? + disabledLabelKey? erweitert (wiederverwendbar)
- Backend-OAuth-Plan unter backend/docs/mail-outlook-oauth-plan.md (mo)
  → Generisches AuthMethod-Framework (app_password | oauth) geplant

Profile — Cooldown-Verlauf als Sparkline statt Endlos-Liste:
- 8 Wochen-Buckets, Bar-Höhe nach Frequenz (cap 5/Woche), leere Wochen als
  2px-Flatlines
- Sub-Label: "{n} Cooldowns in 8 Wochen · Ø 1 pro {avg} Wochen · zuletzt {date}"
- Neutral formuliert (Sucht-/Stigma-Sensibilität: Cooldown = Schutz-Pause,
  kein Rückfall)
- useProfileData.ts liefert rawStartedAt (ISO) zusätzlich zum formatierten Wert
- i18n-Keys unter profile.cooldown.* in DE + EN

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 16:15:54 +02:00
chahinebrini
4a17c7942d fix(rebreak-native): don't crash on old dev-clients missing ExpoLocalAuthentication
appLock.ts is imported from the root layout, so an unguarded top-level
`import * as LocalAuthentication from 'expo-local-authentication'` crashes the
whole app at launch on a dev-client built before the dependency was added
("Cannot find native module 'ExpoLocalAuthentication'"). Load it via a guarded
require; if absent → app lock reports `available: false`, everything else runs.
Real builds (EAS / fresh prebuild) ship the module and work normally.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 19:57:53 +02:00
chahinebrini
aa9466aa92 feat(rebreak-native): Face ID app lock (opt-in)
Privacy/stigma layer on top of the authenticated Supabase session — re-auth on
open so nobody but the user can open Rebreak. Not a login replacement.

- expo-local-authentication; NSFaceIDUsageDescription in app.config
- stores/appLock.ts: persisted `enabled` pref, in-memory `locked`, device-
  capability check (`available`), device-passcode fallback on biometric failure
- AppLockGate wraps the root layout: locks immediately on `background` (not
  `inactive` → app-switcher peek doesn't lock), renders LockScreen while
  `enabled && locked && session`
- LockScreen: dark brand screen, auto-prompts on mount + on return from
  background, "Abmelden" escape hatch (clears session → fresh login next launch)
- Settings: new "Sicherheit" section, native UISwitch; enabling requires a
  successful biometric prompt first; row disabled + explained when device has no
  biometrics/passcode
- de/en strings

Per product call: the lock gates the whole app incl. SOS (SOS already requires
an authenticated user, so there's no unauthenticated path to carve out).

Cold-start: appLock init blocks the splash → `locked` is set before first paint,
no flash of unlocked content. ios/ is gitignored so EAS prebuilds the new module.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 19:41:56 +02:00
chahinebrini
51697c3aa4 feat(tier): plan-change briefing sheet + over-limit cards (Phase 2 UI)
- components/plan/PlanChangeSheet.tsx — upgrade/downgrade briefing per pricing-tiers.md §4
  (fetches GET /api/plan/change-preview; gains/keeps/changes; recovery-safety line;
  billing hint w/o purchase button; CTA row, no 'are you sure?' interstitial)
- debug.tsx: PlanOverrideToggle routes every flip through PlanChangeSheet first
- devices.tsx + protectedDevices.ts: 'degraded' status (red, inline 'protection expired —
  remove the profile yourself' hint, no green checkmark); maxProtectedDevices limit hint
- mail.tsx + MailAccountCard.tsx + useMailStatus.ts: over-limit banner + paused-account
  greyed-out + PausedBadge (all defensive — only shows if backend sends the  field)
- blocker.tsx: free-tier transparency hint ('Grundschutz aktiv — voller Schutz: Pro/Legend')
  + custom-domain over-limit banner
- locales: plan.change.* + plan_limit.* (de + en)

tsc clean. Backend side (GET /api/plan/change-preview, paused/degraded fields) in progress
in parallel — UI built defensively to work before it lands.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 16:21:47 +02:00
chahinebrini
dd3d8c6667 feat(devices): wire Windows DoH AddWindowsSheet into devices screen
- AddWindowsSheet: 5-step Lyra flow (download → datei → shield-check → wifi → done)
- devices.tsx: Windows button enabled, opens AddWindowsSheet
- protectedDevices store: enroll() takes platform 'mac' | 'windows'
- AddMacSheet: pass 'mac' to enroll()

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 13:44:41 +02:00
chahinebrini
6700391eed feat(devices): Settings → Geräte UI + AddMacSheet 3-step Lyra flow
Frontend:
- New devices.tsx: section 'this device' + 'protected devices' + Legend CTA
- AddMacSheet: label → Lyra-onboarding (4 steps) → success
- protectedDevices store (Zustand): load, enroll, confirmInstalled, remove
- Locale strings DE + EN (devices namespace, 36 keys each)
- Path-fix: /api/devices/protected (was /api/devices) + /api/devices/:id/revoke

Free/Pro see upgrade-CTA, Legend see add-Mac. Windows button shown disabled (soon).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 04:06:49 +02:00
chahinebrini
5d6c322129 wip: KeyboardAwareSheet migrations + Snake/Tetris UI + iron.png + useMe live-update
Sheets via neuer KeyboardAwareSheet-Composable (in Modal pattern, auto-grow
mit Tastatur, paddingBottom-Lift): EditMail, AddDomain, CreateRoom, ConnectMail.
GameOverScreen behält Spring-Slide-In, nutzt RN Keyboard.addListener für Lift.

- KeyboardAwareSheet.tsx — universal modal with sheet-grow + keyboard-padding
- react-native-keyboard-controller installiert + KeyboardProvider in Root
- Snake: time + ScoreProgressBar + useSnakeSounds (haptic, audio TODO)
- Tetris: title weg, Buttons zentriert, kein Pressable mit style-fn
- DPad-Buttons 60→48, more bg, no scale
- useMe: pub-sub listener pattern für app-weite avatar/nickname-Updates
- dm.tsx: resolveAvatar wrap (iron.png-Warning)
- Mail-error-humanizer + locales

Recovery-Doc-Update in docs/internal/RECOVERY_LOG_2026-05-10.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 23:59:25 +02:00
chahinebrini
594a43cbf9 feat(theme): Dark Theme — global color-system + Wave 1 screens
Theme-switch in Settings (System/Light/Dark) jetzt App-weit wirksam für die
Core-Screens. Wave 2 dokumentiert (siehe unten).

Color-System:
- lib/theme.ts: refactored zu colors.light + colors.dark (gleiche keys)
  Light: bg #fff, surface #fafafa, surfaceElevated #f5f5f5, border #e5e5e5,
         text #0a0a0a, textMuted #737373
  Dark:  bg #000, surface #1c1c1e, surfaceElevated #2c2c2e, border #38383a,
         text #fff, textMuted #8e8e93
  brandOrange unverändert #007AFF (iOS system blue)
  success/error variieren (light: #16a34a/#dc2626, dark: #30d158/#ff453a)
- legacy `colors` export bleibt als Light-Fallback für nicht-migrierte Files
- new `useColors()` hook → liest aktiven scheme aus useThemeStore

stores/theme.ts:
- Appearance.addChangeListener für live System-Theme-Updates (User schaltet
  iOS Dark/Light → App reagiert sofort ohne Reload)

Wave 1 — migrated Files (Core Screens):
- app/_layout.tsx + app/(app)/_layout.tsx + app/(app)/index.tsx (root + home)
- app/settings.tsx (full theme-aware inkl. TrueSheet)
- app/profile/index.tsx (bg + dividers)
- app/devices.tsx (bg, surface, border, icons)
- app/lyra.tsx (chat container, backdrop, bubbles, ThinkingDots, LoadingPulse)
- components/AppHeader (Nativewind classes ersetzt durch theme-aware Styles)
- components/header/HeaderDropdownMenu
- components/profile/* (ProfileHeader, StatsBar, StreakSection, UrgeStatsCard,
  ApprovedDomainsList, DemographicsAccordion)

Wave 2 (TODOs für separate Session):
- app/urge.tsx (~20 hardcoded colors, größter Screen)
- app/room.tsx, app/dm.tsx, app/(app)/chat.tsx, app/(app)/mail.tsx, app/(app)/coach.tsx
- app/games.tsx, app/profile/[userId].tsx
- Nativewind classes in PostCard, ComposeCard, PostCardSkeleton, NotificationsDropdown

StatusBar style dynamisch synchronisiert (light bei dark-mode, dark bei light).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 22:15:55 +02:00
chahinebrini
d857d2a7aa feat(devices): global Device-Limit-Reached handler + recovery sheet
Backend wirft 403 device_limit_reached für ALLE auth'd endpoints sobald User über
plan-limit ist. Bisheriges Frontend hat silent gefailt → Profile/Notifications/etc
zeigten nichts mehr, User war verwirrt.

Now:
- lib/api.ts: 403 device_limit_reached intercepten, parse error.data.devices,
  trigger useDeviceLimitStore.show()
- stores/deviceLimit.ts: Zustand store (visible, devices, max, plan, show/hide)
- components/DeviceLimitReachedSheet.tsx: TrueSheet (UISheetPresentationController)
  Auto-präsentiert wenn store visible, zeigt device-list mit trash-button per Eintrag,
  DELETE /api/devices/:id mit skipDeviceHeader: true (sonst circular 403)
- app/_layout.tsx: <DeviceLimitReachedSheet /> als globaler overlay vor <Stack>
- i18n: device_limit_* keys DE+EN

UX: User sieht jetzt sofort native bottom-sheet mit erklärung + actionable
device-list statt silent fail. Auto-close wenn devices.length < max nach delete.

TS-fix: detents={['auto', 1] satisfies SheetDetent[]}, onDidDismiss statt onDismiss
(prop heißt anders in TrueSheet API).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 21:27:33 +02:00
chahinebrini
2f5d0382f0 feat(profile,devices): real DB wiring + Devices-Settings migration
Profile (rebreak-native-ui):
- New hook hooks/useProfileData.ts (143 LOC, 4 hooks):
  useSocialStats, useApprovedDomains, useCooldownHistory, useSosInsights
- app/profile/index.tsx: alle DUMMY_* constants entfernt → live data via hooks
- PATCH /api/profile/me/demographics nun wired in onChange (war TODO-only)
- DELETE /api/profile/me/demographics für revoke-consent
- POST /api/profile/me/diga-banner-dismiss

Devices (rebreak-native-ui):
- New app/devices.tsx push-page: slot-counter, progress-bar, device-list mit
  trash-button (gesperrt für isCurrent)
- New lib/deviceId.ts: persistent device-ID via expo-application
  (getIosIdForVendorAsync / getAndroidId) mit AsyncStorage-UUID-fallback
- New stores/devices.ts: Zustand store (loadDevices, removeDevice, ensureRegistered)
- lib/api.ts: x-device-id + x-platform headers bei jedem Backend-Call
  (skipDeviceHeader option für Bootstrap-register)
- app/settings.tsx: Geräte-Row aktiv (push to /devices) statt soon-flagged
- locales: 14 neue settings.devices_* keys DE+EN

Backend-Status: alle Devices-Endpoints existieren (GET /api/devices, POST /register,
DELETE /:id). Pending: GET /api/profile/me/demographics für reload-state-fetch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 20:47:30 +02:00
chahinebrini
fb97cda63d fix(ui): i18n device-locale + share-pill rounded-full + Profile debug-marker
stores/language.ts:
- init() override AsyncStorage-Wert nicht — wenn nichts gespeichert,
  i18n bleibt bei deviceLocale (von lib/i18n.ts via Localization.getLocales).
  Vorher: forced 'en' default obwohl App auf DE.

ComposeCard share-button:
- borderRadius:12 + height:50 → rounded-full px-5 h-11 (44pt)
- text-base → text-sm. Pill-Pattern wie Pre-Session.

app/profile/index.tsx:
- AppHeader title "Profil" → "Profil DEBUG-2300" — TEMPORARY marker
  zur Verifikation ob File geladen wird (user-suspect: routing zu altem
  File). Wird nach Test wieder entfernt.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 23:10:20 +02:00
chahinebrini
922d5dc876 fix(stores): lazy-init theme/language — kein native API-Call beim Module-Load
Hermes wirft "property is not writable" + "Cannot read 'default' of undefined"
wenn beim Module-Load eines Stores native APIs aufgerufen werden bevor
die Bridge bereit ist. Die initialen Werte:

- theme: colorScheme war resolveColorScheme('system') → ruft Appearance.getColorScheme()
  zur Module-Load-Zeit. Jetzt: 'light' als default; korrekter Wert kommt
  asynchron via init().
- language: language war (i18n.language === 'de' ? 'de' : 'en') → liest
  i18n.language zur Module-Load-Zeit (Risiko falls i18n noch nicht init).
  Jetzt: 'en' default; korrekter Wert kommt via init() das i18n.changeLanguage
  callt.

Init-Calls passieren in _layout.tsx useEffect — nach Bridge-bereit, nach
Hermes-Eval-Phase. Sicher.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 22:03:21 +02:00
chahinebrini
9ccd0bd334 feat(settings): Theme + Language + Lyra-Voice Picker; Dropdown-width 280→170pt
Settings-Page (alles auf einen Rutsch):
- Theme-Picker (System/Hell/Dunkel) — neuer useThemeStore (Zustand) mit
  AsyncStorage @rebreak/theme persist; init-call in app/_layout.tsx
  parallel zu auth-init
- Language-Picker (DE/EN) — neuer useLanguageStore mit i18n.changeLanguage
  + AsyncStorage @rebreak/language persist; init beim App-Start
- Lyra-Voice-Picker (Legend-only) — UI live, lokal selectable. BACKEND-GAP:
  PATCH /api/profile/me/lyra-voice fehlt (demographics.patch akzeptiert
  Voice-ID nicht via zod). UI bleibt funktional, Persist erst wenn
  Endpoint da ist.
- Logout neutral (kein style:'destructive' am Alert-Button)
- Coming-Soon-Banner entfernt (war veraltet, Settings ist live)

Dropdown:
- HeaderDropdownMenu minWidth 280→170, Item-Labels numberOfLines={1}
  ellipsize bei langen Strings

Locales:
- 11 neue Keys fuer Theme/Lang/Voice-Picker (DE+EN gepflegt)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 21:22:32 +02:00
RaynisDev
b58588cf3c initial commit: rebreak-monorepo (RN app + standalone Nitro backend) 2026-05-06 07:13:43 +02:00