Felder wurden nirgendwo gelesen/angezeigt (nur in raw_user_meta_data
gespeichert ohne Verwendung). Inkonsistent mit OAuth-Flow der sie
gar nicht erfasst. Entfernt:
- 2 Inputs aus signup.tsx
- firstName/lastName aus signUp metadata-Typ + data
- 8 i18n-keys (de/en/fr/ar)
- DB-Cleanup via SQL fĂŒr 5 existing User (raw_user_meta_data - 'first_name' - 'last_name')
Art. 5(1)c DSGVO: nur Daten verarbeiten die fĂŒr Zweck notwendig sind.
Nickname allein reicht â AnonymitĂ€t-Pattern (memory/feedback_anonymity_nickname.md).
Vorher: fill='#0a0a0a' (schwarz) auf bg-neutral-900 (schwarzem Button) â
Logo unsichtbar. Erste TestFlight-Build (v0.3.0) hatte das noch drin â
Fix fĂŒr v0.3.0-rebuild oder v0.3.1 hotfix.
Beide AppleIcon-Komponenten in signin.tsx + signup.tsx lokal dupliziert
(nicht in shared component) â beide separat editiert.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Vorher: stores/auth.ts hatte TODO + fiel auf Supabase-Web-OAuth-Flow zurĂŒck,
was fehlschlug mit 400 'Unsupported provider: missing OAuth client ID' weil
der Supabase-Apple-OAuth-Provider nicht konfiguriert ist.
Jetzt: native Flow ohne Supabase-Provider-Config â
- expo-apple-authentication.signInAsync() â identityToken
- supabase.auth.signInWithIdToken({provider:'apple', token}) verifiziert direkt
gegen Apple's Public-Keys (kein Client-Secret-JWT-Setup nötig)
- User-Cancel (ERR_REQUEST_CANCELED) â leeres Resultat statt Error
- Platform-Guard: Apple-Path nur auf iOS
app.config.ts: ios.usesAppleSignIn=true â Expo prebuild generiert das
com.apple.developer.applesignin-Entitlement in die .entitlements. Beim
ersten EAS-Build wird die Capability auto-registriert im Apple-Developer-
Portal fĂŒr org.rebreak.app.
signin.tsx + signup.tsx: Apple-Button conditional auf Platform.OS==='ios'
gerendert. Android-User sehen nur Google-Button (auf Android gibt es kein
natives Apple Sign-In).
App-Store-Submission-Pflicht (Apple Guideline 4.8 â wer OAuth-Login mit
3rd-Party-Provider anbietet, muss auch Apple Sign-In bieten).
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-context: user reports nach Cooldown-Disable auf v0.2.1 Android-Build
reactiviert sich Schutz auto â a11y-Settings bleibt blockiert â keine Screenshots
möglich. v0.3.0 hat den Backend-protectionDisabledAt-Guard der das verhindert,
aber Test-Devices brauchen ein direktes Reset-Tool fĂŒr Multi-Locale-Screenshots.
Backend:
- POST /api/protection/dev-force-disabled â sets protectionDisabledAt=NOW()
ohne Cooldown-Vorlauf. Production-Guard (rebreak.org-non-staging â 403).
Frontend:
- /debug Android-Section refactored: "Force Reset + Settings öffnen" Button
- Bundle aus 3 Steps:
1. native forceDisable (VPN stop + tamper disarm + filter_enabled=false)
2. backend dev-force-disabled (Anti-Auto-Reactivation-Mark)
3. Settings â Bedienungshilfen öffnen
- Danach: User toggled ReBreak-Service in Android-Settings manuell off
â frischer a11y-deep-link-Trigger fĂŒr nĂ€chste Screenshot-Iteration
Also: fix /onboarding/welcome â /onboarding (Duo-Rewrite hat den alten Pfad
gelöscht). Route 404 auf Android sichtbar wenn User in debug-toggle 'welcome'
oder 'nickname' tappt.
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>
## Protection Pre-Explainer: External Pointer
Vorher: Pulse-Ring absolute-positioniert IM Screenshot â Position musste
per-locale fine-tuned werden weil Apple-Dialog-Höhe variiert (DE/EN/FR/AR
haben unterschiedliche Text-LĂ€ngen â Dialog hat verschiedene Höhen â
Erlauben-Button rutscht).
Jetzt: animierter Pfeil + Label-Pill UNTER dem Screenshot. Dimensions-
agnostic, funktioniert in allen 4 Sprachen ohne Locale-spezifische Magie.
- ScreenshotPointer komplett refactored: caret-up + bouncing pill mit
Button-Label-Text (z.B. 'Tippe "Erlauben"' / 'Tap "Allow"' / etc.)
- onboardingAssets.ts: getPointerPosition deprecated/entfernt
- ProtectionSlide nutzt neue API mit buttonLabelKey
- 4 Locales: dialog_button_allow + dialog_button_continue
- tap_marker_hint refined (kein "roter Marker"-Ref mehr)
## i18n-aware Screenshots
en/fr/ar Permission-Dialog-Screenshots zur Map ergÀnzt. Resolver fÀllt
auf de zurĂŒck wenn andere Sprache fehlt.
## Dynamic Sizing
ProtectionSlide nutzt useWindowDimensions:
height: min(320, max(200, screenH * 0.32))
â passt auf iPhone SE (213px) bis Pro Max (320px capped) ohne Scroll.
OnboardingShell ScrollView-Padding reduziert (16â12 top, 24â16 bottom).
ProtectionSlide-Spacing tightened.
## Blocker: lockedIn Fix
Bug: `lockedIn = appDeletionLockActive` ignorierte URL-Filter-State â
wenn User nur FC aktivierte (ohne URL-Filter), zeigte App grĂŒnen "Schutz
aktiv"-Banner obwohl URL-Filter aus war. Fix:
lockedIn = urlFilter && appDeletionLock
â Beide mĂŒssen wirklich aktiv sein fĂŒr den grĂŒnen Banner.
## LayerSwitchCard: lockedHint Prop
Optional Hint-Text der unter dem active Layer angezeigt wird, z.B.
"System-gesperrt. Nur in iOS-Einstellungen â Bildschirmzeit â Verwaltung
durch ReBreak deaktivierbar.". Wird fĂŒr iOS App-Lock-Card genutzt.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
## Backend: Anti-Auto-Reactivation nach Cooldown
Bug: nach Cooldown-Ablauf wurde der URL-Filter automatisch wieder
reaktiviert (enforceProtection-Loop fÀngt 'recoveringFromBypass'-Phase ab).
Damit war der Cooldown-Schritt entwertet â User konnte nicht wirklich
abschalten, weil die App den Schutz sofort wieder hochfuhr.
Fix: Profile.protectionDisabledAt (DateTime nullable). Wird in
/api/cooldown/status auf cooldown-auto-resolve gesetzt. /api/protection/state
gibt dann protectionShouldBeActive=false zurĂŒck â Frontend macht KEINE
Auto-Reactivation. User muss explizit re-aktivieren (CTA in der App).
- Migration 20260517_protection_disabled_at
- Schema: Profile.protectionDisabledAt
- /api/cooldown/status: setzt das Feld auf expired+resolve
- /api/protection/state: includes profile.protectionDisabledAt in shouldBeActive-Berechnung
- /api/protection/mark-active (POST, NEU): clears das Feld, vom Frontend
auto-aufgerufen nach erfolgreichem activateUrlFilter
Bypass-Recovery durch externe iOS-Settings-Disable (nicht cooldown-bezogen)
funktioniert weiter â protectionDisabledAt ist dann null, alte Logik greift.
## Frontend: ProtectionOffSheet (Custom-Sheet statt Alert.alert)
Bisheriges native Alert mit OK+Reactivate-Buttons hat keine visuelle
Hierarchy (iOS macht beide gleich). Ersetzt mit FormSheet:
- GroĂer blauer Primary "Schutz wieder einschalten"
- Ghost-Link "SpÀter"
- Swipe-down / Backdrop-Tap zum SchlieĂen
## Frontend: ProtectionSlide mit Pre-Explainer (Screenshot + Pulse-Marker)
User-Request: vor dem iOS-Permission-Dialog ein ErklÀrungs-Screen zeigen
damit der User weiĂ wo er tappen muss (Apple's "Don't Allow" ist groĂ+
blau = Trap, "Allow" ist der unscheinbare Button unten).
- components/onboarding/ScreenshotPointer.tsx â Reanimated pulsing red
ring, positionierbar via {xPercent, yPercent}
- lib/onboardingAssets.ts â locale-aware require()-Map fĂŒr Screenshot-
Assets mit de-Fallback
- assets/onboarding/de/ â 4 iOS-Screenshots vom User (url_filter +
screen_time permission dialogs + 2 confirm screens)
- ProtectionSlide refactored: internal phase state preexplain_url â
preexplain_lock â done. Jede Phase zeigt Screenshot + Pulse-Marker auf
korrekten Button + Lyra-Bubble + activate-CTA.
## Locale-Keys
- onboarding.lyra.protection_url.body, onboarding.lyra.protection_lock.body
- onboarding.protection.url_title, .lock_title, .tap_marker_hint
- onboarding.protection.applock_failed_*, applock_skip
- blocker.protection_off_later, reactivate_btn (refined)
## Bugfix: de.json JSON-syntax
Smart-quote-typo: schlieĂendes "" nach âErlauben" und âFortfahren" war
ein plain ASCII " (U+0022) statt U+201D, was den JSON-String frĂŒh
terminiert hat. Metro+Hermes warfen "unrecognized Unicode â".
Fix: escapte \" verwendet â JSON-safe.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Stage 1+2 des post-signup Onboarding-Flows:
- Welcome-Screen: dark-slate Full-Screen mit Pulse-Hero, 3 Mission-Bullets,
DSGVO-Box, CTA "Los geht's"
- Nickname-Spotlight via react-native-copilot ums TextInput in /profile/edit,
auto-start wenn step='nickname', nach Save â step='block' + back to /(app)
- Backend: Profile.onboardingStep enum (welcome/nickname/block/done),
Migration mit Backfill (existing â done), PATCH /api/profile/me/onboarding-step,
/api/auth/me erweitert
- Frontend: CopilotProvider in root, Routing-Gate in (app)/_layout, useMe um
onboardingStep ergÀnzt
- i18n (de/en/fr) fĂŒr onboarding.welcome.* + onboarding.nickname_spotlight.*
Stage 3 (Block-Aktivierung-Spotlight) folgt in nĂ€chster Session â der bestehende
ProtectionOnboardingSheet auf Android wird daran angebunden.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Blocker banner: show real accessibility status on Android (active/inactive)
instead of the iOS Family-Controls "bald verfĂŒgbar" fallback
- AppState listener refreshes state when user returns from system settings
- New ProtectionOnboardingSheet: enforced order VPN â a11y because once a11y
is on it locks VPN settings access. Step 2 disabled until step 1 done.
Skip is allowed; storage flag set only after both steps complete.
- i18n: blocker.layers_a11y_subtitle_active/inactive + protection_onboarding.*
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Android Theme parent â Theme.MaterialComponents.DayNight.NoActionBar.Bridge
(fix BadgeDrawable crash in react-native-bottom-tabs after AccessibilityService toggle)
- Plugin with-material-theme-android keeps theme idempotent across prebuilds
- Plugin with-release-signing-android wires release signingConfig from key.properties
- Splash: align native splash image with JS BrandSplash (icon.png) to eliminate
double-splash flicker on app start
- DM: reset partner/messages/replyTo state on userId change, disable cache for
history query, switch spinner condition to isLoading||isFetching so reopens
always load fresh and never show empty-state with stale partner
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Header: partner avatar left-aligned (was centered)
- ChatBubble: replace bright blue with subtle mint/brand tint, asymmetric
tail-corner radius, footer pinned bottom-right, reply-quote with green
side-bar
- New DmChatBackground: SVG hex-offset doodle pattern (stars, hearts,
clouds, dots) at 7% opacity â light-cream / dark-warm-green base
- Avatar in chat list: use resolveAvatar() consistently to handle
hero-id, https, and null cases
- Realtime subscription: stabilize deps via partnerRef to stop
re-subscribing on partner state change
- Pressable â TouchableOpacity throughout
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
User added info@info.mail-slotoro.com and it landed in Eigene Domains
as type=web instead of in Eigene Mails as type=mail_domain. Bug trace:
1. AddDomainSheet detects kind='mail' from the @ in the user's input
2. mailDomain() strips the local-part â "info.mail-slotoro.com"
3. handleAdd calls onAdd(pattern) â only the stripped string, no kind
4. useCustomDomains.addDomain then sends { pattern } with no kind
5. Backend Variante C auto-detect keys on @ in the pattern â but the
pattern no longer contains @ (frontend already stripped it), so the
detector falls into the kind='web' branch
Fix: pass the kind explicitly from the sheet through the prop chain.
AddDomainSheet.onAdd is now (pattern, kind?) â the sheet's handleAdd
forwards the kind it detected. blocker.tsx's onAdd handler threads
it into addDomain so the body includes { pattern, kind }. Backend
then takes the explicit path and stores type='mail_domain' for the
already-stripped value. Auto-detect on bare pattern (no kind) still
works for any caller that genuinely doesn't know â that path just
isn't used by the sheet anymore.
Match the existing DomainSection visual pattern. One row at the top:
title "Eigene Filter", inline Web/Mail legend dots, the X/Y count pill
and a small + button â all on the same line. The bar drops below at
5px height (same as DomainSection). The 48Ă48 floating add button is
gone in favour of a 28Ă28 inline button next to the count pill so the
overview reads as a single horizontal strip rather than a tall card.
Single shared affordance for adding either a website-domain or a mail-
sender-domain. The per-section add buttons (one inside "Eigene Domains"
and one inside "Eigene Mails") are gone â replaced by a CustomFilter-
Overview card above both sections with:
- title "Eigene Filter" and a "X von 20" counter (free/pro: 10, legend:
20 â sum of the two per-type buckets)
- a 2-colour progress pill: brandOrange for the web slice, success-green
for the mail slice on top of the surface-elevated rest
- a 48Ă48 rounded-full TouchableOpacity on the right (brandOrange,
ionicons add 24px, white) that opens the AddDomainSheet directly
AddDomainSheet was rewritten one more time: the Seite / E-Mail type
picker is gone. The user types one thing â domain or full address â
and a live preview shows which one we detected (Domain-Filter for a
bare host, Mail-Filter for input that contains "@", stripping to the
domain after the last @). The shape is also what we send: the body is
{ pattern } with no kind field. The backend (commit a2680f6) does the
authoritative auto-detect and sends back the resolved type with the
created row, so the frontend never has to guess in two places.
useCustomDomains.addDomain now treats kind as optional. When omitted,
the request body just carries pattern â when present it's still sent
through verbatim so any caller that wants to force a category still can.
DomainSection no longer renders a per-section add button when its onAdd
prop is undefined â domains and mails sections in blocker.tsx both
omit onAdd now. The mails section stays default-collapsed.
i18n: new keys custom_filter_overview_title / count + preview_web /
preview_mail / preview_invalid; tabs_web / tabs_mail removed since the
TypePicker is gone. type_web / type_mail kept in the locales as
inactive entries in case the type-picker comes back in a future
direct-add flow.
User found that adding bet365.com (which is in the 208k global filter)
silently took a custom-domain slot â they paid a slot for something
the global blocklist already covered. Two pieces:
1. backend/custom-domains/index.post.ts: before any slot-limit check or
DB insert, look the domain up in blocklist_domain (active rows). If
present, return 200 { alreadyGlobal: true, domain }. No row gets
written, no slot consumed. The existing frontend hook + AddSheet
already handle the alreadyGlobal flag â they surface the
"bereits global blockiert" alert and don't refresh as if the entry
landed in the user's list.
2. blocker.tsx default mailOpen state flipped from true to false so the
Eigene Mails section starts collapsed on page load. Domains stays
the primary affordance; mail-patterns are an opt-in expansion.
DomainSection bekommt collapsible-Prop (default false).
Domains-Section: kein Chevron, kein useState, Content immer sichtbar.
Mails-Section: collapsible={true} + open/onToggle wie bisher.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The Seiten/Mails top-tabs added in 5c6fa3d are gone. Per the user's
revised vision, web-domains and mail-patterns live side by side as two
collapsible <DomainSection>s with their own header, slot pill, progress
bar, and add-button â closer to the original Eigene-Domains affordance
plus a sibling Eigene-Mails section. Both default open; chevron-up/down
per the existing icon convention.
AddDomainSheet was rewritten from scratch to fix the layout-bug
visible in the screenshot â SheetFieldStack's two-ScrollView intro/
fields split was wrong for a single-input use case and was rendering
the chip at the bottom of the scroll area with a huge gap under the
TypePicker. The new sheet is a plain ScrollView with TypePicker, label,
TextInput, help-card, preview-card, warning-card, confirm-row, and the
Cancel + HinzufĂŒgen buttons stacked top-to-bottom with `gap: 12`. No
Pressable anywhere â TouchableOpacity only, per the hard rule.
DomainGrid is now a pure tile renderer: the header / slot pill / add
affordance live on the section component above it. Its `kind` prop
(renamed from `activeTab`) drives the type filter â for v1.0, mail
means strictly `mail_domain` (display-name is gone).
i18n: new keys section_domains / section_mails / add_sheet_cta. mail-
related copy (label, placeholder, help, empty) had every "Display-Name"
mention stripped so the user can't read about an option that doesn't
ship.
Progressbar inline in DomainSection with the same Animated.timing
pattern DeviceProgressBar uses, with a 3-step color threshold
(green / brandOrange / error) keyed on the bucket fill ratio.
Top-tabs above the custom-domains grid: Seiten (web) and Mails (mail_*).
2px underline highlight in colors.brandOrange for the active tab, the
muted label otherwise â matches the community/feed tab style we already
use. Pill segmented control would have needed extra inset math for two
tabs without adding clarity.
- DomainGrid filters items by the active tab. Tab-specific empty-state
copy and icon (mail-outline for the Mails tab) so the empty Mails tab
doesn't read like a broken Web view.
- mail_display_name tiles hide the submit-to-global button entirely â
matches the v1.0 backend lock; the user can't accidentally tap into a
400 from the API.
- useCustomDomains exposes countsByType + limits. Provisional client-
side estimation until the new API response shape (extended in the
parallel backend commit f2b81ee) is wired through â same TS shape,
so dropping the estimation is a one-line swap when ready.
- AddDomainSheet picks up initialType so tapping "+" while the Mails tab
is active opens the sheet pre-selected to E-Mail. Plan-limit error
handling maps WEB_LIMIT_REACHED / MAIL_LIMIT_REACHED to the right
per-bucket message.
i18n: tabs_web / tabs_mail / count_label / error_web_limit_reached /
error_mail_limit_reached / empty_web / empty_mail across DE/EN/FR with
%{var} placeholders.
AddDomainSheet now opens with a Seite / E-Mail segmented control.
Web keeps the existing flow (label, placeholder, favicon preview,
domain normalization). Mail switches to a free-form pattern input
(address / domain / display-name â user types what they see in
their inbox) with a mail-icon preview after the field is filled.
addDomain(pattern, kind) now sends { pattern, kind: 'web' | 'mail' }
and the server decides the concrete type. Type field flows through
the CustomDomain type so DomainGrid tiles render the mail-outline
icon for mail entries instead of the favicon fallback.
i18n: blocker.type_web / type_mail / add_web_* / add_mail_* across
de/en/fr with %{var} placeholders per repo convention.
Adds a tabBarBadge on the bottom Chat tab driven by the same
dm-conversations query the chat screen already uses â React Query
dedupes the call. Badge shows the unread total (capped to "99+")
and disappears when 0. Query is gated on session so unauthenticated
launches don't fire it.
- DmItem now goes through resolveAvatar(partnerAvatar, partnerName) so
the Dicebear-initials fallback kicks in for null avatars, hero ids
resolve to their image url, and direct URLs pass through. Adds the
PostCard-style avatarLoadFailed state for graceful broken-image
fallback.
- Search row pill-shaped (borderRadius 999) with 16px horizontal padding
and the outline search icon for better visual rhythm.
Consistent with chat.tsx refactor â ActivityIndicator, joinBtn, and
avatarEdit badge all now use the theme token.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Removes 2-tab Groups/DMs layout; Chat screen is now DM-only for v1.0.
Groups tab state, rooms query, RoomCard/CreateRoomSheet imports removed.
Replaces static title+create-button header with sticky search field
(client-side filter on partnerName + lastMessage). No create-DM button
added â /dm-new route does not exist yet (follow-up task).
All #007AFF in chat.tsx replaced with colors.brandOrange.
Adds chat.search_placeholder to de/en/fr locales.
Tab-bar styles kept in makeStyles (dead code, v1.1 Groups comeback path).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
MobileDeviceRow now handles three binding states driven by
boundToPlan / releaseRequestedAt from the UserDevice type:
- Bound, no release pending: blue "Gebunden" badge next to device name;
trash icon replaced by lock-open icon â Alert â requestRelease()
- Release active (countdown running): footer shows "Freigabe in Xh Ymin"
in amber; close-circle icon â Alert â cancelRelease()
- Current device (isCurrent): existing behaviour unchanged, no action
button regardless of binding state
releaseAt is computed client-side as releaseRequestedAt + 24h â avoids
a backend round-trip for the countdown display.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
After Supabase auth succeeds the store calls POST /api/devices/check-lock
(x-device-id auto-attached via apiFetch). A 409 DEVICE_LOCKED response
triggers a Supabase sign-out and returns { deviceLocked } instead of
proceeding. The signin screen swaps to DeviceLockedPanel which shows:
- lock icon + headline + explanatory body
- amber countdown badge if a release is already in progress
- grey hint pointing to the email notification
- primary CTA to go back and sign in with the original account
Backend TODO: POST /api/devices/check-lock endpoint â same device-lock
query as login.post.ts but callable with a valid Supabase session token
(for email-login flow that bypasses /api/auth/login).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
ImagePicker.launchImageLibraryAsync now opens with `allowsEditing: true`
and `aspect: [1, 1]`, which triggers Apple's built-in square crop UI
(pan + zoom on the user's selection). The output URI is the actually
cropped image â fixing the long-standing bug where AvatarCropSheet
displayed a visual transform but `manipulateAsync` only resized the
original, so any pan/zoom the user did was discarded on confirm.
Removes the entire AvatarCropSheet component (~285 lines) and its sole
consumer wiring in profile/edit.tsx. The avatar continues to render as
a circle everywhere via borderRadius â the underlying square output is
just storage-agnostic.
Native-look-first per memory rule, zero new dependencies, no new
native module to link.
Replaces ad-hoc TouchableOpacity+styled-Text pairs with a single
`<Button>` covering the four variants we actually use (primary,
secondary, ghost, destructive), with size (sm/md/lg), loading,
disabled, icon, iconPosition, and a style escape hatch.
Migrated files: AddMacSheet, AddWindowsSheet, PlanChangeSheet,
devices.tsx CTA, settings SubscriptionSheet CTA.
Skipped (kept as-is to avoid hostile overrides): auth flow buttons
(Google/Apple OAuth with custom SVGs), list-row Touchables, blocker
& mail components (separate sweep when those screens come up).
paddingVertical default 12 (md) â matches the slimmer-buttons direction
we landed on in the devices-page redesign.
- MobileDeviceRow: collapse to 2 lines (name+badge / lastSeen · seit date)
- ProtectedDeviceRow: collapse to 2 lines (name+badge / seit date or degraded hint)
- Both rows now use alignItems:center for visual parity
- Replace dual Mac/Windows buttons with single UIMenu "+ neues GerĂ€t hinzufĂŒgen"
- MenuView disabled (no-op TouchableOpacity) when at device limit
- Dynamic counter below subtitle: "X von 3 GerÀten · noch Y frei" / "Maximum erreicht"
- paddingVertical 16â12 on all primary CTAs in devices.tsx, AddMacSheet, AddWindowsSheet
- New i18n keys: devices.add_device, devices.counter_some, devices.counter_limit (DE/EN/FR)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces the manual "I've installed it" button in AddMacSheet with an
auto-advancing waiting-pill. As soon as the backend flips status from
pending â active (triggered by the DoH handshake from the AdGuard
watcher), the sheet jumps to the success step automatically.
- useProtectedDevicesRealtime hook subscribes to rebreak.protected_devices
UPDATE events for the current user, with auto-reconnect on CHANNEL_ERROR
- AddMacSheet listens only while in step 2 (download/install)
- devices.tsx keeps a list-level subscription so the table refreshes even
if the user dismissed the sheet before activation
- i18n: waiting_install / waiting_hint / activated_toast (DE + EN)
Bug (diagnosed by backyard, see project_session_2026-05-15_push.md):
- Manual `supabase.realtime.setAuth()` calls in subscribe-hooks set
`_manuallySetToken=true` internally, blocking the automatic token-refresh
on heartbeat. After ~1h the cached access_token expires â Postgres-Changes
silently stop arriving (channel still shows "joined" but no events).
- Plus: no AppState handler â no Foreground-Reconnect trigger after
Background-kill of WebSocket.
Fix A â lib/supabase.ts: createClient now passes a `realtime.accessToken`
async callback that returns the current session token. Heartbeat picks
fresh tokens automatically, no manual setAuth needed.
Fix A â all 5 manual `supabase.realtime.setAuth()` calls removed from
useChatRealtime, useCommunityRealtime, useDomainSubmissionRealtime,
stores/notifications. Token is handled by the callback now.
Fix B â _layout.tsx: AppState listener calls
supabase.auth.startAutoRefresh()/stopAutoRefresh() â official Supabase RN
pattern. On Foreground-Return, onAuthStateChange fires TOKEN_REFRESHED â
realtime.setAuth gets called internally.
Required for upcoming Auto-Detect protected-device handshake (Realtime
channel listens on protected_devices status transitions pendingâactive).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- mail/MailAccountSettingsSheet: handleSaveTitle + handleSavePassword now
dismiss sheet FIRST, then trigger parent SuccessAlert via setTimeout(350ms).
Fixes iOS "already presenting" crash + page-freeze when editing mailbox name.
Also fixes double-click-needed UX bug.
- stores/auth: signOut adds WebBrowser.coolDownAsync() to clear OAuth cookies.
signInWithOAuth for Google adds prompt=select_account â forces account-picker
on every sign-in attempt instead of auto-reusing previous account.
- app/(app)/index: feed page uses colors.groupedBg instead of colors.bg â
matches iOS Mail/Messages list-style, post-cards stand out clearer.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- stores/realtimeDebug.ts: neuer DEV-only Zustand-Store mit connection-state,
reconnect-counter, token-expiry-countdown, channel-liste, rolling log-buffer
(last 100 events). Hookt Phoenix-Socket open/close/reconnect + Channel-subscribe.
- _layout.tsx: initRealtimeDebug() im __DEV__-Block beim App-Start.
- debug.tsx: zwei neue Cards (RealtimeStatusCard + RealtimeLogCard) mit
1s-Tick-Refresh, Copy + Clear Buttons. Settings-Entry 'Realtime connection (DEV)'.
- protectedDevices.ts: Array.isArray-Guard fĂŒr apiFetch-Response â verhindert
TypeError 'devices.filter is not a function' wenn Backend Non-Array zurĂŒckgibt.
Diagnostik-Tool fĂŒr Realtime-Disconnect-Bug bei lange eingeloggten Usern.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Two small fixes blocking real "feierabend":
1. Stats-Counter veraltet nach Scan/Connect/Disconnect:
- mail.tsx hatte zwei separate Data-Sources: useMailStatus (accounts +
errors + heartbeat) und useMailStats (blockedByDay + blockedByConnection)
- onScanSuccess + onIntervalChanged + OAuth-onSuccess + disconnect-handler
refreshten nur useMailStatus â der Account-Collapsible-Counter (kommt
aus useMailStats.blockedByConnection) blieb veraltet
- Beobachtet: GMX-Scan-Button meldet "90 blockiert" als Feedback, aber
Card-Header zeigt weiter 60
- Fix: refreshAll() = refresh() + refreshStats() parallel. Alle reactive
callsites (4 Stellen) auf refreshAll umgestellt
- useMailStats hatte refresh schon exportiert (Z. 153), nur nicht
verdrahtet
2. Donut + Legend horizontal zentriert:
- vorher: alignItems center (vertikal), Legend flex:1 â linksbĂŒndig mit
Legend bis Card-Rand gestreckt
- jetzt: justifyContent center + Legend ohne flex:1 â Block in der Mitte
mit Whitespace links/rechts
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
USP-Confirmed: Outlook-OAuth Casino-Bonus-Mail wurde end-to-end gefiltert
(User-verifiziert). Mit dieser Welle ist der Daemon plus alle Scan-Pfade
OAuth-aware.
Backend â Mail-Stack (mo):
- backend/server/utils/mail-auth.ts NEU: zentraler resolveImapAuth-Helper
kapselt OAuth-vs-AppPassword-Entscheidung. 5-min-Token-Expiry-Puffer,
race-condition-sicheres Refresh via refreshAndSaveTokens.
- scan.post.ts + scan-internal.post.ts nutzen jetzt resolveImapAuth statt
decrypt(passwordEncrypted). Vorher: Outlook-Connections wurden still
ĂŒbersprungen weil passwordEncrypted='' â decrypt failed. Cron + manueller
Scan-Button funktionieren jetzt fĂŒr OAuth-Connections.
- imap-idle: Initial-Sweep via triggerScan(conn) direkt nach Connect-Success.
Neue Outlook-Connections kriegen sofort einen Full-Folder-Scan statt bis
zu 30 Min Cron-Lag zu warten. scan-internal scannt ohnehin schon alle
Folders via imap.list() (Junk, Spam, Archive, Custom) â Multi-Folder-
Anforderung ist damit erfĂŒllt.
Frontend â Mail-Page Polish v4 (rebreak-native-ui):
- MailDistributionChart: Donut zurĂŒck auf 200px (240 wuchs auch in der
Breite und quetschte die Legend), "Live"-Pill-Header komplett raus
(paddingTop von 16 auf 13 reduziert fĂŒr tighteres Layout)
- mail.tsx Page-Hierarchie: "Mehr Infos"-Collapsible wandert von unter
der Postfach-Liste direkt unter den Hero-Donut. Sub-Beschreibung
"Blockiert â letzte 30 Tage" entfernt â Title reicht.
- Account-Card Expanded: adaptive Bar-Chart ĂŒber Connection-Age
(too-new <24h zeigt Empty-State, 1-14d Day-Buckets via Backend
?connectionId=, 15-90d client-Week-Aggregation, >90d Month)
- Account-Card Expanded: Scan-Button "Jetzt scannen" mit Refresh-Icon
(Memory: kein Pen-Icon, refresh ok). Spinner wÀhrend Scan, Feedback
mit Blocked-Count nach Success.
Eskalations-Hinweis (nicht in dieser Welle):
- POST /api/mail/scan akzeptiert noch keinen connectionId-Filter â
Scan-Button-Tap scannt aktuell alle Connections statt nur die
angeklickte. Kleiner Folge-Patch, nicht blocking.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User-Feedback nach Live-Test:
Frontend (mail page):
- HalfDonut als shared component in components/common/HalfDonut.tsx
extrahiert (vorher local in ProtectionDetailsSheet). Mail-Page nutzt
jetzt dieselbe SVG-Math, Animation und Stroke-Style wie der
Blocker-Schutz-Details-Sheet â visuelle Konsistenz auf einen Blick.
Mail-Donut: width=168 (kompakter als die 220 in Blocker, weil Legend
rechts daneben sitzt).
- Donut zeigt Total in der Mitte mit kompaktem Format:
< 1000 â "999", >=1000 â "1.2k+" / "12k+" / "27k+"
Headline-Zahl oben links entfĂ€llt â Total ist im Donut-Center.
- "Mehr Infos" + "KĂŒrzlich blockiert" zu EINER Top-Level-Collapsible
zusammengefasst. Beim Aufklappen: Bar-Chart direkt sichtbar, nested
Collapsible "KĂŒrzlich blockiert" darunter (default zu).
- Account-Card Expanded: per-Connection-Bar-Chart mit adaptive
GranularitÀt nach Connection-Age:
· <24h â Empty-State "Daten werden gesammelt, Auswertung nach 24h"
· 1-14d â Day-Buckets (echte Daten via /api/mail/stats/blocked-by-day
?connectionId=)
· 15-90d â Week-Buckets (client-aggregiert)
· >90d â Month-Buckets (client-aggregiert)
- Settings-Sheet komplett refactored: State-Machine `mode: 'list' |
'edit-title' | 'edit-email' | 'edit-password'` mit Back-Pfeil. Inline-
Edit im selben Sheet statt Sub-Sheet öffnen (FormSheet-Pattern).
Email-Edit-Row vorbereitet (Backend-PATCH-Endpoint kommt separat).
- Pen-Icons app-weit entfernt: SheetFieldStack-Row, alle Settings-Rows
auf chevron-forward (Memory-Konvention).
Frontend (MailAccountCard status fix):
- resolveStatusDot nutzt jetzt heartbeat-as-fallback. Vorher: "waiting"
wenn lastScannedAt=null, egal ob Daemon lÀngst connected war. Jetzt:
"waiting" nur wenn weder lebendiger Heartbeat noch vergangener Scan
existiert â frisch verbundene Connections (z.B. OAuth-Outlook 5s nach
Connect) zeigen direkt "live".
- Behebt User-Beobachtung: "wartet auf erste verbindung" bei Outlook
obwohl Daemon-Log "connected, auth=xoauth2" zeigt.
Backend (imap-idle daemon):
- getMailboxLock("INBOX") jetzt mit 30s Promise.race-Timeout gewrappt.
- Outlook/XOAUTH2 hat den Edge-Case, dass der Mailbox-Lock lautlos
hĂ€ngt nach erfolgreichem connect â die Session bleibt offen ohne
Fortschritt bis der Renew-Timer (10min) ein imap.close() schickt.
Mit Timeout wird das Failure-Mode explizit â Auth-Retry-Loop greift
sauber + last_connect_error mit klarem Text (statt stiller HĂ€nger).
- Root-Cause "warum hĂ€ngt es" noch nicht behoben â Diagnose nach
Deploy in Logs (mo).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User-Feedback nach Live-Test:
Frontend:
- FAB raus, Plus-Button zurĂŒck in den Account-Liste-Section-Header
(`add-circle-outline` in brandOrange + Label "Postfach hinzufĂŒgen").
FAB stört am unteren Rand, oben passt zum iOS-NavBar-Pattern.
- Half-Donut Legend strikt max Top-3 + "Sonstige" â Threshold von â€4
auf â€3 gesenkt. Auch bei 4 Connections wird jetzt schon komprimiert.
- Hero-Donut-Subtitle "ĂŒber N PostfĂ€cher" entfernt â Title-Block ist
jetzt eine Zeile: "XX blockiert · â Live"
- Activity-Log default-collapsed war schon richtig (kein Change)
- Activity-Item-Redesign: x-Icon-Pille raus, Zeit + Provider als
Sub-Zeile unter dem Subject ("vor 2h · GMX"), kein Zeit-Label rechts mehr
Bug-Fix â NaNd in Activity-Row:
- Root-Cause: snake_case/camelCase-Mismatch. Backend liefert
`receivedAt`, `senderEmail`, `senderName`, `connectionId` (camelCase),
Frontend-Type hatte snake_case â undefined-Werte â `new Date(undefined)`
â NaN â "NaNd"-Render
- MailBlockedItem-Type auf camelCase umgestellt + nested `connection`-Objekt
(passt jetzt zum Backend-Response)
- formatDate mit Number.isFinite-Guard â gibt null zurĂŒck bei ungĂŒltigem
Datum statt NaN-String zu rendern
Backend (imap-idle daemon):
- Daemon schreibt jetzt unmittelbar nach `client.connect()` einen Heartbeat
(last_idle_heartbeat_at = NOW()) + clear last_connect_error parallel
- Vorher: User sah 2-9min lang "wartet auf erste verbindung" obwohl
Connection lÀngst aktiv war (Heartbeat kam erst beim ersten NOOP-Cycle)
- Re-Connect-Pfad nach AUTHENTICATIONFAILED ist automatisch mit
abgedeckt (geht durch denselben connect-Block)
- ESM-Daemon, kein Build-Step â Pipeline scp + pm2-restart reicht
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
UX-Welle nach User-Feedback aus dem ersten Live-Test der Mail-Page:
Page-Hierarchie neu (top â bottom):
1. HALF-DONUT als HERO-Karte â bisherige "BLOCKIERT XX ĂŒber N PostfĂ€cher Live"-
Banner-Card weg, Inhalt ist jetzt Title-Zeile innerhalb der Donut-Karte
(rendert nur ab â„2 Connections; Fallback-Stats-Row fĂŒr 0-1 Connections)
2. Postfach-Liste (Account-Cards aus letztem Refactor â schlanker Header)
3. NEU: "Mehr Infos"-Collapsible â Bar-Chart "Blockiert letzte 30 Tage"
liegt jetzt versteckt drin (default collapsed)
4. Activity-Log "KĂŒrzlich blockiert" (unverĂ€ndert)
5. NEU: FAB unten rechts â 56pt brandOrange Kreis mit "+"-Icon,
öffnet ConnectMailSheet. Section-Header-Plus-Button entfÀllt.
Half-Donut Legend-Truncation:
- â€3 Connections â alle anzeigen
- =4 Connections â alle anzeigen
- â„5 Connections â Top-3 by blocked-count + "Sonstige"-Bucket
· Donut: 4 Segmente (Top-3 + OTHER_COLOR grau)
· Legend: 4 Zeilen (Top-3 fett, "weitere"-Zeile in regular grau)
Backend: GET /api/mail/stats/blocked-by-day?connectionId=<uuid> als
optionaler Filter (fĂŒr per-Connection-Bar-Chart in expanded Account-Card,
in dieser Welle noch nicht im UI verdrahtet â Erweiterung kommt wenn
gewĂŒnscht).
FAB-Details (iOS-diskreter Shadow statt Material-Glow):
- position absolute, right 24, bottom = tabBarHeight + insets.bottom + 16
- 56pt, borderRadius 28, brandOrange BG, weiĂes Plus-Icon
- ScrollView paddingBottom angehoben damit kein Content unter dem FAB clipped
Edge-Cases:
- 0 Accounts â FAB sichtbar, Donut/Stats/Charts/Log versteckt + EmptyState
- 1 Account â Donut hidden (nur mit â„2 Connections sinnvoll), Fallback-Stats-Row
- limitReached + FAB-Tap â bestehender Plan-Alert (FAB ist visuell nicht disabled)
Memory: Pull-to-refresh + bestehendes 30s-Status-Polling reichen fĂŒr "wartet
auf erste verbindung"â"aktiv"-Ăbergang nach OAuth-Connect (Daemon-Heartbeat
braucht initial 2-9min, mo-Befund). UX-Polish-Option fĂŒr spĂ€ter: in der
Initial-Phase einen freundlicheren "Verbinde geradeâŠ"-Status anzeigen.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>