604 Commits

Author SHA1 Message Date
chahinebrini
fb2d90b947 fix(calls): no duplicate incoming-call notifications
- backend: skip Expo alert push to iOS devices that already received VoIP push
  (CallKit + banner = double ring)
- native: receiveIncoming no longer triggers InCallManager.startRingtone —
  CallKit/ConnectionService play their own ring. Dedup if same callId
  arrives twice (Realtime + VoIP-Push race).
2026-06-04 18:28:00 +02:00
chahinebrini
92ad4c93b5 fix(dm): smooth image lightbox + stable online/typing status
- MediaLightbox component extracted from dm.tsx. Image now fills a fixed
  full-screen box with contentFit=contain instead of an onLoad-computed
  aspect ratio, removing the square->real-size jump ("jitter") on open.
- Info-sheet images: render a nested MediaLightbox inside the FormSheet
  (stacks above the sheet modal) and track lightboxSource. Removes the
  close-sheet-then-reopen workaround that switched context back to the DM.
- Typing indicator: heartbeat (every 2s while focused + non-empty) instead
  of keystroke-only sends, so "typing…" holds through thinking pauses;
  receiver clear raised to 6s. stop on blur/send/empty.
- Presence: debounce going offline by 12s (online immediate) so brief
  presence-sync gaps no longer flicker "Online" <-> "last seen".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 10:48:00 +02:00
chahinebrini
ba200d54f4 fix(coach): keep SOS out of Coach chat history
SOS (urge.tsx) uses /api/coach/message as a stateless LLM proxy for game
comments, share drafts and the stream fallback — sending SOS_BOOT +
[INTERN:] prompts. The endpoint persisted the full messages array into
coachSession for pro/legend users, so those internal prompts and the raw
JSON replies leaked into the Coach chat history as visible bubbles.

- Reactivate the sosMode flag (already sent by all three SOS call-sites):
  when set, the endpoint skips coachSession persistence, memory extraction
  and feedback detection — pure LLM proxy, no shared state.
- Add a defensive filter on /api/coach/history that strips internal
  messages (SOS_BOOT, [INTERN:], [SYSTEM-HINT], raw JSON / [[CHIPS]]
  replies) so already-contaminated sessions self-heal on next load.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 10:45:38 +02:00
chahinebrini
848b517d22 fix(voip-push): dynamic import @parse/node-apn — nitro bundler bricht statisches Tracing (Class extends Module-namespace) 2026-06-04 10:40:44 +02:00
chahinebrini
57e0a23021 fix(nitro): externalize @parse/node-apn + imapflow — CJS-extends-Pattern bricht beim Bundle (calls/ring + mail/scan 500) 2026-06-04 10:35:33 +02:00
chahinebrini
4a520ba7c9 feat(calls): B4 — VoIP-PushKit config-plugin + client voipToken + JS payload bridge 2026-06-04 10:02:48 +02:00
chahinebrini
f8181d63b9 fix(deploy): pm2 stop staging services + 2.5GB heap vor build (OOM-fix) 2026-06-04 09:35:43 +02:00
chahinebrini
822053e11e feat(calls): CallKit/ConnectionService + VoIP-PushKit + EU-Ringback
Caller/Callee UX:
- lib/ringback.ts + assets/sounds/ringback_eu.mp3 (EU 425Hz Festnetz-Tone)
- stores/call.ts: stopRingback bei connected, hangup-reasons, logCallToChat fix
- locales: 'Wird angerufen…' statt 'Ruft an…'

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

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

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

Docs: ops/CALLKIT_SETUP.md (Apple-Portal-Steps für VoIP-Cert)
2026-06-04 09:27:13 +02:00
chahinebrini
0cac3c9d1a feat(calls): Phase 1a — TURN ice-servers endpoint + coturn ops + DM call-button header
Backend:
- GET /api/calls/ice-servers: ephemeral HMAC TURN credentials (10-min TTL),
  iceTransportPolicy:"relay" (no IP leak), 503 until coturn configured
- nitro runtimeConfig: turnHost/turnSecret/turnRealm (Infisical staging set)

Ops:
- ops/calls/ runbook + turnserver.conf (self-hosted coturn, force-relay,
  use-auth-secret, hardening). coturn provisioned + verified on rebreak-server.

Frontend (DM header redesign):
- removed standalone "i" button; header center (avatar+name+chevron) opens info sheet
- call icon top-right, only when canCall (mutual-follow + callsEnabled);
  shows "coming soon" until the WebRTC client lands

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 03:06:33 +02:00
chahinebrini
89e4e3481b feat(calls): Phase 0 — calls_enabled opt-out + canCall guard (mutual-follow); DM UI batch
Backend (voice-call groundwork, no call engine yet):
- Profile.callsEnabled (Boolean default true) + migration
- canCall(caller,callee): mutual-follow AND callee.callsEnabled — server-side hard guard
- POST /api/me/calls-enabled (opt-out toggle), GET /api/chat/can-call/:userId
- expose callsEnabled in /api/auth/me

Frontend:
- "Allow calls" toggle in Profile privacy section (default on, optimistic+rollback)
- Me.callsEnabled + i18n DE/EN/FR/AR

Bundled DM UI work from this session:
- image lightbox is now a swipeable carousel over all shared images (+ counter)
- keyboard stays open after sending (input ref refocus)
- voice notes: Instagram-style waveforms (own=white/mint, other=black/grey),
  removed the blue progress dot; lazy-load expo-media-library with clean fallback
- expo-linear-gradient + expo-media-library deps

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 21:14:31 +02:00
chahinebrini
50425a62ee fix(devices): Magic-Hub zeigt jetzt alle Native-Geraete, Native dedupliziert Mac
Magic-Mac-Hub (/api/magic/devices):
- Filter boundToPlan war zu eng \u2014 iPhone/iPad ohne aktiven Plan-Lock
  fielen raus. Jetzt: alle UserDevice-Rows des Users ausser den
  magic-enrolled, plus ProtectedDevice mit Dedupe.

Native /devices Page:
- MacBook erschien doppelt: einmal als UserDevice (registriert via
  Magic-Mac, model=Mac14,9) und einmal als ProtectedDevice (alter
  DNS-Flow). Dedupe per platform-key (mac/ios/android/win):
  wenn UserDevice mit gleicher Plattform existiert, blende
  ProtectedDevice aus.
- Slot-Counter zaehlt jetzt nach dedupe (totalRegistered).
2026-06-03 19:43:33 +02:00
chahinebrini
187a2d8c19 feat(magic): Hub Header mit Avatar+Nickname + iPhone/iPad via UserDevice-Locks + MacBook-Dedupe
- Neuer Endpoint /api/magic/me liefert nickname/avatar/plan fuer
  Hub-Header. Mac-App ruft fetchMe() beim Hub-Load.
- DeviceHubView Header zeigt jetzt Avatar (AsyncImage mit Fallback
  auf Initial-Letter), Nickname + Plan-Badge statt nur 'ReBreak Magic'.
- /api/magic/devices erweitert: listet zusaetzlich UserDevice-Rows mit
  boundToPlan != null (das sind iPhone/iPad aus dem Native-App-Login-
  Flow, Legend-Device-Lock). source='locked'.
- Dedupe: ProtectedDevice wird unterdrueckt wenn bereits ein UserDevice
  mit aehnlichem Namen + gleicher Plattform existiert (fixt doppelten
  MacBook im Hub).
- Helper prettyPlatform() + Normalisierung (platform-key 'mac'/'ios'/
  'android'/'win') fuer robusten Vergleich.
2026-06-03 11:41:06 +02:00
chahinebrini
ac72fabc34 feat(magic): Hub vereinigt Magic-Bindings + alte ProtectedDevices
- GET /api/magic/devices fetcht jetzt parallel listMagicDevices()
  + listProtectedDevices() und merged beide Quellen in eine
  Response. Items haben neues 'source' Feld (magic|protected).
- ProtectedDevice (alter Native-DNS-Flow) wird auf gleiche
  Shape gemappt: label->hostname, platform->model.
- Mac-App MagicDevice: source-Feld optional + resolvedSource
  Fallback fuer Backwards-Compat. id mit source-Prefix gegen
  Collisions zwischen Tabellen.
- DeviceHubView Row: protected-Geraete bekommen graues
  'Native-App' Badge und Hinweis 'Verwaltung in der
  ReBreak-App' statt Trash-Button (Release laeuft dort).
2026-06-03 11:05:15 +02:00
chahinebrini
dbc62b98ca perf(chat): index direct_messages + DB-side latest-per-partner query; remove unconditional Lyra welcome-back
- getDmConversations: DISTINCT ON (partner) ORDER BY partner, created_at DESC
  → one row per conversation in a single indexed query instead of fetching
  up to 500 rows and de-duplicating in JS
- add indexes on direct_messages (sender_id,created_at DESC),
  (receiver_id,created_at DESC), (receiver_id,read_at) — table had none, so
  every conversation-list load (runs per user on app launch for the badge)
  was a full-table scan + sort
- lyra.tsx: drop the welcome-back greeting that fired on every first coach
  open per session regardless of protection status/language (always German,
  unconditional). Endpoint kept for future conditional use

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 10:57:29 +02:00
chahinebrini
d54bd06727 feat(magic): post-login Device-Hub als zentraler Einstieg + Limit 3->5
Redesign:
- Nach Login landet User direkt im neuen DeviceHubView statt
  Auto-Mac-Registrierung. Hub zeigt: User-Email, X/5-Slot-Counter,
  Liste aller registrierten Geraete + 'Geraet hinzufuegen' mit
  iPhone/iPad vs Mac Wahl.
- Mac wird NUR registriert wenn User aktiv 'Mac' im Hub waehlt
  (frueher: auto on app-start, frass Slot).
- iOS-Pfad: Hub -> Welcome/Preflight/Supervise/Enroll/Configure
  -> Done -> 'Zurueck zur Geraete-Uebersicht'.
- Mac-Pfad: Hub -> MacRegistrationView (Register+DNS-Install)
  -> 'Fertig -> Hub'.
- Wizard-Header hat jetzt Grid-Icon 'Zur Geraete-Uebersicht' als
  Escape-Hatch jederzeit.
- Per-Device-Loeschung im Hub: Trash-Icon -> Confirm-Dialog
  ('Auf X muss Freigabe bestaetigt werden, 24h Cooldown') ->
  request-release-Endpoint (existing infra).
- Device-Limit 3 -> 5 in backend (Staging-Testing + Legend-Wert
  fuer spaeter).
- StepIndicator/Step-Counter: macRegistration zaehlt nicht im
  iOS-Flow.
2026-06-03 10:39:51 +02:00
chahinebrini
87d6395ed2 fix(magic-mac): macOS 26 profile install via NSWorkspace + de-dup register card
Zwei Bugs:

1) 'profiles install -path' wurde mit macOS 15+ entfernt
   ('profiles tool no longer supports installs. Use System Settings
   Profiles to add configuration profiles.'). Auf macOS 26 (Tahoe)
   ist das Hard-Removal.
   -> Switch zu NSWorkspace.shared.open(profileURL): \u00f6ffnet die
   .mobileconfig in System Settings -> Profile-Pane. User best\u00e4tigt
   manuell + gibt Admin-PW. Einziger Weg ohne MDM-Enrollment.
   -> success-Text passt: 'Bitte in System Settings Installieren
   klicken'.

2) Doppelte 'Mac registriert'-Karte: successMessage-Card UND
   strukturierte Registration-Status-Card beide sichtbar nach
   register. Auto-Profile-Install nach Register war eh totes
   Verhalten (DNS jetzt optional).
   -> successMessage wird nicht mehr in handleRegistration gesetzt,
   nur noch in handleProfileInstall. Eine Karte.
2026-06-03 10:29:30 +02:00
chahinebrini
18c3a49404 fix(magic-mac): DNS-Schutz wird optional, blockt iPhone-Setup nicht mehr
Design-Klarstellung: Magic ist primaer fuer iOS-Supervision/MDM-
Enrollment. Mac-Registrierung dient als Setup-Bruecke. DNS-Filter
auf dem Mac bleibt eine optionale Self-Service-Option fuer den
User — kein Gate mehr fuer den iPhone-Flow.

- Intro-Text neu: erklaert Magic = iOS-Setup, Mac als Bruecke
- Nach Register: 'Weiter -> iPhone-Setup' immer sichtbar
- 'DNS-Schutz installieren' ist jetzt sekundaerer Bordered-Button
  mit '(optional)' im Label
- Bisheriger Template-Download-Fix vom letzten Commit bleibt
  natuerlich bestehen — Download funktioniert wieder, ist nur
  nicht mehr Pflicht
2026-06-03 10:04:08 +02:00
chahinebrini
8670b45351 fix(magic): inline mobileconfig template as TS constant
serverAssets approach didn't bundle the template into the Nitro
output (no .output-staging/server/chunks/raw/ dir, no asset-storage
mount in nitro.mjs). Logs confirm: '[Magic] Profile template missing
in serverAssets'.

Drop serverAssets entirely. Inline the template (~2KB) as a TS
constant in backend/server/utils/magic-profile-template.ts. Build-
robust, no FS/storage dependency at runtime. Canonical source of
truth remains ops/mdm/rebreak-mac-dns-filter.mobileconfig — keep in
sync manually until/unless we add a codegen step.
2026-06-03 09:57:27 +02:00
chahinebrini
d212452a5d fix(magic): bundle mobileconfig template via Nitro serverAssets
Previously read template via process.cwd() + 'ops/mdm/…' — but pm2
runs the bundled output from /root, not the repo root, so the path
resolved to /root/ops/mdm/… (does not exist) → HTTP 500 'Profile
template not found' after Mac registration.

Switch to Nitro's serverAssets (baseName 'mdm', dir '../ops/mdm')
which is bundled at build-time and read via
useStorage('assets:server'). cwd-independent + survives any deploy
layout change.
2026-06-03 09:46:13 +02:00
chahinebrini
038c383bef fix(magic): use hex for DNS token (AdGuard rejects base64url '_')
AdGuard validates client IDs as DNS labels: 'invalid clientid: bad
hostname label rune'. base64url alphabet contains '_' which fails.
Switch to hex (a-f, 0-9) — 32 bytes hex = 64 chars, 256-bit entropy,
DNS-safe.
2026-06-03 09:41:47 +02:00
chahinebrini
77edd67cbe fix(magic): explicit imports + staging defaults + sheet height
- backend/api/magic/register: explicit import of MAGIC_DEVICE_LIMIT
  and createAdGuardClient (Nitro auto-import was missing them
  → ReferenceError → HTTP 500 on /api/magic/register)
- mac-app: default backendBaseUrl falls back to staging.rebreak.org
  (app.rebreak.org serves wrong TLS cert)
- native MagicSheet: fallback download/dmg URLs point to staging
- native settings: Magic sheet capped at detents=[0.85] so AppHeader
  stays visible
- bundles all in-flight Magic feature work (pair create/redeem,
  device endpoints, schema, adguard utils, mac-app, locales)
2026-06-03 08:25:02 +02:00
chahinebrini
941dd60f36 feat(magic): pairing-code login flow
Backend:
- MagicPairingCode + MagicSession Prisma models
- /api/magic/pair/create (6-digit code, 10min TTL, single-use)
- /api/magic/pair/redeem (no auth, returns mgc_* token)
- /api/magic/info (public DMG metadata)
- requireUser() accepts mgc_* tokens

Mac-App (RebreakMagic):
- LoginView: 6-digit code input (OTP-style), real AppIcon, no signup
- AuthService: signInWithPairingCode() replaces email/pw flow

Native-App:
- MagicSheet (TrueSheet) in Settings: download + code generator + linked Macs
- AddMacSheet: subtle banner pointing to /settings
- de/en locales
2026-06-03 00:18:24 +02:00
chahinebrini
138e45fe0a feat(dev): add 'magic' subcommand to dev.sh for RebreakMagic macOS app
./dev.sh magic            - xcodegen + xcodebuild Release + relaunch
./dev.sh magic --debug    - Debug-Config
./dev.sh magic --xcode    - Only generate project + open Xcode
./dev.sh magic --no-build - Skip build, just relaunch
./dev.sh magic --no-launch - Build only

Auto-checks for ~/.config/rebreak-magic/config.json and points to template
if missing. Pre-kills any running RebreakMagic instance before launch.
2026-06-02 11:07:48 +02:00
chahinebrini
c16b0f2260 feat(magic): map ADGUARD_* secrets to NITRO runtime config in start-staging
Adds NITRO_ADGUARD_BASE_URL / NITRO_ADGUARD_USER / NITRO_ADGUARD_PASSWORD
exports so Nitro runtimeConfig picks up the AdGuard admin credentials at
runtime. Required for /api/magic/register to provision DoH clients on
dns.rebreak.org.
2026-06-02 10:31:03 +02:00
chahinebrini
ea759cc79c fix(magic): explicit imports for new db/utils functions
Nitro auto-import did not pick up findMagicDeviceByToken / listMagicDevices /
countActiveMagicBindings / createAdGuardClient on first build. Added explicit
imports as safety net.
2026-06-02 09:54:40 +02:00
chahinebrini
c1edef8abd feat(magic): RebreakMagic device-binding + DNS profile
- backend: /api/magic/{register,devices,profile,release} + AdGuard provisioning + 24h cooldown
- prisma: magic_binding_fields migration (additive on UserDevice)
- mac-app: Phase 2 - Login + MacRegistration + Profile install
- marketing: landing section + /download/rebreakmagic + DMG
- lyra: forbidden phrases + RebreakMagic coach guidance
2026-06-02 09:15:19 +02:00
chahinebrini
1dc4e4f9cd fix(chat): voice bubble preview in action menu + popup positioning
- previewNode: add audio case → VoiceNoteBubble renders correctly in blur
  overlay when long-pressing a voice message (was rendering empty/null)
- MessageActionMenu: account for input bar (bottomInset=90) in positioning —
  menu no longer slides behind input bar on bottom messages; barTop clamped
  to never overlap the menu; both above/below paths respect usableH

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-02 08:44:53 +02:00
chahinebrini
603ed9f392 fix(voice): DM recording bar sendIcon arrow-up (matches coach, shared component) 2026-06-02 02:13:29 +02:00
chahinebrini
9d9a17955c fix(voice): compact audio bubble + remove emoji from chat list
- VoiceNoteBubble: width 72%→60%, paddingVertical 4→2, play button 36→30pt,
  waveform height 32→26pt, gap 8→6, duration font 11→10pt, dot 9→7pt
- Chat list: remove 🎤/📷/📎 emoji prefix from attachment type fallback labels

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-02 02:10:42 +02:00
chahinebrini
2e49aad386 feat(voice+chat): voice notes DM, chat list attachment preview, DiGA milestone modal
Voice Notes (DM):
- WhatsApp-style voice recording bar (shared VoiceRecordingBar component)
- Audio bubbles: 80 fixed-2dp bars (Instagram-style thin), space-between layout,
  deterministic waveform, moving blue position dot, WA gray bar colors
- Cancel flash fix: setIsVoiceRecording delayed 350ms so trash flash is visible
- Mic button 44pt (Apple min), hitSlop on all recording controls
- startReply shows 🎤/📷 label for voice/image instead of empty

Chat list:
- lastAttachmentType from backend (getDmConversations now selects attachmentType)
- Shows '🎤 Sprachnachricht' / '📷 Foto' / '📎 Medien' as fallback per type
- User search second stage: GET /api/users/search?q= + debounced frontend section
- Push preview: audio → '🎤 Sprachnachricht', image → '📷 Foto' (was '📎 Anhang')

Blocker iOS Layer 3 (Screen Time):
- ScreentimePasscodeCard visible in locked-in state (was hidden once both layers active)
- Confirmed status loaded from backend on mount
- Numbered step instructions (iOS has no deep link to passcode dialog)
- Guard: only for unsupervised VPN+FC path (!mdmManaged && !nefilterActive)
- URL fallback: App-Prefs:SCREEN_TIME → App-Prefs:root=SCREEN_TIME → openSettings

DiGA Milestone Modal:
- Day 3/7/10 celebratory bottom sheet with soft demographic data ask
- Per-user/milestone AsyncStorage tracking, never shows if demographics filled
- Opens DemographicsAccordion in profile via ?openDemo=1 param

Lyra coach: contextual DiGA demographic nudge (optional, positive moments only)
i18n: DE/EN/FR/AR for voice_message, photo, media_sent, mic_access, diga_milestone,
  screentime steps, chat search strings

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-02 01:59:26 +02:00
chahinebrini
80165c851c chore: NEXT_RELEASE.md — session 2026-06-01 release notes 2026-06-01 11:10:44 +02:00
chahinebrini
585cb73947 feat(coach): voice bar silence/speech detection + trash flash + timer fix
- VoiceBars: active=false → kleine Punkte (Stille), active=true → animierte
  Bars (Sprechen). Übergang fließend via Animated.timing.
- Metering via isMeteringEnabled:true + getStatusAsync() alle 200ms.
  audioLevel (0-1) aus dBFS normalisiert. Threshold >0.1 = Sprechen.
- Trash-Button: 400ms roter Flash (backgroundColor + Icon-Farbe) beim
  Klick bevor Recording verschwindet — wie Instagram.
- Timer: Date.now()-basiert statt Increment → kein Android-setInterval-Jitter.
- VoiceBars volle Breite via flex:1 + justifyContent:space-evenly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-01 10:54:20 +02:00
chahinebrini
7db32ca606 feat(coach): Instagram-style voice recording bar
Beim Aufnehmen ersetzt jetzt eine volle Pill-Bar die Eingabe:
  - Links: Trash-Icon (neutral, dezent)
  - Mitte: Live-Dot (brandOrange) + animierte Waveform-Bars + Timer
  - Rechts: Senden-Button (brandOrange, Pfeil-Icon)

Keine roten Farben mehr, kein separater Mic-Button beim Aufnehmen.
Mic-Button verschwindet komplett während Recording (erst wieder
sichtbar wenn aufgehört). Konsistent mit Rebreak-Farbschema.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-01 10:47:08 +02:00
chahinebrini
bd8d5a3072 fix(dm): Android scroll-to-bottom via scrollToOffset(999999)
scrollToEnd() unterschätzt Content-Höhe auf Android und stoppt
konsistent eine Message zu früh (verifiziert per adb-Screenshot).
scrollToOffset({offset:999999}) wird auf den echten Max-Wert geclampt
und landet immer am absoluten Ende der Liste.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-01 10:42:44 +02:00
chahinebrini
89391a807b fix: Arabic STT + DM scroll + info sheet FormSheet
STT (Arabic):
- Deepgram hat nova-2-general ar/tr-Support eingestellt (400 Bad Request)
- Fix: einheitlich nova-3 für alle Sprachen inkl. ar/tr
- Stale Kommentar aus 2026-05-30 entfernt

DM scroll-to-bottom:
- onLayout auf FlatList hinzugefügt → zusätzlicher scrollToEnd nach
  initialem Layout-Pass (Android-specific race condition)
- onOpenImage im FlatList-Renderer auf Lightbox verdrahtet (war () => {})

Info-Sheet:
- Modal(pageSheet) → FormSheet mit initialHeightPct={0.85}
- Nutzt jetzt unsere eigene Sheet-Komponente konsistent

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-01 10:25:35 +02:00
chahinebrini
0533fcad71 fix(mail): classifier v1.2 — FS-token + extreme-percent + casino-name + threshold 40
- SUBJECT_PERCENT_PATTERN weight 10 → 15
- SCORE_BLOCK_MIDRANGE 50 → 40
- new FS-token pattern (+20): \b\d+\s*FS\b (free-spins abbreviation)
- new extreme-percent pattern (+20): \b[1-9]\d{2,}\s*% (>=100% signals gambling)
- casino in sender display-name (+30): targeted v1.2 reactivation

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-01 10:17:25 +02:00
chahinebrini
f59d1800c7 feat(dm): info sheet + avatar→profile + scroll fix + image cache
- Header: Avatar-Tap navigiert direkt zum Profil des Chatpartners
- Header: Info-Icon (ℹ) rechts öffnet neues Info-Sheet
- Header: kein separater BG mehr — blendet in native Background ein
- Info-Sheet (pageSheet Modal):
    - Partner-Karte mit Avatar + "Profil anzeigen"-Link
    - Geteilte Medien als 3-Spalten-Grid (neueste zuerst)
    - Tap auf Bild → Lightbox-Modal (Vollbild mit Close-Button)
- Scroll-to-bottom: 3-stufiges Timing (rAF + 100ms + 300ms) für
  zuverlässiges Bottom-Scroll auch wenn Bilder nachgeladen werden
- expo-image cachePolicy="memory-disk" überall: ChatBubble-Image +
  alle Images im Info-Sheet + Lightbox
- i18n: dm.view_profile / dm.shared_media / dm.no_shared_media (DE/EN/FR/AR)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-01 10:00:03 +02:00
chahinebrini
24044c3a0c feat(backend): mail_classification_samples row-cap (100k max)
Tabelle war auf 13GB gewachsen und hat heute den Disk voll gemacht.
Neuer täglicher Row-Cap-Job hält die Tabelle unter 100k Rows —
löscht älteste Samples wenn Cap überschritten. CTE-basierter Delete
nutzt created_at-Index, kein Full-Table-Scan.

Bestehende Jobs bleiben: Subject-Nullification (30 Tage) + Sample-Purge
(12 Monate). Row-Cap ist die harte Schranke gegen Disk-Wachstum.
100k Rows ≈ ~500MB — nachhaltig für Staging + Prod.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-01 09:44:55 +02:00
chahinebrini
53d8ace974 chore(native): strings fix + app.config + plist updates
- strings.xml: accessibility_service_summary = "ReBreak — Schutz"
  (war durch Linter mehrfach zurückgesetzt worden)
- app.config.ts + Info.plist: version bumps
- CHANGELOG.md + deploy-runtimes: release-tracking aktualisiert

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-01 09:32:41 +02:00
chahinebrini
372aaa43dd fix(ci): pipeline race-condition + health-check retry + maestro secrets
Hauptproblem: Webhook-Deploy (deploy.sh) und GH-Actions-Deploy
(deploy-from-artifact.sh) liefen gleichzeitig → Race auf .output-staging
und doppelter pm2-restart.

Fixes:
- deploy-from-artifact.sh: setzt .deploy-ga.lock (noclobber, mit PID)
  während Deploy läuft; stale locks werden erkannt und überschrieben
- deploy.sh: prüft .deploy-ga.lock bei Start — wenn GH-Actions aktiv,
  sauberes exit 0 statt Kollision
- Health-Check: Retry-Loop (12× × 5s = max 60s) statt einmaligem sleep 5;
  Infisical-Login + Nitro-Start braucht auf gestresstem Server bis 30s
- maestro-cloud.yml: ungültiges `if: secrets.X != ''` entfernt (secrets
  in if-conditions sind in GH-Actions immer leer); stattdessen expliziter
  secrets-check als erster Step mit klarer Fehlermeldung
- pnpm --prefer-offline in deploy-from-artifact.sh: nutzt Store-Cache
- .gitignore: .deploy-ga.lock ergänzt

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-01 09:32:25 +02:00
chahinebrini
617312f367 fix(vpn): bypass own domains in DNS filter (rebreak.org, rebreak.app)
OAuth-Callbacks gehen an db-staging.rebreak.org — wenn der In-Flight-Cap
kurz erreicht wird, kriegt das SERVFAIL statt einer echten Antwort.
Eigene Infrastruktur-Domains explizit als Bypass deklariert: werden nie
aus der Blocklist geblockt und umgehen den In-Flight-Zähler nicht
(Forward läuft weiterhin normal, aber Block-Entscheidung wird übersprungen).

Gilt für iOS (PacketTunnelProvider) und Android (DnsFilter) gleichzeitig.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-01 05:12:50 +02:00
chahinebrini
b1d382bada testflight 2026-06-01 04:44:19 +02:00
chahinebrini
e227eb0407 chore: trigger deploy after screentime migration fix 2026-06-01 04:38:53 +02:00
chahinebrini
c39dfcbf8b fix(migration): screentime_passcode in korrektes Prisma-Migration-Format
Loose .sql Datei → 20260601_add_screentime_passcode/migration.sql
(prisma migrate deploy erwartet Verzeichnis-Format)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-01 04:34:34 +02:00
chahinebrini
d65ba84eb1 feat(binder): MDMClient, EnrollView improvements + supervise flow_backup
- MDMClient: error handling verbessert
- SuperviseRunner: robustere EOF-nach-Success Erkennung
- EnrollView: Enrollment-Status-Polling, Retry-Logik
- SuperviseView: UX-Verbesserungen
- ConfigureView: minor cleanup
- flow_backup.go: backup flow für supervise-magic

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-01 04:30:28 +02:00
chahinebrini
db0aa6d24e feat(native): Protection Onboarding v2 + Devices + ProtectionSlide
- ProtectionOnboardingSheet: Android a11y 2-step flow mit tamper-arm nach Return
- ProtectionDetailsSheet: cleanup, iOS/Android split, locked-state logic
- ProtectionSlide: neuer Onboarding-Slide für Protection-Intro
- _layout.tsx: reconcileVpn on app-foreground (Android VPN self-heal)
- devices.tsx: Two-device approval flow
- useProtectionState: applyCooldownDisableIfElapsed, forceDisable on cooldown-end
- iOS module Info.plist: bundle version bumps
- app.config.ts: minor config updates
- tmp/.deploy-runtimes: build-time metrics aktualisiert

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-01 04:30:20 +02:00
chahinebrini
adf0d33f1b feat(android): Protection Module v2 — VPN self-heal, boot-receiver, multilang
- RebreakProtectionModule: reconcileVpn, Samsung overlay guide, openPowerDialog,
  armTamperLock preconditions (VPN+a11y required), openAccessibilitySettings fallback chain
- RebreakVpnService: onRevoke auto-recover, blocklist self-heal on empty start,
  ACTION_RESTART für hard-reload nach blocklist-Änderung
- VpnBootReceiver (neu): startet VPN nach Geräte-Neustart wenn filter_enabled=true
- Strings: DE/EN/FR/AR a11y-Guide, Overlay-Permission, Hint-Steps
- RebreakProtectionModule.ts: reconcileVpn, armTamperLock, disarmTamperLock,
  openPowerDialog, openAccessibilitySettings, dismissAccessibilityHint exports

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-01 04:30:07 +02:00
chahinebrini
ab4b9c48e5 feat(ios): Screen Time Passcode als Layer 3 (setup flow)
User generiert 4-stelligen Code in der App, setzt ihn manuell als
Screen Time Passcode → ReBreak speichert ihn auf dem Backend.
Damit kann niemand Screen Time deaktivieren → deny-removal bleibt
aktiv → App nicht deinstallierbar ohne den Passcode.

Backend:
- Profile.screentimePasscode Feld (Migration add_screentime_passcode)
- POST /api/protection/screentime-passcode — Code speichern
- GET /api/protection/screentime-passcode — Code abrufen (nach Cooldown)

iOS UI (blocker.tsx):
- ScreentimePasscodeCard erscheint wenn Layer 1 + 2 aktiv (iOS only)
- Code-Generierung → Einmal-Anzeige → Deep-Link zu Settings → Screen Time
- Bestätigung speichert Code auf Backend, Card zeigt Confirmed-State

Locales: DE/EN/FR/AR screentime_* Keys

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-01 04:19:43 +02:00
chahinebrini
59766f8530 fix(android): block Force Stop + App-Info auf Samsung OneUI
Samsung nutzt generische Klassen (SubSettings, FrameLayout) statt
InstalledAppDetails für die App-Info-Seite — classGuard schlug fehl,
Tamper-Lock griff nicht.

Fixes:
- classGuard für Uninstall-Pfad entfernt: Text-Kombination "rebreak"
  + Deinstall-Keyword reicht (App-Liste zeigt Buttons nie inline)
- Force-Stop-Bestätigungsdialog explizit erkannt via "stopp erzwingen"
  + "abbrechen" + "ok" (Dialog nennt App-Namen nie)
- Throttle-Reset bei TYPE_WINDOW_STATE_CHANGED: eliminiert 400ms-Fenster
  zwischen Activity-Wechsel und erstem CONTENT_CHANGED-Check
- ApplicationDetail (ohne 's') zu Pattern-Listen: Samsung OEM-Variante
- accessibility_service_summary korrigiert: "ReBreak — Schutz" statt
  "Sichert den Schutz gegen Abschalten ab" (HIGH_CONFIDENCE_KEYWORD muss matchen)

Getestet auf Samsung Galaxy A50 (One UI): App-Info-Seite wird sofort
per BACK-Action geblockt, Lyra-Toast erscheint.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-01 04:11:01 +02:00
chahinebrini
2e056c7257 feat(devices): Apple-style two-device approval flow + email fallback
iCloud-Sign-In Pattern: wenn ein neues Gerät versucht sich anzumelden
und das Plan-Limit erreicht ist, kann der User auf einem bereits
angemeldeten Gerät bestätigen — Code wird auf BEIDEN Geräten gezeigt
für visuellen Vergleich (verhindert Code-Forwarding-Attacken).

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

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

Migration läuft automatisch via prisma migrate deploy bei push.
2026-06-01 02:36:28 +02:00