239 Commits

Author SHA1 Message Date
chahinebrini
448d64dbd5 fix(ios): re-enable family-controls entitlement for development builds
It was commented out wholesale in 398b7b9 so the App-Store/TestFlight provisioning
profile would build (Apple hasn't granted the *Distribution* Family Controls
entitlement yet). But that also killed it for the dev-client, so denyAppRemoval /
ManagedSettings throws "NSCocoaErrorDomain:4099 — can't talk to the helper app"
when you flip the Blocker-page App-Lock.

Gate it on REBREAK_ENABLE_FAMILY_CONTROLS, set to "1" in eas.json's development
profile (internal distribution → Development entitlement, which we do have). The
preview/production profiles stay without it until Apple approves the Distribution
entitlement — then add the flag there too + bump buildNumber.

NOTE: the next `eas build -p ios --profile development` will re-provision the main
app profile to include the entitlement; if Apple turns out NOT to have granted the
*Development* one either, that build will fail the same way the TestFlight one did.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 21:30:45 +02:00
chahinebrini
a841b32c31 feat(rebreak-native): <FormSheet> — one reusable bottom-sheet composable (phase 1)
The custom modals each rolled their own Modal + animated-height + PanResponder +
keyboard handling, inconsistently. <FormSheet> is the single parametrized
composable, generalized from the proven PostCommentsSheet pattern:

  - standard header: centred grabber + left-aligned title — NO Fertig/Abbrechen/
    Zurück buttons (dismiss = swipe down / backdrop tap)
  - resizable via drag on handle/header; drag-down past minHeightPct (or a fast
    flick) dismisses
  - height hard-capped at 75% of the screen — drag AND keyboard-expand
  - keyboard-aware: sheet grows by the keyboard height (capped), iOS paddingBottom
    pushes the content exactly above the keyboard; Android adjustResize handles it
  - JS-driver height / native-driver translateY split (avoids the "height not
    supported by native animated module" crash)
  - props: title, initialHeightPct, minHeightPct, backdropOpacity, dismissOnBackdrop,
    safeAreaBottom, growWithKeyboard, topRadius

Migrated (phase 1 — the no-input content sheets):
  - ProtectionDetailsSheet → drops the bespoke Modal/PanResponder + the "Fertig"
    header button; was 0.9–0.95 tall, now ≤0.75
  - DeactivationExplainerSheet → was a pageSheet Modal with a "Zurück" button;
    now the standard bottom sheet, header button gone
  - PostCommentsSheet → capped its expand height 0.92 → 0.75 (TODO phase-1b: move
    it onto <FormSheet> too instead of pinning magic numbers)

Phase 2 (next): <SheetFieldStack> — progressive multi-input flow (active input
pinned above the keyboard + "→" to advance, filled fields stack above, the rest
of the form reveals after the last field) for ConnectMailSheet / AddDomainSheet /
EditMailAccountSheet / CreateRoomSheet; then the auth/edit full-screen pages.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 21:03:17 +02:00
chahinebrini
52fd1bcce3 feat(rebreak-native): Android counterpart for the app lock
expo-local-authentication already works on Android (fingerprint / face unlock /
device PIN — the module auto-adds the biometric permission, no app.config change
needed). Only the settings description was iOS-flavoured ("Face ID, Touch ID");
add an Android variant and pick by Platform.OS. The lock screen + biometric
prompt strings were already generic.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 20:42:52 +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
5d2db6d642 fix(ios): register RebreakURLFilter extension with EAS for credential generation
The TestFlight build kept failing with:
  error: No profiles for 'org.rebreak.app.RebreakURLFilter' were found ...
  Automatic signing is disabled and unable to generate a profile.
  (in target 'RebreakURLFilter' from project 'ReBreak')

EAS managed credentials only provision the main app bundle ID. App extensions
must be declared up-front via extra.eas.build.experimental.ios.appExtensions so
the CLI knows to register the extension's App ID (+ app-group + network-extension
capabilities) and generate a distribution provisioning profile for it.

Next step (interactive, needs Apple login): `eas credentials` → iOS → preview
to actually create the extension credentials, then re-run the build.

(FamilyControls entitlement stays commented out in with-rebreak-protection-ios.js
until Apple grants the Distribution entitlement — this build ships without App-Lock.)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 17:50:48 +02:00
chahinebrini
398b7b9d58 fix(ios): two real build blockers — FC entitlement and extension dev team
The EAS error message "Xcode 14 resource bundle signing" was a misleading wrapper.
Pulled the actual Xcode log via the EAS CLI; the real failures were:

  error: Provisioning profile "...AppStore..." doesn't support the Family Controls
         (Development) capability.
  error: Provisioning profile ... doesn't include the com.apple.developer.family-controls
         entitlement.
  error: Signing for "RebreakURLFilter" requires a development team. (in target
         'RebreakURLFilter' from project 'ReBreak')

Two fixes:
1. Family Controls is requested with Apple but not yet granted (Distribution), so
   EAS can't generate an AppStore provisioning profile that includes it → comment
   out the family-controls entitlement claim in withMainAppEntitlements. Re-enable
   once Apple grants the entitlement. The iOS Swift code still imports
   FamilyControls/ManagedSettings (public frameworks, link fine without the
   entitlement); activateFamilyControls would throw at runtime — handled by the
   JS layer's catch. Net: TestFlight build works, iOS App-Lock feature is dormant
   until the entitlement lands.
2. The RebreakURLFilter extension target had no DEVELOPMENT_TEAM set — EAS managed
   credentials only set it on the main app target; sub-targets don't inherit.
   Hardcoded the team ID 84BQ7MTFYK on the extension's build configurations
   (matches eas.json submit.production.ios.appleTeamId).

(The resource-bundle-signing fix from the previous attempt stays — it's
not the cause here but is correct hygiene for static-frameworks builds.)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 23:57:43 +02:00
chahinebrini
572766ab88 fix(ios): broaden resource-bundle signing fix — disable code-signing on all Pods targets
First attempt targeted only `target_installation_results.resource_bundle_targets` —
too narrow. With privacyManifestAggregationEnabled the Pods project has additional
bundle targets (aggregated privacy manifests) that also need code-signing disabled.
Brute-force fix: set CODE_SIGNING_ALLOWED/REQUIRED = NO and clear
EXPANDED_CODE_SIGN_IDENTITY on every target in installer.pods_project — pod
targets don't need signing, only the main app does. Added a Pod::UI.puts so we
can see the fix run in the EAS build log.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 23:48:29 +02:00
chahinebrini
ac956b8311 fix(ios): config plugin to disable code-signing for pod resource bundles
EAS iOS build failed: "Starting from Xcode 14, resource bundles are signed by
default, which requires setting the development team for each resource bundle
target." Cause: we build with useFrameworks: "static" (expo-build-properties),
so CocoaPods generates resource-bundle targets for pods with resources, and
Xcode 14+ wants them signed. EAS has no dev team for those.

New plugin with-resource-bundle-signing-fix injects into the Podfile's existing
post_install hook: sets CODE_SIGNING_ALLOWED = 'NO' for every pod resource-bundle
target (they don't need signing). Idempotent; runs as withDangerousMod('ios')
during prebuild so it survives EAS's clean prebuild.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 23:35:32 +02:00
chahinebrini
1a201289cc chore(eas): drop the update channels (no expo-updates installed yet)
The preview/production profiles referenced channel "preview"/"production" which
requires expo-updates + `eas update:configure`. Not needed for the TestFlight
build; dropping them avoids the install prompt. (Re-add when we deliberately
wire up EAS Update / OTA.)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 23:30:18 +02:00
chahinebrini
c004dc7517 chore(eas): preview profile → distribution: store (for TestFlight uploads)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 23:11:07 +02:00
chahinebrini
5b12f14a90 feat(rebreak-native): Nuxt-style splash, domain normalization on blur, app-wide keyboard fix
- app/index.tsx: replaced the placeholder landing with the BrandSplash look
  (#0f172a bg, SVG radial glows, breathing animation, staggered fade/bounce-ins
  for app name / logo / tagline / CTAs, "Made in Germany" footer). Dropped the
  "v0.1.0 RN Migration Phase 1 Skeleton" line; landing.version removed from locales.
- AddDomainSheet: onBlur runs normalizeDomain() (strips scheme/www./path/query and
  email local-part) so the user sees the cleaned registrable domain before adding;
  also swapped the two leftover Pressables → TouchableOpacity (no-Pressable rule).
- KeyboardAwareSheet: clamp the sheet height to (screenHeight - insets.top - 20)
  while the keyboard is up, so tall sheets (e.g. AddDomainSheet's 600px) don't grow
  off-screen and clip the inputs at the top.
- ConnectMailSheet: automaticallyAdjustKeyboardInsets on iOS so focused inputs scroll
  into view. Covered sheets: AddDomainSheet, ConnectMailSheet, EditMailAccountSheet,
  AddMacSheet, AddWindowsSheet.

JS-only (hot-reloadable).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 22:58:59 +02:00
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
790b5e5c24 feat(admin): surface Lyra-Posts page in nav + dashboard grid
The pages/lyra.vue page (create community posts as Lyra/ReBreak, AI-generated
or manual) existed but wasn't linked anywhere. Adds it to the sidebar +
mobile bottom-tab (grid-cols-5→6) and the dashboard quick-links grid
(lg:grid-cols-4→5). Admin app stays team-internal (stats / users / domain
approval / social posts / moderation) — no relation to the RN app.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 15:06:12 +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
b1b3b5eb36 feat(admin): migrate lyra-posts feature from legacy nuxt-rebreak
- Add apps/admin/pages/lyra.vue — LLM-generierter oder manueller Bot-Post als Lyra/ReBreak
- Add apps/admin/server/api/admin/lyra-generate.post.ts — Proxy zu backend
- Add apps/admin/server/api/admin/lyra-post.post.ts — Proxy zu backend
- Add apps/admin/server/api/admin/lyra-profile.get.ts — Proxy zu backend
- Add apps/admin/server/api/admin/set-lyra-avatar.post.ts — Proxy zu backend
- Update apps/admin/pages/index.vue — Lyra-Posts Quick-Link auf Dashboard
Auth via admin-auth Middleware + server-side adminSecret Proxy-Pattern.
BenAvatar (Rive, legacy) entfernt, Avatar-Anzeige bleibt via lyra-profile.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 02:11:51 +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