68 Commits

Author SHA1 Message Date
chahinebrini
0bad2185ec chore(ios): set ios.buildNumber to 3 (matches Android versionCode) for TestFlight
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 22:42:50 +02:00
chahinebrini
af87893eb9 fix(android): self-heal — restart VpnService if it should be running but isn't
After an APK reinstall (or an OS low-memory kill that START_STICKY didn't recover
promptly), the VpnService dies but `filter_enabled` stays true. isVpnEffectivelyOn
then reports vpn:true (from the flag) → tamperLock:true → lockedIn:true → the green
"protection active" card with no toggles, while in reality nothing is filtering.

New native reconcileVpn(): if `filter_enabled` && !RebreakVpnService.isRunning &&
VpnService.prepare()==null → startVpnService(). Wired into _layout.tsx enforceProtection()
(runs on launch / foreground / 15s poll), called before reading combined state. No-op
on iOS/web. If the VPN consent was revoked, isVpnEffectivelyOn already clears the flag,
so that case self-resolves too.

Net behavior: while `filter_enabled` is true (user hasn't exited via the cooldown),
the app keeps the VPN alive. Exiting still goes through the cooldown → forceDisable →
filter_enabled=false → reconcile leaves it off. DiGA-compliant.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 20:10:43 +02:00
chahinebrini
4492c7b265 feat(blocker): reactivation only re-arms the VPN/filter; a11y setup is first-time-only
The a11y (App-Lock) permission flow now runs only the first time the user turns
protection on. Reactivating after a cooldown / external disable just re-starts the
VPN/DNS filter — no a11y system prompt, no modal loop ("a11y can't be activated…").

- blocker.tsx handleActivateFamilyControls: no error modal when error === 'accessibility_pending'
  (we just opened the a11y settings — that's the feedback; tapping again re-opens, no loop).
- lib/protection.ts getCombinedState: "active" = urlFilter on (App-Lock is optional hardening,
  not a precondition); "recoveringFromBypass" now means urlFilter is OFF while the backend
  says it should be on (a real bypass), instead of "lock is off".
- blocker.tsx recoveringFromBypass alert: offers "turn back on" → activateUrlFilter (VPN),
  not activateFamilyControls.
- _layout.tsx bypass re-arm (enforceProtection fallback + onBypassNotificationTap):
  protection.activate() instead of activateFamilyControls().
- new i18n keys: blocker.protection_off_title / protection_off_message / reactivate_btn.

JS-only (hot-reloadable).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 18:46:21 +02:00
chahinebrini
3c2aee7bda fix(android): tamper-lock can't linger armed while protection is off (stuck "locked" UI)
Repro: after a reinstall / external VPN-revoke, `filter_enabled` flipped to false
but `tamper_armed` stayed true. Result: buildDeviceState reported tamperLock:true
purely from `tamper_armed` → UI mapped that to appDeletionLock:true → lockedIn:true
→ showed the green "protected & locked" card with no toggles → no way to reactivate.
(The a11y service didn't block — handleProtectedSettingsBlock checks isProtectionEnabled
— but it kept logging every settings-navigation, wasting CPU.) "Armed but disabled"
is an invalid state.

- RebreakAccessibilityService: top guard is now `if (!isTamperLockArmed() || !isProtectionEnabled()) return`
  — fully passive (no logging) whenever protection is off, regardless of a stale tamper flag.
- RebreakProtectionModule.buildDeviceState: tamperLock = tamper_armed && filter_enabled.
- RebreakProtectionModule.isVpnEffectivelyOn (revoke branch) and RebreakVpnService.onRevoke
  now clear `tamper_armed` together with `filter_enabled` — the two can't desync.
Self-heals: opening the blocker page after the update re-fetches state → tamperLock:false → toggles back.

Also: the tamper-block toast is now Lyra-voiced instead of a shield emoji (a real avatar
image isn't possible — Android 11+ ignores Toast.setView() for app toasts; lyra-persona
can refine the wording).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 18:34:45 +02:00
chahinebrini
fc7a243c9b refactor(android): a11y service is now tamper-lock only — no browser URL filtering
The AccessibilityService used to also do a browser-address-bar filter (read the
URL bar of Chrome/Firefox/etc., hash-match against blocklist.bin, GLOBAL_ACTION_BACK
on a hit) as a "layer 2" alongside the VpnService DNS filter. That's redundant
(the VPN catches everything network-level, in browsers AND apps), fragile (per-browser
view-IDs), and produced ghost-blocks (VPN off, a11y still blocking sites). The DNS
filter is the protection; the a11y service's only real value-add is tamper-resistance.

So the a11y service now does ONLY the tamper-lock, and only when the user has armed
"App-Lock": block opening protection-critical settings (disable the ReBreak VPN,
uninstall the app, disable the a11y service itself). Top-level guard is now simply
`if (!isTamperLockArmed()) return` — when App-Lock isn't armed the service is fully
passive. Getting out is still via the regular deactivation cooldown (which disarms
the tamper-lock and stops the VPN).

- RebreakAccessibilityService.kt: removed browser-URL extraction, BROWSER_PACKAGES,
  URL_BAR_IDS, hashList loading, throttle bookkeeping, the block-toast. Kept the
  settings-watchdog (it already covered VPN settings via VpnSettings/vpndialogs +
  the vpn-page keyword cluster) and adjusted its keyword lists to the new a11y
  service summary (old summary kept as a legacy fallback for stale installs).
- accessibility_service_config.xml: dropped browser packages + flagRequestEnhancedWebAccessibility.
- strings.xml (de+en): a11y permission copy reframed — it safeguards the VPN/uninstall,
  it doesn't filter your browser; ends with "you can always exit via the cooldown".
- lib/protection.ts: comment-only (activateFamilyControls logic unchanged).
- locales de/en: App-Lock card copy ("Familienzugriff aktiv" → "Verriegelt — ...",
  "...ReBreak oder den Filter im Impuls abschaltest"), genericised the iOS Screen-Time
  error string.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 17:42:05 +02:00
chahinebrini
a80cc8b08d fix(rebreak-native): track custom native module source (was swallowed by .gitignore)
apps/rebreak-native/.gitignore had bare `ios/` + `android/` patterns meant for the
Expo-prebuild output dirs — but with no leading slash they also matched
modules/rebreak-protection/{android,ios}, so the entire custom expo native module
(RebreakProtectionModule.kt, RebreakAccessibilityService.kt, RebreakVpnService.kt,
the DNS filter, the iOS NEFilter extension, podspec, ...) was never tracked. A
fresh clone / CI / `git clean` would lose it.

Anchor the prebuild patterns (`/ios/`, `/android/`), keep ignoring the module's
build artifacts (build/, .cxx/, .gradle/, Pods/), and commit the source.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 17:22:22 +02:00
chahinebrini
5291a8a95a fix(realtime): domainRealtime CHANNEL_ERROR — wrong filter column + missing publication
Two bugs caused the domainRealtime channel to fail with CHANNEL_ERROR and
reconnect-loop every 3s (which also dragged down the notifRealtime channel via
the shared websocket close):
  1. useDomainSubmissionRealtime.ts filtered domain_submissions on a column that
     doesn't exist (`submitter_id`) — the actual column is `user_id`. Postgres
     raised on the publication-side filter registration → CHANNEL_ERROR.
  2. rebreak.user_custom_domains was never added to the supabase_realtime
     publication — the channel also subscribes to that table. New migration
     20260511_fix_realtime_user_custom_domains adds it.

(Diagnosis via backyard agent against the self-hosted Supabase on the Hetzner box.)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 17:19:37 +02:00
chahinebrini
33f411ab55 feat(blocker): post-cooldown disable shows a11y-settings notice (DiGA — user must be able to fully exit)
After the cooldown elapses and forceDisable() runs (VPN off + tamper-lock
disarmed), Android's a11y service can't deactivate itself — surface a friendly
Alert routing the user to Settings → Accessibility so they can finish removing
protection. Wired into both the fetchState cooldown active→inactive transition
and the AppState 'active' check; idempotent via ref.

(Native side — disable() also disarms the tamper-lock, RebreakAccessibilityService
goes fully passive when neither tamper-locked nor enabled, syncBlocklist no longer
re-starts the VpnService when disabled — lives in the gitignored module/android dir,
not committed here.)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 17:12:39 +02:00
chahinebrini
aac709ec41 fix(android): forceDisable disarms the tamper-lock — protection stayed enforced after cooldown
When the cooldown elapsed and forceDisable() stopped the VPN, the tamper_armed
SharedPref flag was left set → the AccessibilityService kept enforcing protection
(e.g. blocked the user from turning the a11y service off in system Settings) →
the user couldn't actually get out of protection despite the cooldown elapsing.
forceDisable() now calls disarmTamperLock() before disable().

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 16:59:58 +02:00
chahinebrini
184a601616 fix(blocker): relabel deactivate button 'Wie funktioniert der Cooldown?' → 'Schutz deaktivieren' (+ lock-open icon), remove debug marker
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 16:51:09 +02:00
chahinebrini
4bed8a1e5a debug(blocker): temporary red marker at bottom of ProtectionDetailsSheet ScrollView (to diagnose the 'can't scroll to the bottom' report — revert after)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 16:48:38 +02:00
chahinebrini
d5b753d329 fix(blocker): trim ProtectionDetailsSheet bottom padding (was over-padded → empty space under the bottom button)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 16:46:57 +02:00
chahinebrini
4e73d2ac92 fix(blocker): ProtectionDetailsSheet — taller sheet (0.9) + bigger bottom padding so the bottom button clears the nav bar
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 16:44:26 +02:00
chahinebrini
6870f71265 feat(blocker): __DEV__ test-cooldown toggle (40s) + auto-disable on elapse + safe-area fixes for deactivation sheets
- protection.ts: setCooldownTestMode/getCooldownTestMode (AsyncStorage 'dev:cooldown-testmode');
  requestDeactivation sends testMode:true when on (__DEV__ only)
- debug.tsx: CooldownTestModeToggle (Switch) — '40s instead of 24h, staging only'
- useProtectionState.ts: wire applyCooldownDisableIfElapsed() — fires on cooldown
  active→false transition (guarded so no extra fetch per poll) + on AppState 'active';
  protection actually turns off when the (test-)cooldown elapses (the 'Step 5b' auto-disable)
- DeactivationExplainerSheet.tsx: useSafeAreaInsets — header paddingTop insets.top+14,
  ScrollView paddingBottom max(insets.bottom,12)+24; back btn Pressable→TouchableOpacity
- ProtectionDetailsSheet.tsx: ScrollView paddingBottom max(insets.bottom,16)+24 (was 40);
  backdrop + 'Fertig' Pressable→TouchableOpacity

tsc clean. (Note: 'sheet doesn't scroll' — the bottom content was being clipped under the
home indicator; the paddingBottom fix should resolve it. Broader UI polish deferred to a
separate session — Task #10.)

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:40:58 +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
16c2e40242 chore(android): versionCode 3 (build v0.1.0 vc3 — icon fix + updates)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 16:18:03 +02:00
chahinebrini
2dcff6408c feat(chat): redesign chat list + conversation view
- RoomCard / chat.tsx DmItem: cleaner list rows (48px avatar, minHeight 68,
  consistent padding, time next to name, TouchableOpacity)
- ChatBubble: timestamp inline under content (no absolute-position hack),
  borderRadius 20, 28px avatar, lighter backdrop
- ChatInput: surface bg, hairline-bordered input pill, 38px send button

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 15:52:45 +02:00
chahinebrini
7369912d60 feat(dev): switch plan-override to POST /api/dev/set-plan + add Settings debug row
debug.tsx: removed admin-403 special-case, calls /api/dev/set-plan directly.
settings.tsx: new PlanPickerSheetContent (TrueSheet, DEV-only) in debug section
with three plan options; uses same endpoint + invalidateMe().

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 15:51:14 +02:00
chahinebrini
a8e638ed88 feat(profile): replace system-crop with custom gesture-based AvatarCropSheet
Picker now uses allowsEditing:false + quality:1; picked URI routes through
AvatarCropSheet (Pinch+Pan via RNGH+Reanimated, square crop frame with
corner markers). manipulateAsync crop left as TODO — expo-image-manipulator
not yet installed; sheet passes URI through unchanged until then.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 15:51:09 +02:00
chahinebrini
14452b2a46 refactor(native): Pressable → TouchableOpacity sweep (style-fn swallows Android styles)
Alle <Pressable style={({pressed}) => ({...})}> ersetzt — style-Funktion
droppt auf Android (New Arch) intermittierend width/height, führt zu 0×0
unsichtbaren Elementen. TouchableOpacity mit activeOpacity ist stabil.

Außerdem übrige Pressables (plain style) aus components/ und app/
migriert sowie zwei überschüssige </View>-Tags in chat.tsx + RoomCard.tsx
entfernt die TS-Fehler verursacht haben.

64 Dateien, typecheck sauber.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 15:43:10 +02:00
chahinebrini
f6852be760 fix(native): useUserPlan derive from useMe (was a stale module cache)
useUserPlan had its own module-level cache + fetch-once-on-mount, separate
from useMe's invalidateMe(). So the __DEV__ tier-override toggle (which calls
invalidateMe()) never reached useUserPlan consumers → the app didn't react to
a plan switch. Now useUserPlan just reads me.plan from useMe → inherits its
live-invalidation, the toggle propagates everywhere.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 15:18:59 +02:00
chahinebrini
297ad7437b fix(android): adaptive icon — white bg + padded dark logo (was zoomed/clipped)
Old adaptiveIcon was a full-bleed dark logo on a #0a0a0a background → the
launcher mask cropped it ("zoomed in" look). Now: white background (matches
the Play Store listing icon look) + the same logo at ~62% on a transparent
canvas → mask has nothing to clip.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 15:06:12 +02:00
chahinebrini
385f0b42a9 fix(android): blocker toggles + invisible avatar + adaptive icon
- protection.ts: normalize Android device-state keys (vpn/accessibility/
  tamperLock) to the iOS-shaped names the UI reads (urlFilter/familyControls/
  appDeletionLock) — on Android the layers came back under different keys, so
  blocker.tsx saw all toggles as undefined → always off → optimistic toggle
  flipped back to off after enabling
- AppHeader.tsx: avatar/bell/back Pressable-with-style-fn → TouchableOpacity
  with plain style — style-fn was swallowing width/height on Android → 0×0
  + overflow:hidden → avatar invisible (same pattern as Mac-CTA fix 7d04e42)
- app.config.ts: adaptiveIcon.foregroundImage → padded adaptive-foreground.png
  (logo in ~66% safe zone, was full-bleed → clipped by launcher mask);
  icon → icon.png (clean 1024 opaque, was the 512px alpha variant)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 14:52:42 +02:00
chahinebrini
e9d34dbe78 feat(settings): subscription section + __DEV__ plan-override toggle
- settings.tsx: real "Abo" row showing current plan (Free/Pro/Legend badge),
  taps open a sheet explaining subscriptions are managed on rebreak.org
  (Linking.openURL → /account; TODO: gate for iOS App-Store submission per
  Apple 3.1.1 — no in-app purchase flow)
- debug.tsx: __DEV__-only plan-override toggle (free/pro/legend) via
  PATCH /api/admin/users/:id + invalidateMe(); shows admin-only hint on 403
- locales: settings.subscription_* keys (de + en)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 14:13:47 +02:00
chahinebrini
bcc6e5cba1 chore(android): versionCode 2, eas.json profiles, avatar PNG fix
- app.config.ts: android.versionCode 2 (was 1)
- eas.json: development/preview/production profiles, EXPO_PUBLIC_API_URL=staging,
  appVersionSource=local, autoIncrement=false
- avatars: switch DiceBear endpoint /svg -> /png — RN <Image> can't decode SVG,
  Hero-Avatars rendered transparent/blank on Android

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 14:00:02 +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
518510c088 feat(mail): IONOS-Detection + MX-Lookup-Fallback + humanisierte Error-Messages
- imap-providers: IONOS/1&1/1blu, msn.com, magenta.de, yahoo.co.uk, ymail.com, tutanota hinzugefügt
- detectImapProviderAsync: MX-Lookup-Fallback für Custom-Domains (IONOS kundenserver.de/ionos.de Pattern)
- connect.post.ts: nutzt jetzt detectImapProviderAsync statt sync-Variante
- ConnectMailSheet: rohe Server-Errors werden via humanizeMailError() + t() übersetzt
- useMailConnect: IONOS/t-online/freenet Domains in Client-Side-Detection ergänzt
- Locale de/en: provider_other, app_password_guide_other, host_unreachable, unknown Text präzisiert

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 05:15:29 +02:00
chahinebrini
7d04e42bb5 fix(devices): Mac CTA-button invisible — Pressable style-fn → TouchableOpacity
Memory feedback_known_ui_layout_bugs.md Pattern 5: Pressable with
style={({pressed}) => ({...})} is layout-poison in some RN-render-paths,
button collapses to 0-height and renders invisible. Windows-button right
below worked because it uses static style={{...}}.

TouchableOpacity gets same press-feedback via activeOpacity prop.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 04:38:26 +02:00
chahinebrini
eccc04b1e3 fix(android): generate missing a11y service resources in plugin
Plugin referenced @string/accessibility_service_summary +
@xml/accessibility_service_config in AndroidManifest but never created the
underlying resource files. EAS Cloud prebuild --clean exposed this — local
dev worked because resources were sometimes already there from previous builds.

- withStringsXml: adds accessibility_service_summary string (DE)
- withDangerousMod: writes res/xml/accessibility_service_config.xml at prebuild
- Config flags match native service (TYPE_WINDOW_CONTENT_CHANGED + STATE_CHANGED,
  canRetrieveWindowContent for URL-bar reading)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 04:32:16 +02:00
chahinebrini
5eebda4b6b fix(android): bump compileSdk + targetSdk to 36 for androidx.core 1.17
EAS Cloud prebuild ignores local android/build.gradle pins (android/ is gitignored).
Plugin compileSdk 35 → 36 satisfies new androidx.core dependency requirements.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 04:09:45 +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
3088526fc1 feat(icon): use rebreak_android.png from Play Console as app icon
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 02:43:31 +02:00
chahinebrini
ee25a50288 chore(eas): add Expo projectId to app.config.ts
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 02:34:31 +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
6afffdbb18 fix(mail): clear connect-error on re-connect + return error fields in status
- upsertMailConnection: bei Update lastConnectError + lastConnectErrorAt auf
  null — User aktualisiert App-Passwort → UI zeigt sofort wieder Live (statt
  stale Auth-Fehler-Status bis nächstem IDLE/Scan-Cycle)
- /api/mail/status: liefert lastConnectError, lastConnectErrorAt,
  lastIdleHeartbeatAt mit (waren bisher nicht im Response → Frontend hat den
  Status nie korrekt rendern können)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 23:58:05 +02:00
chahinebrini
f00d2319a5 fix(coach): markdown-strip safety-net + tier-aware speak-endpoint
Backend:
- New stripMarkdown() util (utils/strip-markdown.ts) — handles **bold**,
  bullet-lists, headings, code-fences, links, blockquotes
- /api/coach/message: applies stripMarkdown(text) post-LLM as safety-net
  because Haiku/Llama keep emitting markdown despite explicit prompt rule

Frontend:
- lyra.tsx voice-flow: hardcoded /api/coach/speak-openai → /api/coach/speak
  (tier-aware dispatcher: Free=Google, Pro=Cartesia, Legend=ElevenLabs)
- Added Metro debug-logs at TTS call-site for endpoint + status visibility
- detectEmotion extracted to lib/lyraResponse.ts (was inline duplicate)
- RiveAvatar: small type-export adjustment for shared Emotion type

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 17:54:34 +02:00
chahinebrini
6c3c37afbf feat(games,lyra): GameOverScreen migration + Lyra markdown-strip
GAMES (Nuxt → RN migration):
- New components/games/GameOverScreen.tsx — slide-in + fade overlay
  Props: score, bestScore, gameName, onRetry, onExit, isNewBest
- New lib/gameScores.ts — AsyncStorage helpers
  rebreak_best_snake (higher=better), _tetris (higher=better),
  _memory (lower=better, inverted isNewBest)
- UrgeGames.tsx wired: snake-collision/tetris-topout/memory-finish trigger
  GameOverScreen with retry/exit + best-score persist
- TicTacToe NICHT — round-aggregation game hat eigenen Fertig-Flow
- 7 i18n keys (gameOver.* DE+EN, 5 motivational texts statisch aus pool)

LYRA (markdown-bug fix):
- User-Report: Lyra antwortet mit ** in mobile-app, verwirrt user
- Beide system-prompts (COACH_SYSTEM_PROMPT für SOS, COACH_CASUAL_SYSTEM_PROMPT
  für Coach) bekommen "ANTWORTFORMAT - KRITISCH"-section:
  NIE Markdown (kein **bold**, _italic_, #-Headings, -Bullets) — Klartext only
- Reason: Mobile-App-bubbles rendern markdown nicht → User sieht raw `**text**`

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 16:16:49 +02:00
chahinebrini
755dae1f0a feat(lyra): Coach-Mode persona refactor + mode-badge UI distinction
Per strategist-spec: Lyra-Coach-Mode klarer von SOS-Mode trennen.
- SOS-Mode (urge): crisis-intervention, focused, kurz
- Coach-Mode (lyra): casual, profile-building, philosophy, features

Backend (backend/server/api/coach/message.post.ts):
- COACH_CASUAL_SYSTEM_PROMPT komplett neu strukturiert (~620 tokens)
- Stärkerer Fokus: 3 explicit Aufträge (echtes Gespräch / Profile-Building /
  Rebreak sprechen)
- Profile-building-mandate: "wenn du wenig weißt, sag's ehrlich; frag nach
  Hobbies/Zielen/Menschen — eingewoben, NICHT als Checkliste"
- Cleanere Mission-Section: Bewegung, Anonymität, kein-pathologisieren,
  community-getrieben, DiGA-Listung-Ziel
- Hard-rules klarer: NIE demographics extrahieren (User-Form ist tabu),
  kein Sucht-Vokabular, kein medical-advice
- Existing PLAN_DETAILS-template-var bleibt
- Memory-system unverändert (lyra-memories table, extractAndStoreMemories
  fire-and-forget — kein schema-change nötig)

Frontend Mode-Badges:
- app/lyra.tsx (Coach-Mode): Header-pill "Coach" in brandOrange-tint neben
  Lyra-name
- app/urge.tsx (SOS-Mode): Header-pill "SOS" in error/red-tint neben
  Lyra-name (alt: "Lyra · SOS [v2]" inline-text → cleaner badge-style)

i18n:
- coach.modeBadge.coach + coach.modeBadge.sos in DE + EN

Switch-Logic: route-based (lyra.tsx vs urge.tsx → separate persona via
backend endpoint). Kein User-Toggle — User soll nicht entscheiden müssen
"bin ich grade in Krise?".

Implementation Risk: LOW — schema-neutral, prompt-only + 2 small UI badges.
Erste Beta-Testing-Phase: ~1-2 Wochen iterieren bei Feedback.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 16:06:32 +02:00
chahinebrini
c9029b8fb5 fix(games): Tetris controls centered + Snake icon visibility + digital score-dashboard
User-Wünsche:
1. Tetris bedien-buttons mittig zum Spielfeld (war off-center)
2. Snake geklickte button-icons NICHT weiß (sonst light-theme unsichtbar)
3. Beide games: digital score-counter über playfield

Tetris:
- Controls in alignItems:'center'-wrapper mit width:boardWidth child +
  justifyContent:'space-between' → Move-Pad+Action-Pad bündig zum Feld
  unabhängig von screen-width
- Old Score/Level/Lines header entfernt → DigitalScore übernimmt

Snake:
- DPadBtn: ALWAYS color={tint} (#007aff iOS-blue) für Ionicons
- Active-state via borderColor + scale(1.04), NICHT mehr durch white-icon
- Semi-transparent blue bg (rgba) sichtbar in beiden themes
- Android-Branches + elevation entfernt (überall einheitlich)

DigitalScore (neu):
- 7-segment-feel via Courier New monospace + letterSpacing 2 + tabular-nums
- padStart(5,'0') Score+Best, padStart(2,'0') Level/Length
- Dunkles Panel (#0d1117) + border #1f2937, intentional contrast
- width:boardWidth, alignSelf:center
- Snake: SCORE+BEST | Tetris: SCORE+BEST+LVL

TS clean. Frontend-only, Metro reload reicht.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 15:46:17 +02:00
chahinebrini
417191c90a test(maestro): 6 E2E flows + setup-guide + testID-TODO
User-runnable lokal via maestro CLI oder Studio (GUI). Ahmed-agent built.

Neue flows (.maestro/):
- auth/email-signin.yaml (admin@rebreak.org login via env-vars, NOT hardcoded)
- profile/view-and-edit.yaml (avatar tap → edit → save → verify)
- profile/demographics.yaml (accordion → fill 3 fields → verify save)
- settings/dark-theme.yaml (Settings → Theme → Dark → verify)
- urge/sos-flow.yaml (start SOS → atemübung → finish → rating)
- community/create-post.yaml (compose → publish)

SETUP.md ergänzt: install, prerequisites, env-vars, troubleshooting.

TODO_TESTIDS.md (17 missing testIDs, 7 high-prio):
- AppHeader: header-avatar-btn (alle flows betroffen, aktuell coordinate-fallback)
- urge: sos-send-btn (SOS-flow blocked ohne)
- profile/edit: nickname-input, save-btn

GH-Actions template (.github/workflows/maestro-cloud.yml) — NICHT aktiv,
braucht User-OK + EAS-secrets.

User runs:
  maestro test apps/rebreak-native/.maestro/auth/email-signin.yaml \
    --env=E2E_TEST_USER=admin --env=E2E_TEST_PASSWORD=<from Infisical>
  maestro studio  # GUI

Stolperfalle: charioanouar (Google OAuth) funktioniert nicht — admin-account
nutzen.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 15:45:53 +02:00
chahinebrini
d7b15e231a feat(theme): Dark Mode Wave 2 — blocker, mail, chat, community, notifications, all remaining screens
Wave 2 = ALLE app-files die in Wave 1 noch hardcoded waren. Komplette App-weit
theme-aware-Migration jetzt durch. Legacy `import { colors }` flat export
vollständig eliminiert.

Migrated this wave:

Top-level Screens:
- app/urge.tsx (makeStyles factory mit ~20 colors)
- app/room.tsx + dm.tsx + games.tsx
- app/(app)/chat.tsx + mail.tsx + coach.tsx + notifications.tsx
- app/profile/[userId].tsx + profile/edit.tsx (INPUT_STYLE in body moved)
- app/debug.tsx + auth/callback.tsx

Blocker (7):
- AddDomainSheet, CooldownBanner, DeactivationExplainerSheet, DomainGrid,
  ProtectionCard, ProtectionDetailsSheet, ProtectionLockedCard

Mail (3):
- ConnectMailSheet, EditMailAccountSheet, MailEmptyState

Chat (1):
- ChatBubble, ChatInput

Community/Posts/Notifications:
- PostCard, PostCardSkeleton, ComposeCard, PostCommentsSheet
- NotificationsDropdown
- StreakBadge (Nativewind classes durch inline dynamic styles ersetzt)

Reusable Sheets:
- WheelPickerModal, OptionsBottomSheet, DeviceLimitReachedSheet

Urge subsystem (5):
- InlineRatingDrawer, ShareSuccessDrawer, UrgeStats, SosFeedbackModal,
  Breathing

Profile components:
- DigaMissionBanner

Pattern: useColors() hook in component body, makeStyles(colors) factory wo
StyleSheet.create vorher hardcoded war. 11 base-tokens (bg/surface/
surfaceElevated/border/text/textMuted/brandOrange/brandBlue/success/error/
warning) nutzen colors.light vs colors.dark scheme.

Bewusst NICHT migriert (semantic colors):
- DigaMissionBanner amber (#fffbeb, #854d0e) — DiGA-brand, nicht neutral
- Lyra-thinking #3b82f6 in urge.tsx — Lyra-brand-color
- scrollDownBtn #374151 — intentional dark floating-button

TS clean. Test: Settings → Theme → Dark — alle screens sollen jetzt dunkel
werden ohne white-flashes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 14:51:02 +02:00
chahinebrini
f3a316460f fix(profile/edit): surface real error message instead of generic 2026-05-08 22:52:57 +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
8f2b93f881 feat(profile): Avatar + Nickname edit-flow
User-Wunsch: auf Profile Avatar + Nickname ändern können. Avatar entweder
preset aus signup-list ODER eigene Foto mit cropper.

New files:
- app/profile/edit.tsx — vollständiger Edit-Screen (Avatar-Gallery + Photo-Picker
  + Nickname TextInput + Save-Button)
- lib/avatars.ts — HERO_AVATARS preset-list (matched mit Nuxt-app Signup) +
  getAvatarUrl helper
- lib/resolveAvatar.ts — resolveAvatar(avatarId, nickname): URL für
  preset-id ODER fallback auf nickname-initial-tile

Profile-Page wiring:
- Avatar-Tap + Nickname-Tap pushen jetzt zu /profile/edit (statt Alert-stub)
- Nach successful save: useMe.reload() + router.back()

Edit-Flow:
- Preset (HERO_AVATARS, 12 items): tap-grid mit selected-State + brand-Border
- Eigenes Photo: expo-image-picker mit allowsEditing+aspect[1,1] (OS-nativer
  Crop-Dialog), expo-file-system/legacy für base64-Konvertierung, upload via
  POST /api/avatar/upload (writes Supabase-Storage rebreak-avatars + updated
  Profile)
- Save: PATCH /api/auth/me { nickname, avatar }

i18n: profile.edit_* keys DE+EN

Backend-API:
- PATCH /api/auth/me — existiert (apps/admin/composables nicht — backend!)
- POST /api/avatar/upload — existiert

TS-fixes:
- expo-file-system → /legacy import (SDK 54 breaking change, siehe Task #14)
- ?? + || mixing fixed mit klammern

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 22:15:13 +02:00
chahinebrini
c4cfd351c4 feat(profile): useDemographics hook + page-reload re-hydration
User-Bug: Demographics werden korrekt gespeichert (DB verified), aber nach
Page-Reload sah User leere Felder → dachte save kaputt. Root: kein GET-endpoint
+ kein server-state-rehydrate nach PATCH.

- hooks/useProfileData.ts: useDemographics() wraps useFetchOnce<DemographicsResponse>
  ('/api/profile/me/demographics'), splittet in fields + meta (consentAt/withdrawnAt)
- app/profile/index.tsx: serverDemographics ?? EMPTY_DEMOGRAPHICS const statt local
  state. Nach PATCH/DELETE: reloadDemographics() pulled fresh server data.

Edge-cases:
- 404 (endpoint nicht live) → fallback EMPTY, kein crash
- loading → EMPTY initial bis fetch resolved, konsistent mit other hooks
- withdrawnAt set → demoComplete=false (Demographics-Hint sichtbar trotz potentiell
  noch befüllter felder durch race-condition)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 21:32:39 +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
c776570106 fix(demographics): align Frontend enum/prefix values with Backend zod schema
Backend-Agent identified 2 mismatches that caused 422 on save:

1. MARITAL_OPTIONS values:
   - 'partnership' → 'partnered' (Backend expects this)
   - 'none' → 'no_answer'

2. BUNDESLAND_OPTIONS values:
   - 'BW' → 'DE-BW' (alle 16 Bundesländer mit DE-prefix)
   - Backend zod-regex: ^DE-(BW|BY|...)$

3. germanCities.ts getCitiesForBundesland:
   - Akzeptiert jetzt sowohl 'BY' als auch 'DE-BY' (strip prefix on lookup)

User-visible labels unverändert. Nur internal values aligned mit Backend-API.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 20:47:43 +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
aa609de46f feat(ui): Settings + Demographics native UIMenu + clean Wheel backdrop
Settings:
- Theme/Sprache UIMenu: chevron-forward anchor (statt chevron-down — chevron-down
  reserviert für Collapsibles, siehe feedback_chevron_icon_convention memory)
- Theme menu: SF-Symbol images entfernt → Theme/Sprache haben gleiches Padding

Demographics (Profile):
- Geschlecht (3 options) → UIMenu (anchored Pull-Down) statt Wheel
- ≤3 → UIMenu, >3 → Wheel (Apple HIG-konform)

WheelPickerModal:
- Backdrop transparent (kein darkening overlay)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 20:37:50 +02:00
chahinebrini
c24ab64c9d chore(deps): expo-router 6 + register expo-font/expo-web-browser plugins
- expo-router 6 (per Phase 1 upgrade): all our imports remain valid
  (Stack, useLocalSearchParams, useRouter, useFocusEffect, withLayoutContext,
  RelativePathString). No `Href<T>` generic usage in codebase. expo-splash-screen
  already explicit in deps. Zero code changes needed for router-6 migration.
- Register expo-font + expo-web-browser as plugins per `expo install --fix`
  recommendation. Both already in deps but plugins block missing.

TS: 0 errors. Bundle still works.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 19:47:21 +02:00