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>
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>
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>
- 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).
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.
- 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>
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.
- MessageActionMenu: scharfe Preview-Kopie der gedrückten Bubble am Anker
(bleibt über dem Blur sichtbar, WhatsApp-Stil) statt mitgeblurrt
- Reaktions-Leiste: kein Ring/Hintergrund mehr, aktives Emoji nur leicht größer
- Reaction-Pills: plain Emoji + Count ohne Hintergrund/Border
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
In der "Vordefinierte Top-Seiten"-Sektion der VIP-Liste ein
"Seite vorschlagen"-Link → SuggestCuratedSheet: Domain-Eingabe →
POST /api/curated-domains/suggest (Land via Geräte-Region). Response-
Handling: Erfolg / schon vorgeschlagen / approved / rejected / ungültig.
- useCuratedSuggest.ts (neu), SuggestCuratedSheet.tsx (neu)
- VipDomainList.tsx: Suggest-Link in der curated Sub-Sektion + Sheet-State
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Der setTimeout(80)+onImageLoad-Ansatz war ein Timing-Hack gegen ein
strukturelles Problem (lazy Item-Measurement unter Fabric -> scrollToEnd
landet zu kurz). Stattdessen jetzt inverted FlatList: Index 0 sitzt
permanent am Bildschirmrand, neueste Nachricht immer sichtbar.
- dm.tsx: inverted + reversedMessages, Gruppen-Logik gespiegelt,
manuellen Auto-Scroll + keyboardHeight-State entfernt
- ChatBubble.tsx: onImageLoad-Prop entfernt (obsolet)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- useCustomDomains: CustomDomain um vipDeferUntil/vipEvictAt, AddDomainResult
um vipFull/newDomainId; addDomain liefert vipFull durch; submitVipSwap()
- VipSwapSheet (neu): Dialog wenn VIP voll — User wählt eine eigene Domain,
die in 24h ersetzt wird
- VipDomainList: Badge „wird in Xh ersetzt" auf der ersetzten Kachel
- blocker.tsx: vipFull → AddDomainSheet zu, VipSwapSheet auf
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Bug 1 — Chat-Liste: RefreshControl nutzte React-Querys `isRefetching`,
das bei JEDEM Background-Refetch (focus-/stale-getriggert) true wird →
nach Zurück-Navigation hing der Pull-to-Refresh-Spinner endlos. Fix:
eigener `userRefreshing`-State, nur bei explizitem Pull-to-Refresh true,
im finally zurückgesetzt.
Bug 2 — Conversation scrollte nicht bis zur letzten Nachricht, wenn die
ein Bild war: onContentSizeChange-scrollToEnd feuerte vor dem Bild-Load.
Fix: ChatBubble bekommt onImageLoad-Callback, die letzte Bild-Nachricht
triggert nach dem Laden erneut scrollToBottom.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- PostCard: Bilder mit borderRadius 10 + overflow:hidden — Ecken wieder rund
- dm.tsx: myUserId synchron aus useAuthStore statt async getSession —
behebt den hängenden Lade-Spinner beim Zurück aus einer Conversation
(async getSession-Fenster auf jedem Mount → enabled-Flackern der Query)
- ChatBubble: expo-image memory-disk-Cache + 200ms-Transition für
smootheres Bild-Laden
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Custom-Domain-Slots sind jetzt EIN gemeinsamer Pool für web + mail
(Pro 10 / Legend 20) statt getrennter web/mail-Buckets. Free-Tier ist
entfallen — PLAN_LIMITS hat nur noch pro + legend, getPlanLimits
defaultet auf pro.
Backend:
- plan-features: customDomains ist eine Zahl (CustomDomainLimits weg)
- index.post: Slot-Check gegen Gesamt-Count, Fehler einheitlich LIMIT_REACHED
- index.get: liefert { items, count, limit }
- change-preview + coach/message an die neue Form angepasst
Frontend:
- useCustomDomains: count/limit (Zahlen) statt countsByType/limits
- AddDomainSheet: ein generischer Limit-Hinweis (error_limit_reached)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
VIP-Liste-Sektion: zwei Kachel-Sektionen statt flacher Chips —
"Meine VIP-Domains" (eigene Custom-Domains, Stern + Status-Badge) und
"Vordefinierte Top-Seiten" (kuratiert, schlicht). Read-only, kein
Freigabe-Button. Kein Pulse-Ring (auf User-Wunsch entfernt).
docs/concepts/brand-token-matching.md: abgenommenes Konzept für geteiltes
Brand-Token-Matching (Layer 1 DNS + Mail/Mo) gegen den Nummern-Trick der
Gambling-Industrie (slotoro.bet → slotoro88.bet). Im Backlog.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Blocker-UI:
- FilterTile: Trash-Button → status-aware Freigabe-Button (Freigeben/Erneut/
in-Prüfung); RemoveDomainSheet entfernt — kein Domain-Entfernen mehr in der UI
- VIP-Liste landabhängig: zeigt die komponierte Endpoint-Liste statt nur
eigener Customs; Land über Geräte-Region (expo-localization)
- VIP-Realtime: refetch bei Domain-Add/Approve/Reject, pulsierender Ring
für neue/active/submitted Chips
VIP-Komposition (webcontent-domains):
- Hybrid: Customs auf 30 gekappt, 20 Slots fest für die kuratierte Top-Liste
reserviert — Customs können die Top-Gambling-Domains nicht verdrängen
Add-Check (custom-domains POST), für web reaktiviert — 3 Fälle gegen
Layer 1 (global) + Layer 2 (kuratierte VIP):
- weder global noch kuratiert → normaler active-Eintrag
- global + kuratiert → alreadyProtected, kein Slot
- global, nicht kuratiert → inGlobalNotVip; per addToVip als status=approved
speicherbar (kein Slot, nur VIP-Liste)
DE-Gambling-Liste 30→36, nach Relevanz sortiert (erste 20 = reservierte Plätze)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Meine Filter (unified web + mail_domain, Typ-Badge pro Kachel, Slot-Pool-
Zaehler) + collapsible VIP-Liste (Zweitschutz-Beschreibung, read-only). Die
3 redundanten Web-Bloecke (CustomFilterOverview + DomainSection Eigene Domains)
entfernt. Slot-Pool rein frontend (limits.web+limits.mail), kein Backend noetig.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Neue VIP-Sektion in der Blocker-Page: eigene Web-Custom-Domains als Tiles mit
Zaehler (X/10 Pro, X/20 Legend), Hinzufuegen frei. Entfernen geht durch einen
3-Klick-Friction-Flow (RemoveDomainSheet, gespiegelt vom Deactivation-Muster) —
kein Block-Abbau im Craving-Moment. free-Tier-Zweig entfernt (kein Free mehr).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
debug.tsx war hart __DEV__-gated → der Protection-Log-Viewer (v0.3.3)
wäre im TestFlight-Build unerreichbar gewesen.
eas.json production-Profil setzt jetzt EXPO_PUBLIC_ENABLE_DEBUG=1.
debug.tsx + HeaderDropdownMenu prüfen `__DEV__ || EXPO_PUBLIC_ENABLE_DEBUG`.
Für den echten App-Store-Release einfach das Flag aus eas.json nehmen.
Avatare (Dicebear-URLs), Chat-Attachments und Feed-Bilder wurden bei
jedem App-Reload neu vom Netzwerk geladen — RN Image hat nur flüchtigen
Memory-Cache. expo-image (~3.0.11) bringt persistenten Disk-Cache
(cachePolicy 'memory-disk' default).
14 Files migriert: UserAvatar, ChatBubble, RoomCard, ChatInput, PostCard,
ComposeCard, NotificationsDropdown, AppHeader, ProfileHeader,
AddDomainSheet, DomainGrid, room, profile/edit, signup.
API-Mapping: resizeMode→contentFit. PostCard onLoad las e.nativeEvent.
source — expo-image liefert e.source direkt (sonst wäre der Post-Bild-
Aspect-Ratio-Fix still gebrochen).
PostCard: nur Image-Zeilen angefasst, Like/Count/Memo-Logik unberührt
(memory/feedback_minimal_post_changes.md).
Kommt mit v0.3.3 (expo-image ist Native-Modul, braucht neuen Build).
Back-Button:
- OnboardingNavContext liefert der Shell einen optionalen goBack-Handler
(kein prop-drilling durch 8 Slides).
- OnboardingShell: chevron-back links neben der Progress-Bar wenn goBack
gesetzt ist.
- Controller: goToLinearPrevious() + BACK_ALLOWED-Liste. Back nur auf
privacy/nickname/diga_choice/plan/payment — NICHT welcome (erste),
done (final), diga_code (eigener onBack), protection (Backend-Step +
Permission-Flow).
Language-Switcher:
- WelcomeSlide: 4 Sprach-Pills (DE/EN/FR/AR) oben rechts. User kommt
während Onboarding nicht zu Settings — sonst kein Weg die Sprache
zu wechseln. setLanguage persistiert + flippt RTL für AR.
Bug: User mit FR-locale sahen Lyra-Confirmation-Posts trotzdem auf Deutsch
(Banner/Tabs richtig FR). Root: approve.post.ts generierte den Text via
Groq mit hartcodiertem 'auf Deutsch'-Prompt, speicherte als plain content.
Server (approve.post.ts):
- 4 parallele Groq-Calls (Promise.allSettled) — de + en + fr + ar
- Per-Locale-PROMPT_CFG mit subject/action/statsLine/thanksSegment-Texten
- Locale-aware Number-Format (toLocaleString('de-DE'|'en-US'|'fr-FR'|'ar-EG'))
- Content als JSON {de:'...',en:'...',fr:'...',ar:'...'} gespeichert
- Mindestens DE muss gelingen, sonst kein Post (Sicherheit gegen halbe Posts)
- ~4x Groq-cost pro Post (sehr günstig bei Llama-3.3-70b, parallel-latency
bleibt ähnlich)
Frontend (PostCard.tsx):
- resolveLocalizedJsonContent() — try-parsed JSON content
- Wenn JSON-Object mit Locale-Keys → pickt i18n.language, fällt auf DE → EN
- Sonst plain content (Legacy-Posts, Comments, User-Posts unverändert)
- Quick-Reject auf '{' first-char vermeidet JSON.parse-Overhead für 99.9%
der Text-Posts
Legacy-Posts in DB bleiben DE-only (kein retroaktiver Multi-Locale-Rewrite).
Neue Posts ab Deploy haben alle 4 Sprachen.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
## TTS Auto-Play Preference
User-Request: wenn Voice einmal aktiviert, soll Lyra auf jeder Slide
automatisch sprechen — nicht jede Slide extra antippen.
- stores/lyraVoice.ts: zustand-store mit AsyncStorage-Persistence
(@rebreak/lyraVoiceEnabled). Default OFF.
- LyraBubble auto-plays on text-change wenn enabled
- Audio-Button toggled die Preference + stoppt current playback
- Visuell: Button ist orange-filled wenn voice ON, ghost-bordered wenn OFF
- Icon: volume-mute-outline (OFF) / volume-medium / hourglass / stop
- Cleanup beim Unmount (stopLyraSpeech) + bei text-change
Initialisiert via init() in app/_layout.tsx (analog language/theme/appLock).
Locale-keys: audio_play → "Stimme einschalten", neu audio_disable → "Stimme
ausschalten" in 4 Sprachen.
## DiGA Test Codes 011-100
Aktuell 10 Codes (REBREAK-TEST-001..010), aber 100 Android-Tester kommen
morgen onboarding. Migration 20260518_extend_diga_test_codes seeded 90
zusätzliche Codes via generate_series(11, 100) + LPAD-Padding.
- Label: 'test_batch_2026-05-android' für Auditbarkeit (vs '...2026-05'
für die ersten 10)
- grants_plan: 'legend' wie die ersten 10
- ON CONFLICT DO NOTHING — idempotent
Distribution-Pattern: Tester N kriegt Code REBREAK-TEST-<NNN-padded>.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Bug: 3 Stellen hatten `behavior={Platform.OS === 'ios' ? 'padding' : undefined}`.
Auf Android = `undefined` = KeyboardAvoidingView macht NICHTS → Input wird von
Tastatur verdeckt (chat-input, profile-edit-nickname, room-chat).
Fix: switch zu react-native-keyboard-controller's KeyboardAvoidingView mit
behavior='padding' für beide Plattformen. Funktioniert sauber cross-platform
weil KeyboardProvider schon im root-layout sitzt.
Affected Files:
- components/KeyboardAwareScreen.tsx (used by profile-edit + auth-screens)
- app/dm.tsx (DM chat)
- app/room.tsx (room chat)
lyra.tsx war bereits OK (`'height'` für Android — kein Fix nötig).
iOS-Verhalten unverändert.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>