82 Commits

Author SHA1 Message Date
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
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
f743556dc5 feat(domain-approval): Legend-priority + 24h-SLA-deadline + user-info cards
User-Wunsch: Legend-User priorisieren, 24h Approval-SLA, sichtbar wer/wann/Restzeit.

Backend:
- Schema: DomainSubmission.user @relation Profile (FK + composite-index status,createdAt)
- Migration: 20260509_domain_submission_user_relation (additive, FK via DO $$ block,
  idempotent IF NOT EXISTS index)
- db/domains.ts getPendingSubmissions enriched:
  - include user { id, nickname, plan }
  - returns PendingSubmissionRow with planPriority (legend=2, pro=1, free=0)
  - deadlineAt = createdAt + 24h
  - msUntilDeadline (negative when overdue)
  - sort: Legend > Pro > Free, FIFO innerhalb plan-bucket
- Constant ADMIN_APPROVAL_SLA_MS exported

Tests:
- backend/tests/admin/domains.test.ts — 5 cases (priority-sort, FIFO, deadline,
  overdue, user-null fallback). 83 backend tests passing total.

Frontend (apps/admin/pages/domains.vue):
- Card-list (statt UTable — sichtbarer urgency-stripe links)
- Filter-chips „Alle | Nur Legend | Überfällig" mit live counts
- Per row: nickname, plan-badge (Legend = sparkles + warning/gold),
  request-age (relative), deadline-countdown („noch 18h" / „ÜBERFÄLLIG (6h)")
- Visual urgency-stripe (1px border-left full-height):
  - Overdue: red-600 + warning-icon
  - <2h: red-500
  - Legend: amber-400 (gold)
  - <12h: yellow-500
  - Normal: gray-700

⚠️ Migration auto-deploy via pipeline (b38bf17 detection).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 15:55:18 +02:00
chahinebrini
0700f65485 feat(admin): index.vue als quick-link-dashboard (Phase 2 done)
Phase 2-pending-Liste durch 4 NuxtLink-Cards ersetzt → tap navigiert direkt
zur jeweiligen page. Plus separater Stats-Quick-Link unten.

Pages-content unangetastet, nur dashboard refresh.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 15:48:26 +02:00
chahinebrini
e9d4434bf8 feat(admin): responsive layout — bottom-tabs auf mobile, sidebar auf desktop
User-Wunsch: kleine screens (iPhone) keine sidebar, sondern bottom-tab-bar
wie native rebreak-app.

Layout-Architektur:
- Desktop (lg+, ≥1024px):
  - Topbar: email + logout-button
  - Sidebar links (w-56) mit full-label-nav (versteckt <lg)
  - Content rechts (p-6)
- Mobile (<lg):
  - Topbar: hamburger UDropdownMenu rechts (email + logout)
  - Sidebar versteckt
  - Content full-width (p-4 pb-24, damit content nicht hinter tab-bar)
  - Bottom-tab-bar: fixed bottom-0, border-t, bg-gray-950/95 backdrop-blur
  - 5 tabs in grid-cols-5: Home / Domains / Users / Stats / Mod
  - Icon (h-5 w-5) + label (text-[10px])
  - Active-state: text-white bg-gray-800 (route-match isActive helper)
  - Safe-area-bottom respektiert via env(safe-area-inset-bottom)

Pages-content unangetastet, nur layout. Build clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 15:47:28 +02:00
chahinebrini
68fe8afab2 feat(admin): Phase 2 Frontend — Domains/Stats/Users/Moderation pages + responsive layout
4 page-implementations + server-route-proxies (admin-secret stays server-only):

DOMAINS (apps/admin/pages/domains.vue):
- UTable mit pending-submissions queue
- Approve / Reject buttons per row
- Reject-confirm-modal mit optional note
- useToast + refresh nach action
- 3 server-routes: GET list + POST approve/reject

STATS (apps/admin/pages/stats.vue):
- Stat-cards: Total Users + delta-week, Total Posts + delta-week,
  Domains pending (link to /domains), Domains approved, Feedback pending,
  Lyra-Posts (30d)
- UProgress für Domain-Approval-Quote
- Auto-refresh 60s + manual refresh-button
- USkeleton während loading
- 1 server-route: GET /api/stats

USERS (apps/admin/pages/users.vue):
- UTable mit avatar+nickname/username, plan-badge, streak, status, createdAt
- Search-input + plan-filter dropdown
- Action-dropdown per row: Plan-Change / Ban-Toggle / Soft-Delete
- 3 separate UModals mit confirm-pattern
- Cursor-pagination (Mehr laden button)
- 3 server-routes: GET list, PATCH /:id, DELETE /:id

MODERATION (apps/admin/pages/moderation.vue):
- Stack-layout mit card-pro-item (statt table — content-preview braucht space)
- Type-badge (Post/Comment), Author + Plan-badge, content-preview (200 chars),
  reportedAt
- Action-buttons: Dismiss (gray), Delete Content (red soft + reason-modal),
  Ban User (red solid + warning-modal)
- Empty-state, cursor-pagination
- 4 server-routes: GET /queue, POST /:id/dismiss/delete/ban-user

Server-route pattern (apps/admin/server/api/...):
- Use useRuntimeConfig().adminSecret server-only
- Client never sees x-admin-secret
- Body/query passthrough to backend

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 15:47:05 +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