193 Commits

Author SHA1 Message Date
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
444688b6e9 test(eval): Concurrency-Limit + 429-Retry für Lyra-Eval-Suite
LYRA_EVAL_CONCURRENCY (Batch statt Promise.all-30-parallel) und
LYRA_EVAL_TIMEOUT_MS als Env-Schalter, plus 429-Retry mit Backoff —
nötig für Live-Runs gegen Provider mit niedrigem TPM-Limit (Groq
on-demand 12k). Default-Verhalten unverändert; Prompts/Regeln/
System-Prompt nicht angefasst.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 06:36:32 +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
df3c4fafa3 fix(mail): force full-sweep on idle-daemon recovery (cold-start + downtime>5min)
Eine global-blocklistete Casino-Mail (mpmgame.com) rutschte in die Inbox,
obwohl die Absender-Domain Layer-2-Hard-Block ist. Ursache: kein Erkennungs-
Fehler, sondern ein Scan-Gap. Der idle-daemon crash-loopte heute (Port-3016-
Race, ~49 Restarts) und machte nach jedem Reconnect/Cold-Start nur einen
INKREMENTELLEN Scan (lastUid). Mails, die waehrend einer Downtime ankamen,
fielen damit dauerhaft durch das last-seen-UID-Raster.

Fix in backend/imap-idle/index.mjs:
- Cold-Start (Prozess-/pm2-Restart): immer forceFullSweep
- Reconnect nach Error mit Downtime > 5min (RECOVERY_SWEEP_THRESHOLD_MS):
  forceFullSweep beim naechsten erfolgreichen Connect
- Routinemaessiger IDLE-Renew (10min Close-Reopen): bleibt inkrementell
  -> kein teurer Dauer-Sweep im Normalbetrieb
- triggerScan(conn, {forceFullSweep}) + Coalesce-State "full"/"incremental"
  (ein full-Trigger wird nicht von einem spaeteren inkrementellen degradiert)

scan-internal.post.ts wertet body.forceFullSweep bereits korrekt aus.
Im Zweifel Full: eine global-blocklistete Domain darf nie durchrutschen.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 21:58:00 +02:00
chahinebrini
7ce5e58f8d fix(backend): fix imapflow bundling — inline-Funktion statt fragiler Regex
Die inline-Regex /^(?!imapflow)/ griff nur gegen bare Specifier, nicht
gegen aufgelöste absolute Pfade (z.B. /node_modules/.pnpm/imapflow@x/…).
Nitro prüft inlineMatchers VOR externalMatchers — bei absoluten Pfaden
startete der String mit "/" → Lookahead ^(?!imapflow) schlug durch →
imapflow wurde gebundelt → imap-flow.mjs-Chunk → util.inherits-Crash
→ scan-internal 500 (Incident 2026-06-05, Regression in 1493752).

Fix: inline als Funktion mit expliziter blocklist (imapflow, expo-server-sdk,
@supabase/supabase-js). Deckt bare Specifier UND aufgelöste node_modules-Pfade.
Lokal verifiziert: imap-flow.mjs-Chunk weg, alle 3 Packages in server/package.json.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 15:35:52 +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
547f86187b fix(magic): createAdGuardClient idempotent — 400 → clients/update
Verwaiste AdGuard-Clients (magic_<deviceId> existiert, aber DB-Row fehlt nach
Crash zwischen clients/add und DB-Upsert) führten beim Re-Register zu 400 → 502.
Jetzt: bei 400 auf clients/update zurückfallen und den bestehenden Client auf
die frisch generierte clientId umbiegen. Behebt Magic-Register-502.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 02:45:28 +02:00
chahinebrini
0bff819b09 fix(tts): Legend übergangsweise auf Cartesia (ElevenLabs payment_issue 401)
ElevenLabs-Account hat ein Zahlungsproblem → Legend-TTS bekäme 502.
Leite Legend bis zur Klärung auf Cartesia sonic-3 (unlimited), damit
Legend-User Sprachausgabe behalten. Revert-Config steht im Kommentar.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 03:04:17 +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
ac05e255da feat(diga): Technische-Akte Runde 1+2 — Requirements, Risiko-Akte, Datenschutz-Audit, Lyra-Eval
DiGA-Dossier weiter aufgebaut (docs/specs/diga/):
- 03 Requirements (57 REQ-IDs aus dem Code, Traceability-Anker)
- 04 Risiko-Akte (ISO 14971 Erstliste; R-LYRA-01 = verpasste Krise als Top-Risiko)
- 05b Test-Verifikation (Maestro/Vitest-Inventar, IEC-62304-Luecken)
- 05c Lyra-Eval (Suite-Doku)
- 08 Datenschutz-Audit (hans-mueller; Groq/Art.9, DSFA-Pflicht, Mail-Agent, Anonymitaet)
- 00 Dossier-Plan Status aktualisiert

Lyra-Eval-Suite: backend/tests/eval/ (30 Prompts, 5 Kategorien, Vitest-Runner,
Mock-Modus ohne Key; Live-Run misst Crisis-Recall).

Konvergenter Befund aller 3 Agents: Lyras Krisen-Pfad haengt zu sehr am LLM
(R-LYRA-01 + fehlender SOS-Handler-Fallback) -> deterministisches Sicherheitsnetz noetig.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 07:38:11 +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
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
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
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
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
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
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
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