deviceImage()-Helper mappt Plattform→assets/devices/*.png; ersetzt Ionicons
in Geräte-Rows, MagicSheet und Detail-Sheet.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
Tauri 2 DoH-Schutz für Windows: PowerShell-DoH-Takeover, Tamper-Service
(SYSTEM, windows-service), Browser-Policies (Chromium built-in-DNS + eigenes
DoH aus → OS-Resolver), 24h-Cooldown via bestehende magic/*-Endpoints.
GitHub-Actions baut den x64-NSIS-Installer auf windows-latest.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
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>
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>
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>
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.
- 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).
- 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.
- 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).
- 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>
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>
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).
- 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).
- 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>
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.
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.
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
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.
Nitro auto-import did not pick up findMagicDeviceByToken / listMagicDevices /
countActiveMagicBindings / createAdGuardClient on first build. Added explicit
imports as safety net.
- 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>
Beim Aufnehmen ersetzt jetzt eine volle Pill-Bar die Eingabe:
- Links: Trash-Icon (neutral, dezent)
- Mitte: Live-Dot (brandOrange) + animierte Waveform-Bars + Timer
- Rechts: Senden-Button (brandOrange, Pfeil-Icon)
Keine roten Farben mehr, kein separater Mic-Button beim Aufnehmen.
Mic-Button verschwindet komplett während Recording (erst wieder
sichtbar wenn aufgehört). Konsistent mit Rebreak-Farbschema.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
scrollToEnd() unterschätzt Content-Höhe auf Android und stoppt
konsistent eine Message zu früh (verifiziert per adb-Screenshot).
scrollToOffset({offset:999999}) wird auf den echten Max-Wert geclampt
und landet immer am absoluten Ende der Liste.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
OAuth-Callbacks gehen an db-staging.rebreak.org — wenn der In-Flight-Cap
kurz erreicht wird, kriegt das SERVFAIL statt einer echten Antwort.
Eigene Infrastruktur-Domains explizit als Bypass deklariert: werden nie
aus der Blocklist geblockt und umgehen den In-Flight-Zähler nicht
(Forward läuft weiterhin normal, aber Block-Entscheidung wird übersprungen).
Gilt für iOS (PacketTunnelProvider) und Android (DnsFilter) gleichzeitig.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>