Beim Entfernen des Client-Gates blieb ein <template> ohne Direktive als Wrapper
um den gesamten Inhalt stehen. Vue 3 rendert ein direktivenloses <template> an
dieser Stelle nicht → komplett leere Seite. Wrapper entfernt, Inhalt liegt jetzt
direkt im Root-<div>. Verifiziert: Header + Hero rendern in Safari.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
Client-seitiges Passwort-Gate entfernt: schrieb das Passwort ins offen
erreichbare JS-Bundle (= Leak, identisch zum nginx-PW) und verursachte doppelte
Passworteingabe. Schutz läuft jetzt serverseitig via nginx auf /preview (Seite +
Bilder). Ein Passwort, kein Leak.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- protection.ts: gate recoveringFromBypass on a local 'everActiveHere' flag,
set after a successful activateUrlFilter(). The 'protection off' sheet + tamper
push no longer fire on fresh devices/sims where protection was never activated
locally. Root cause: backend protectionShouldBeActive is the account DEFAULT
(only false after a held cooldown), not an 'ever active' signal.
- locales: blocklist size 208k -> 300k (native de/en/fr/ar + marketing de/en).
- bundles already-deployed prior polish living in these files: native wording
(clean->spielfrei, DiGA wording), marketing de-AI pass (live on prod).
- NEXT_RELEASE.md seeded for the next app release.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
clean-ios.sh rief 'expo prebuild --clean' ohne --platform → wollte auch android/
löschen. Hält ein Gradle-Daemon android/build|.gradle offen, failt 'rmdir android'
mit ENOTEMPTY und hinterlässt ein halb-gewischtes android/ (Source weg, gelocktes
build/ bleibt) → späterer Release-Build failt mit "autolinking.json doesn't exist".
- clean-ios.sh: 'prebuild --clean --platform ios' → android/ wird beim iOS-Clean
gar nicht mehr angefasst.
- deploy.sh: release_android_locks() (gradlew --stop + GradleDaemon-kill + rm
build/.gradle) läuft vor dem android-prebuild --clean in ensure_native_dir.
Ergänzt den ensure_native_dir-Self-Healing-Fix (fe6a63b): jetzt wird der Halbwipe
nicht nur erkannt+repariert, sondern an der Quelle verhindert.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Release 0.4.4 zu Play Internal Testing (Android Tamper-Lock-Präzision +
a11y-Onboarding-Guide), Notes ins CHANGELOG archiviert.
- deploy.sh ensure_native_dir jetzt self-healing: prüft Marker-Datei
(android/app/build.gradle bzw. ios/Podfile) statt nur ob der Ordner
existiert. Ein halb-gewischtes native-Verzeichnis (nur build/-Output) wird
erkannt und via 'expo prebuild --clean' sauber regeneriert, statt mit
"autolinking.json doesn't exist" zu failen.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Tamper-Lock von Keyword-Scanning auf präzise Einzel-Surfaces umgebaut:
blockt nur ReBreaks eigene Screens (Admin-Deaktivierung via DeviceAdminAdd,
a11y-Ausschalten, VPN-Trennen/Surface), nie Listen oder fremde Apps.
- Deny-Removal = Admin-only: OS graut Uninstall+Force-Stop für aktiven
Device-Admin aus; einziger Bypass (Admin deaktivieren) bleibt a11y-gesperrt.
Andere Apps verwalten/force-stoppen/deinstallieren bleibt komplett frei.
- a11y-Onboarding: passiver Bottom-Overlay-Hinweis + Settings-Reset auf
Startseite nach Aktivierung + 1s-Delay vor App-Rückkehr.
- VPN-Trennen-Dialog + a11y-Ausschalten neu abgedeckt.
- a11y-Service-Icon im Plugin (klar als ReBreak erkennbar).
Verifiziert auf A50 per logcat: alle 4 Surfaces blocken, Listen + fremde
Apps frei, keine False-Positives.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Statt Half-Donut (Höhen-Mismatch mit Circles): zwei volle Circles (Mobil/Computer)
+ darunter ein eigener animierter Balken (grün/blau-Segmente, gleiche Easing/Dauer
wie die Ringe) mit Legende. Kein native-Default.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
DeviceSlotDonut bekommt half-Modus. Reihenfolge: Mobil-Circle, Computer-Circle,
Gesamt-Half-Donut (Mobil/Computer-Anteil als zwei Bögen). Alle SIZE×SIZE.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Entfernen passiert am Gerät selbst (Cooldown, win-App/Mac), nicht aus der
Liste gesteuert. Row-Pfeil öffnet nur das Info-/Detail-Sheet.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>