138 Commits

Author SHA1 Message Date
chahinebrini
5b0a4d03d2 feat(magic): identify current device via hardwareId, migrate existing devices 2026-06-17 17:18:40 +02:00
chahinebrini
10ca1c4bc5 tmp(backend): debug bypass code 000000 -> charioanouar session 2026-06-17 03:13:36 +02:00
chahinebrini
97977011ae feat(backend): include status, cooldownUntil, lastSeenAt and sleep flag 2026-06-16 20:28:21 +02:00
chahinebrini
4ee6849f3f feat(backend): add cooldown and cancel-cooldown endpoints 2026-06-16 20:24:57 +02:00
chahinebrini
1a270739bc feat(coach): Lyra-LLM auf direkten Anthropic Haiku 4.5 + SOS-Fallback-Kette; ElevenLabs reaktiviert
- Chat (message.post.ts): neuer nativer anthropic-Branch in tryModel
  (api.anthropic.com/v1/messages, x-api-key, system top-level), führt die
  Fallback-Kette claude-haiku-4-5 → gemini-flash-lite → gemini-flash → gpt-4o-mini.
- SOS (sos-stream.get.ts): Dispatch-Refactor mit buildUpstream() + Kandidaten-
  Fallback-Kette (anthropic → gemini → openai). Behebt strukturell den Bug
  "SOS liefert nur Krisen-Text" (vorher single fetch ohne Fallback). Nativer
  Anthropic-Stream: Delta-Parser liest content_block_delta.delta.text.
- nitro.config.ts: anthropicApiKey deklariert (ANTHROPIC_API_KEY).
- plan-features.ts: Legend-Voice zurück auf ElevenLabs eleven_turbo_v2_5
  (Cartesia-Übergang nach Zahlungsproblem 2026-06-08 aufgehoben, neuer Key).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 22:41:59 +02:00
chahinebrini
2c33ba55a4 fix(backend): username (Login-Identifikator) aus öffentlichen Payloads entfernt
community/posts.get.ts + social/profile/[userId].get.ts lieferten neben
nickname auch username an fremde Clients — username ist der Login-
Identifikator ({username}@rebreak.internal) und verletzt die Nickname-
Anonymitäts-Invariante (REQ-COMM-005 / R-DATA-07) + exponiert das halbe
Login-Credential-Paar. Frontend rendert das Feld nirgends (verifiziert);
totes Typ-Feld in stores/community.ts entfernt.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 06:36:16 +02:00
chahinebrini
1493752634 fix(backend): IMAP ECONNRESET crash-loop + dm-push ESM interop
- mail/scan{,-internal}.post.ts + imap-idle: attach imap.on('error')
  + targeted uncaughtException/unhandledRejection guards so a
  connection-level IMAP error (ECONNRESET / TLS disconnect) can no
  longer propagate to a process-level uncaughtException and kill the
  Nitro API (root cause of the staging 502 crash-loop)
- services/push.ts: lazy dynamic-import expo-server-sdk (singleton,
  like voip-push.ts) to fix "Class extends value [object Module]"
  (ESM/CJS undici interop) that broke DM push notifications;
  + nitro.config externals safety net

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 14:59:37 +02:00
chahinebrini
5fb441817f feat(magic): RE-hardening Quick Wins (ACL, #if DEBUG guards, rate-limit)
Härtung der öffentlich downloadbaren Magic-Apps gegen Reverse Engineering
(Assessment: docs/specs/magic-re-hardening.md):
- Windows: protection.json per ACL auf SYSTEM+Admins (DNS-Token nicht mehr von
  Standard-Usern lesbar) — setup.rs
- Mac: MacProfileInstaller.remove() + Debug-Supervision-Modi/Reset nur noch
  #if DEBUG (kein Removal-/Debug-Pfad im Release-Binary)
- Mac: staging-URL einmal als Konstante statt 4x Literal; interne Infra-Notizen
  aus String-Literalen raus
- Backend: Rate-Limit (10/IP/min) auf /api/magic/pair/redeem

NUR Backend-Teil deployt via Push; Mac/Win brauchen Xcode-/Cargo-Release-Build
(zied) + Smoke-Tests vor Release. MagicAPIClient.swift trägt etwas vorbestehenden
WIP mit (gleiche Magic-Client-Domäne).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 05:19:10 +02:00
chahinebrini
db6db547ff fix(magic): DNS-Token auf 48 hex kürzen (AdGuard 63-char clientid-Limit)
randomBytes(32).hex = 64 Zeichen → AdGuard lehnt clientid ab
('hostname label is too long: got 64, max 63') → 'DNS-Provisioning
fehlgeschlagen' bei jeder Magic-Registrierung. Live gegen AdGuard
reproduziert: 64→HTTP400, 62→HTTP200.

- register: randomBytes(24).hex = 48 Zeichen (192 bit, ≤63)
- status: Token-Regex {64} → {1,63} (Länge nicht hart koppeln)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 23:33:56 +02:00
chahinebrini
a95e66560d feat(magic): Hard-Lock + Geräte-UX (Push, Realtime, Detail-Sheet, Offline-Removal)
Devices/Magic:
- Offline-Profil-Enroll deaktiviert (410) — Lock-PW würde im Klartext im
  Download landen; stationärer Schutz läuft jetzt nur über Rebreak Magic
- Mac-DNS-Template: ProhibitDisablement (Filter nicht abschaltbar)
- Push "Neues Gerät verbunden" an mobile Geräte bei neuer Bindung
- Realtime auf user_devices → Settings aktualisiert Magic-Bindings live
- Geräte-Detail-Sheet (Tap auf Gerät): Status, verbunden-seit, Schutz-Donut

Hard-Lock (server-gehaltenes Removal-PW, User sieht es nie):
- magic_removal_password generiert/gespeichert + in Profil injiziert (Lazy-Backfill)
- Reveal NUR bei Account-Löschung (user/delete) + Kündigung (stripe webhook),
  per Resend-Mail + in-Response
- Signing config-gated (inaktiv ohne Cert; Lock greift auch unsigniert)

Migrations: user_devices-Realtime-Publication + magic_removal_password-Spalten

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 22:26:25 +02:00
chahinebrini
80be124592 fix(dns): blocklist als ||domain^ statt apex-only → blockt Subdomains
Apex-only-Format (bare domain) blockte nur z.B. bet365.com, NICHT
www.bet365.com — Casino-Content + Google-Ad-Links liegen aber auf www.
Adblock-Syntax ||domain^ deckt Domain + alle Subdomains ab.
Betrifft alle DNS-Plattformen (Mac/Windows/DoH-Server).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 13:26:16 +02:00
chahinebrini
7f529c3be3 feat(privacy): Coach-Payload an LLM-Provider pseudonymisieren (Art.9/DSGVO)
Schliesst hans-muellers K1-Befund (Datenschutz-Audit): der Coach-Prompt
sendete Identifier + Art.9-nahe Daten an US-LLMs (Gemini/OpenAI/Anthropic).

- message.post.ts: Geburtsjahr/exaktes Alter -> Altersgruppe (Dekaden-Bucket);
  Stadt komplett entfernt (Bundesland bleibt). Geschlecht/Familienstand/Beruf/
  Nickname unveraendert (gewollte Personalisierung; Nickname = Pseudonym).
- lyraMemoryExtract.ts: Extraction-Prompt reduziert Dritt-Klarnamen auf Rolle
  ("Frau Maria" -> "seine Frau"), keine Orte/Arbeitgeber im Memory-Content.
- 08-datenschutz-audit: Payload-Audit-Platzhalter durch Vorher/Nachher-Tabelle
  ersetzt, K1 erledigt, ZDR-Update (DPA/SCCs deemed-signed, TIA offen).

Pseudonymisierung zaehlt jetzt als zweite Schutzmassnahme neben ZDR fuer die TIA.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 08:35:13 +02:00
chahinebrini
96e1b8368c feat(lyra): deterministisches Krisen-Sicherheitsnetz (R-LYRA-01)
LLM-unabhaengiges Sicherheitsnetz fuer Lyras SOS-Pfad, schliesst das
Top-Risiko der Risiko-Akte (verpasste Krise, ISO 14971 R-LYRA-01).

Backend:
- crisis-filter.ts: deterministische Krisen-/Suizid-Erkennung (DE primaer,
  EN/FR/AR Grundabdeckung) auf den letzten User-Nachrichten, synchron, kein LLM
- sos-session.post: liefert crisisLevel sofort an die App (vor Stream-Start)
- sos-stream: sendet bei Krise zuerst 'crisis_chips' (BZgA/112/Telefonseelsorge);
  Fallback an 3 Stellen (LLM-Fehler/Abbruch/keine Chips) -> nie leerer Screen
- 43/43 Unit-Tests (crisis.json positiv, harmless.json False-Positive-Guard)

Frontend (urge.tsx):
- permanente rote Krisen-Bar oben, durch LLM-Chips nicht ueberschreibbar
  (eigener State-Slot), Hotline-Chips als tel:-Links
- neue Locale-Strings DE/EN

Risiko-Akte: R-LYRA-01 Restrisiko HOCH -> MITTEL.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 07:56:34 +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
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
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
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
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
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
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
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
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
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
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
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
chahinebrini
efca157969 fix(backend): device-mgmt cleanup + stats rejected fallback + realtime refresh
- devices: cleanupStaleDevices() purges phantoms >14d not bound; called from
  GET /api/devices + register limit-check. Deterministic sort
  (lastSeenAt, createdAt, id) so iPad+iPhone see identical order.
  Merge-cutoff tightened 30d -> 7d.
- stats: rejected aggregates from notifications(type='domain_rejected')
  via Math.max — admin reject cascade-deletes submission row.
- blocker.tsx: useDomainSubmissionRealtime triggers blockerStats.refresh()
  directly (not stale-check only) so info-sheet shows fresh rejected count.
2026-06-01 02:23:27 +02:00
chahinebrini
55e3cdfb26 fix(transcribe): pass language=ar/tr to nova-2-general so Lyra answers in correct language
Without explicit language param, nova-2-general falls back to multilingual
auto-detect and often misdetects arabic audio as english (phonetic transcript
'salam alaikum' instead of 'السلام عليكم'). detectLang() then sees only
latin chars and answers in english.

Confirmed via Deepgram docs: nova-2-general accepts language=ar and language=tr
(only nova-3 rejects them with HTTP 400).
2026-05-31 01:37:11 +02:00
chahinebrini
3a4e1ecfba feat(coach): switch Lyra to Gemini 2.5 Flash Lite (Groq+OpenRouter quotas dead)
- Primary: gemini-2.5-flash-lite (~789ms TTFR, ~10x cheaper than Haiku, no reasoning overhead)
- Fallback 1: gemini-2.5-flash (smarter when Lite overloaded)
- Fallback 2: gpt-4o-mini (anchor on different provider)
- message.post.ts: candidates chain replaced
- sos-stream.get.ts: gemini-flash-lite default + auto-fallback to gpt-4o-mini if key missing
- nitro.config.ts: geminiApiKey runtimeConfig
- start-staging.sh: GEMINI_API_KEY export + NITRO_GEMINI_API_KEY

OpenRouter credits = 0, Groq TPD exhausted - users get 503 currently.
2026-05-31 01:07:10 +02:00
chahinebrini
487af4ede1 fix(coach): duplicate lastUserMsg declaration in message.post 2026-05-31 00:15:28 +02:00
chahinebrini
685782b538 fix(coach): dynamische Sprache (Text-Detection + App-Locale-Fallback)
LLM-Prompt (message.post + sos-stream):
- LANG_INSTRUCTIONS Map raus, ersetzt durch dynamische Instruktion
  'Reply in {detectedFromUser} ... fallback: {appLang}'
- Lyra matcht jetzt die Sprache der letzten User-Message (per
  detectLang Unicode-Detection); App-Locale ist nur noch Fallback
- Instruktion doppelt eingehängt (Anfang + Ende des System-Prompts)
  gegen recency bias bei langen deutschen Prompts

TTS (speak dispatcher + speak-cartesia + speak-elevenlabs):
- Kein 'de'-Default mehr für language. detectLang(text, locale) leitet
  Sprache primär aus dem Antwort-Text ab (Arabic/Cyrillic/CJK/Turkish-
  Letters), Locale als Fallback
- Cartesia + ElevenLabs: language/language_code nur senden wenn
  ableitbar, sonst Provider auto-detect statt erzwungenem 'de'
- speak-cartesia: sonic-2 → sonic-3 (Multi-Lang, war beim Dispatcher-
  Fix gestern vergessen worden)
- Google: en-US neutraler Fallback statt de-DE-Bias

Neu: server/utils/detect-lang.ts
2026-05-31 00:12:40 +02:00
chahinebrini
69f01c5a0c feat(chat): DM-Reaktionen + Soft-Delete Backend + comment_likes realtime
- db/chat: getDmHistory liefert reactions + deletedAt; toggleDmReaction
  (WA-Toggle) + softDeleteDmMessage (nur eigene)
- Endpoints: /api/chat/reaction (Emoji-Toggle, 7er-Allowlist) +
  /api/chat/delete-message (Soft-Delete für alle)
- dm/[userId].get: aggregierte reactions + deleted im Response, Inhalt bei
  Tombstone geblankt
- Migration: comment_likes zur supabase_realtime-Publication (fixt
  Post-Kommentar-Like-Realtime, eskaliert von rebreak-native-ui)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 11:18:32 +02:00
chahinebrini
38df6fc79d feat(chat): push notifications for DMs + rooms
Backend:
- Prisma PushToken model + chat_push_enabled flag on profiles
- Migration 20260530_add_push_tokens (push_tokens table + profile flag)
- Service sendChatPush with expo-server-sdk (auto-disable invalid tokens)
- Fire-and-forget push trigger in sendDirectMessage + createRoomMessage
- API POST /users/me/push-token (upsert) + DELETE (soft-disable)

Client (rebreak-native):
- usePushTokenRegistration hook: permission, getExpoPushTokenAsync,
  Android channel 'chat', POST to backend; idempotent per session
- Notification tap deep-link: dm -> /dm?userId, room -> /room?roomId

Deploy:
- run_quiet spinner for silent altool/xcodebuild/gradle phases
- Release-notes pipeline (--notes flag / NEXT_RELEASE.md / interactive)
  archived to CHANGELOG.md, printed with ASC + Play Console links
- Default version bump ON (--no-bump opt-out), build cleanup
- NEXT_RELEASE.md with push-notification release note
2026-05-30 08:16:45 +02:00
chahinebrini
d31d5b3b83 fix(coach): use nova-2-general for ar/tr STT to avoid 400 errors 2026-05-30 02:14:16 +02:00