573 Commits

Author SHA1 Message Date
chahinebrini
90f63eb21e feat(diga): Regulatory-Doku-Agent + Technische-Akte-Start (Zweckbestimmung)
DiGA/MDR-Vorbereitung als eigener Workstream:
- Neuer Agent 'diga-regulatory' (Dr. Marlene Brandt) — owns docs/specs/diga/,
  draftet die Technische Akte (Zweckbestimmung, MDR-Klassifizierung, ISO 13485/14971,
  IEC 62304, klinische Bewertung, Labeling, PMS, QMS-Templates). Ergänzt hans-mueller
  (Datenschutz) + rebreak-strategist (Business); validiert nicht selbst (Profi-Grenze).
- docs/specs/diga/00-dossier-plan.md — Plan + Arbeitsteilung (Claude draftet, Gründer
  entscheidet, Profi validiert) + 10-Dokumente-Landkarte mit Status.
- docs/specs/diga/01-zweckbestimmung-v0.md — Keystone-Entwurf (Intended Use, Indikation
  F63.0, 3-Ebenen-Wirkmechanismus, Abgrenzung kein Therapieersatz, Klassifizierungs-Hinweis).

Naechste Aufgaben fuer den Agent (aus dem Code, ohne Gruender-Input): 03 Requirements,
04 Risiko-Erstliste, 05 SOUP + Architektur.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 06:39:13 +02:00
chahinebrini
eb95258b5e docs(marketing): Rebreak Magic (Selbstbindung) als 4. USP in alle 3 Mails
iOS no-erase Supervision als therapeutisches Selbstbindungs-/Commitment-Device-
Argument formuliert (Schutz im Drang-Moment nicht löschbar, nur via Vertrauens-
person/Cooldown, ohne Geräte-Reset). Maßvoll positioniert: in DE/für Glücksspiel-
sucht einzigartig, gratis im Legend-Tier (vs. kostenpflichtige US-Tools wie TechLockdown).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 06:08:17 +02:00
chahinebrini
db8e38ae6c docs(marketing): FAGS+NLS Förder-/Partnerschafts-Plan + 3 Mail-Entwürfe
Konkreter Go-to-Market-/Funding-Plan für die Marketing-Phase:
- Strategie: 2 Knoten (FAGS/Ilona + NLS Hannover), Geld-Mechanik (Modellprojekt →
  gemeinnütziger Träger → Rebreak als bezahlter Partner), Win-Win pro Partner.
- Sequenz: erst echte Daten + Fachstellen-Validierung (Phase 1), dann Förderung
  via NLS/NDS-Modellprojekt (Phase 2). Daten machen den Förder-Pitch glaubwürdig.
- Plan B/C: nicht single-threaden auf Ilona — STEP gGmbH / Lukas-Werk / NLS parallel.
- GmbH-Reframe: keine 25k Cash nötig (UG/Sachgründung), Kredit erst später.
- 3 sende-fertige Mails: Ilona (warm, ohne Förder-Ask), Lukas-Werk/STEP (Testphase
  = echte Daten), NLS (digitale Ergänzung zu abgezockt! + Modellprojekt).
- Gamban-Vergleich + USP-Hervorhebung (IMAP-IDLE-Mail-Schutz, Lyra).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 06:02:54 +02:00
chahinebrini
c7fc237dfd feat(android-protection): device-admin uninstall-block + boot-receiver + config plugin
Android self-bind protection auf nahezu MDM-Niveau ohne Device-Owner:
- Device-Admin (RebreakDeviceAdminReceiver) blockt Uninstall OS-seitig, aktiv ab
  Boot ohne Prozess/a11y. Deaktivierung nur via 24h-Cooldown (removeDeviceAdmin in
  forceDisable). a11y blockt die DeviceAdminAdd-Settings-Seite (Class-Match, auf
  Samsung One UI per Logcat verifiziert).
- Boot-Receiver (RebreakVpnBootReceiver) startet VPN+a11y nach Reboot, damit der
  Tamper-Lock ohne manuellen App-Start hochkommt.
- Manifest-Wiring (Device-Admin-Receiver, Boot-Receiver, RECEIVE_BOOT_COMPLETED,
  device_admin.xml) ins with-rebreak-protection-android Config-Plugin verlagert →
  ueberlebt 'expo prebuild' (android/ ist gitignored).
- a11y-Detection zurueck auf die funktionierende Version: zu breites 'loeschen'-
  Uninstall-Keyword raus (blockte halbe Settings); a11y-Label jetzt 'ReBreak Schutz'.
- a11y-Deeplink behaelt den Samsung-Step-Guide (openAccessibilitySettings).

Session-Frontend in diesem Batch:
- Avatar-Placeholder: neutrales clarity-avatar-line SVG statt dominantem Blau.
- DiGA-Milestone folgt kumulativen protectedDays (erreicht rueckfall-anfaellige User).
- Dev-Build crasht nicht mehr ohne CallKit-Native-Modul.
- VPN-Permission-Dialog nur noch im Bypass-Fall.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 04:52:49 +02:00
chahinebrini
6a3c1e13da feat(lyra): admin DiGA-reminder post category
New 'erinnerung' topic for manual Lyra community posts that gently remind
users they can add optional, anonymous profile details. Wording stays
jargon-free (no 'DiGA'/'data'/'study'). Manual-only, not in the auto-cron
catalog.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 00:11:01 +02:00
chahinebrini
b757486579 fix(mail): forceFullSweep on domain-add + 30s idle tick
Domain/display-name adds now force a full re-scan so newly-added gambling
senders are caught immediately instead of waiting for the incremental UID
window. IMAP-idle NOOP tick lowered 2min -> 30s to close the Junk-folder gap
faster (Outlook drops straight into Junk, which IDLE does not watch).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 00:11:01 +02:00
chahinebrini
d31e45e2a8 feat(streak): protection-coverage metric (DiGA core) replacing broken streak
The old streak was non-functional: streaks.current_days was always 0 (never
computed/incremented), and the profile page read me.streak (0) + account
created_at as the "since" date — showing "0 days protected since <signup>"
for everyone. This is the DiGA key metric, so it had to be rebuilt.

New model: optimistic protection-coverage based on actual VPN/MDM protection
state, never resets to 0.
- backend: append-only protection_state_log + migration; POST /api/protection/event
  (ingestion, deduped) + GET /api/protection/coverage (read-time compute, no cron);
  server-side cooldown_disable event on cooldown resolve. Generous >6h-off/day rule.
- frontend: report protection on/off transitions (initial + flips, deduped) from
  useProtectionState; rewrote profile StreakSection → half-donut (protected vs
  unprotected) + progress bar (current streak → personal record) + empty state.
- coverage starts fresh from deploy (no historical backfill — clean data for DiGA).
- spec: docs/specs/protection-coverage-streak.md (shared contract).
- old streaks/streak_events/profiles.streak left intact (coach/scores consumers).

Also adds go-to-market one-pagers under docs/marketing/.

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

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

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 10:03:27 +02:00
chahinebrini
084f821bc5 fix(mail): incremental scan UID search returned seq-nums not UIDs
Der inkrementelle Scan-Pfad rief imap.search({ uid: 'X:*' }) ohne das
zweite { uid: true }-Argument auf → ImapFlow sendet "SEARCH UID X:*"
statt "UID SEARCH UID X:*" → Server antwortet mit Sequence-Numbers.
Die nachfolgende fetchAll(..., { uid: true }) interpretiert diese als
UIDs → fetcht die falschen (alten) Mails → neu eingegangene Gambling-
Mail (höchste echte UID) wird nie klassifiziert/gelöscht (>15min Lag).
Auch Ursache des "Command Error. 10" Log-Spams (Mega-UID-Liste).

Fix: { uid: true } als zweites search()-Argument.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 08:43:59 +02:00
chahinebrini
1f73bd8d8d fix(mail): BigInt-Serialisierung blockierte Phase-2-Persistierung
imapflow.status() liefert uidValidity als BigInt. Der Code reichte den BigInt in
JSON.stringify (patchFolderScanState) → 'TypeError: Do not know how to serialize
a BigInt' → vom stummen connection-level catch verschluckt → weder
patchFolderScanState noch markFullSweepDone liefen je → folder_scan_state blieb
{} + last_full_sweep_at NULL → inkrementeller Scan aktivierte nie (immer Full-Sweep).

Fix:
- serverUidValidity: Number((status).uidValidity ?? 0) — BigInt → number vor JSON.
- Stumme catches (auth/lock/conn) loggen jetzt; Persist-Calls (patchFolderScanState
  x2, markFullSweepDone) in eigene try/catch mit console.error — Diagnostik bleibt
  drin für Post-Deploy-Verify.

Lokal verifiziert: Build EXIT 0, imapflow extern.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 12:16:09 +02:00
chahinebrini
cc549c7f17 perf(mail): re-apply incremental UID-scan (Phase 2) — safe w/ externals fix
Wiedereinspielung von mo's inkrementellem Scan (war nach dem imapflow-Bundle-
Incident zurückgerollt). Jetzt safe, weil der nitro externals-Fix (d64f31d)
imapflow robust extern hält — lokal verifiziert: Build clean, imapflow in
node_modules (kein Chunk), kein util.inherits.

- scan-internal: inkrementeller UID-Scan (status uidNext/uidValidity, search
  UID>lastUid, leere Ordner skip), UIDVALIDITY-Wächter, täglicher Quality-Full-
  Sweep (last_full_sweep_at). Klassifikation/Delete/Consent 1:1.
- db/mail: patchFolderScanState (atomic JSONB-merge) + markFullSweepDone;
  toter getAllActiveMailUserIds entfernt.

Schema + Migration (folder_scan_state, last_full_sweep_at) sind bereits live.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 11:59:44 +02:00
chahinebrini
d64f31d115 fix(nitro): robuste imapflow-Externalisierung — behebt scan-internal 500
Der inline-Negative-Lookahead (/^(?!...)(?!imapflow)/) griff nur auf den nackten
Specifier, nicht auf aufgelöste node_modules-Pfade. Bei Module-Graph-Shifts
(Phase-2 Prisma-Felder) wurde imapflow doch gebundlet → CJS-inherits-Bruch
(util.inherits: superCtor.prototype undefined) → scan-internal 500 → Mail-Filtern
(USP) down (Incident 2026-06-05).

Fix: expliziter external-Eintrag mit Pfad-Regex /(^|node_modules/)imapflow(/|$)/
erzwingt imapflow robust extern. Lokal verifiziert: imapflow landet in
.output/server/node_modules + output-package.json, NICHT als chunk.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 11:29:39 +02:00
chahinebrini
0dbaac97a2 revert(mail): roll back Phase-2 scan-internal — fixes 500 (CJS-extends bundle break)
Phase-2-Rebuild reaktivierte den bekannten imapflow/node-apn util.inherits-Bundle-
Bruch → scan-internal warf 500 → Mail-Filtern (USP) down. Rollback von
scan-internal.post.ts + db/mail.ts auf den funktionierenden Stand (5b57bea).

Schema (folder_scan_state, last_full_sweep_at) + Migration BLEIBEN angewendet —
kein Prisma-Drift; die Spalten warten ungenutzt auf den gefixten Phase-2-Retry.
Root-Cause (warum der inkrementelle imap.status/search-Pfad das Bundle bricht)
muss vor erneutem Phase-2-Deploy in der nitro-Externalize-Config gelöst werden.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 11:08:03 +02:00
chahinebrini
04e2979b8d perf(mail): incremental UID-scan + daily quality full-sweep
Backend-Lag-Fix Phase 2 — entfernt die Scan-Grundlast an der Wurzel:
- mail_connections: +folder_scan_state JSONB, +last_full_sweep_at TIMESTAMPTZ
  (additive Migration, DEFAULT '{}' deckt Bestandsrows; erster Lauf = Full-Sweep
  wie bisher → null Verhaltens-/Qualitätsänderung initial).
- scan-internal: pro Ordner status(uidNext,uidValidity); inkrementeller
  search(UID > lastUid) statt Last-200-Refetch. Leere Ordner → skip. UIDVALIDITY-
  Wächter (Server-Renumber → einmal Full-Sweep). maxUid persistiert via JSONB-Merge.
- Quality-Full-Sweep 1x/Tag (last_full_sweep_at) re-scannt Last-200 → Blocklist-
  Updates greifen rückwirkend. Klassifikation/Delete/Consent-Logik 1:1 erhalten.
- db/mail: patchFolderScanState (atomic ||-merge) + markFullSweepDone; toter
  getAllActiveMailUserIds entfernt (Cron weg).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 10:56:55 +02:00
chahinebrini
5b57bea9c0 perf(mail): kill redundant 30min scan-cron + in-flight scan guard
Backend-Lag-Fix Phase 1 — entlastet die CPU-Dauerschleife im Mail-Stack:
- delete mail-scan-cron.ts: der 30-Min-Nitro-Cron scannte alle User parallel
  (Promise.allSettled) und war redundant zum IMAP-IDLE-Daemon (Single Source
  of Truth). Reine Dauerlast ohne Mehrwert.
- imap-idle: In-Flight-Guard (scanInFlight + coalescePending). triggerScan ist
  jetzt re-entry-safe — pro Connection max. 1 aktiver + 1 pending Scan statt
  bis zu 8 gestapelt pro 2-Min-NOOP-Tick. Gilt für NOOP + exists-Event.
- plan-features: Pro mailAgents 3->2 (+ Math.min-Hack in coach/message aufgeräumt).

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

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

Verified: foreground call screen stays up the full UNANSWERED_MS (35s) and
caller-side hangup('unanswered') correctly triggers iPhone closeScreen via
cancel-broadcast.
2026-06-04 21:48:34 +02:00
chahinebrini
7fae4539ae diag(calls): add VoIP+push-token+ring-target logs; fix /call mount race
- AppDelegate: NSLog for didUpdate token, didInvalidate, didReceiveIncomingPush
- backend/push: log [push-token] register, [call-ring] receiver token-counts +
  expo-push-fanout for android-fallback
- app/call.tsx: 250ms grace window before closeScreen on initial idle (fixes
  'foreground call flashes briefly then disappears' race when dm.tsx
  startCall set() hasn't propagated through useCallStore selector yet)
2026-06-04 20:37:43 +02:00
chahinebrini
43eeeb3716 fix(calls): VoIP push + ring logging; call-DM gets proper preview
- ring.post: log [ring] when triggered
- voip-push: log [voip-push] sent on success with env (prod/sandbox) + callId
- chat.ts sendDirectMessage: when attachmentType=='call' parse audio:<state>:<sec>
  into proper preview (Verpasster Anruf, Anruf abgelehnt, Anruf (m:ss), \u2026)
  so post-call push has body text instead of empty.
- callkit.startOutgoingCall: skip on Android (telecomManager opens dialer UI \u2014
  wrong for in-app WebRTC; iOS-CallKit only for audio-session mgmt).
2026-06-04 19:54:51 +02:00
chahinebrini
6a907cf89b fix(calls): sandbox/prod VoIP-push failover + foreground CallKit-UI suppress
- voip-push: build both APNs Provider (production+sandbox) and try each per
  token with memoization. Fixes BadDeviceToken on Xcode-Dev-Builds where the
  token is Sandbox-only.
- stores/call: only call callkit.displayIncomingCall when app NOT in foreground
  \u2014 in foreground the /call route handles ringing UI, otherwise double UI
  (system banner + fullscreen).
- patch react-native-callkeep: New-Arch TurboModule compatibility (no overloads,
  no Bundle params in @ReactMethod).
- pushTokenRegistration: more verbose [voip] diagnostics.
2026-06-04 19:42:44 +02:00
chahinebrini
fb2d90b947 fix(calls): no duplicate incoming-call notifications
- backend: skip Expo alert push to iOS devices that already received VoIP push
  (CallKit + banner = double ring)
- native: receiveIncoming no longer triggers InCallManager.startRingtone —
  CallKit/ConnectionService play their own ring. Dedup if same callId
  arrives twice (Realtime + VoIP-Push race).
2026-06-04 18:28:00 +02:00
chahinebrini
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