- 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.
- 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>
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.
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.
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>
TN-User fielen bisher mangels TN-Liste auf die DE-Liste zurück. Jetzt
eigene (kurze) TN-Starter-Liste: mbet216.com, 2xbet365.com, cesar365.com,
icombet.com, unibet365.net (von einem TN-Test-User gemeldet).
TN in COUNTRY_KEYS (webcontent-Endpoint) + VIP_COUNTRIES (Geräte-Region-
Auflösung + Add-Check). Native Region-Logik ist generisch (Locale.region
→ JSON-Key) — kein Native-Code nötig. gambling-domains.json _meta v3.
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>
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>
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>
syncWebContentDomains war als Side-Effect an syncBlocklist gehaengt, das nur
bei aktivem URL-Filter laeuft. Layer 2 haengt aber an Family Controls — der
Sync lief nie wenn nur App-Lock/FC aktiv war. Jetzt eigene syncWebContent-
Funktion, ungated: Mount + App-Foreground + nach Domain-Add/-Remove.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
syncWebContentDomains (gespiegelt von syncBlocklist): holt die Domain-Liste vom
Backend, cached sie als webcontent-domains.json im App-Group-Container, ETag/304,
Reapply nach Sync wenn FC aktiv. loadWebContentDomains liest cache-first, faellt
auf die gebuendelte gambling-domains.json zurueck (Offline-Seed). Getriggert am
selben Punkt wie syncBlocklist (useBlocklistSync).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Web-/Mail-Limit getrennt gegen apiCounts/apiLimits geprueft (Single Source of
Truth); Legacy-Response ohne counts/limits faellt auf Backend-Rejection zurueck.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
WebKit-interner Content-Filter via ManagedSettingsStore().webContent als
stilles Sicherheitsnetz. Blockt eine kuratierte, laenderabhaengige Top-
Gambling-Domain-Liste plus systemseitig Adult-Content (.auto-Variante).
Braucht NUR Family Controls — kein MDM, kein neues Entitlement, keine
Config-Plugin-Aenderung.
- gambling-domains.json: gebuendelte Starter-Liste (DE/GB/FR), je <=50
Domains (Apple-Hartlimit), klar als STARTER markiert. Via Podspec-
resource_bundles ins App-Bundle gepackt.
- applyWebContentFilter / clearWebContentFilter: zwei native AsyncFunctions.
Land via Locale.current.region, iOS 16+ gegated, FC-Auth vorausgesetzt.
- JS-Bridge (Module-Decl, types, web-stub, lib/protection.ts) + Actions im
useProtectionState-Hook. getDeviceState liefert webContentFilter-Layer mit.
KEINE Auto-Trigger-Logik — Layer 2 ist vorerst nur explizit aufrufbare
Capability. Siehe TODO(layer2-gating) im Swift-Modul und lib/protection.ts.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Modal zeigte auf iOS "Du kannst den ReBreak-Bedienungshilfe-Dienst jetzt
in den Einstellungen ausschalten" — Bedienungshilfe/Accessibility-Service
ist ein Android-Konzept, existiert auf iOS nicht.
iOS: NEFilter + Family Controls werden von forceDisable() vollständig
abgeschaltet, User muss nichts in Settings tun. Neue iOS-Variante zeigt
nur "Cooldown abgelaufen — Schutz deaktiviert." + OK, kein Settings-Button.
Android: unverändert (a11y-Service braucht Settings-Deeplink).
i18n DE/EN/FR/AR: cooldown_elapsed_message_ios neu.
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>
User saw entries like "vor 61d · Outlook" under the "Kürzlich
blockiert · In den letzten 24h" header. createdAt (when the daemon
wrote the mail_blocked row) is always inside the 24h retention window
because deleteOldMailBlocked sweeps everything older than that on
every fetch — but the row preserves the original receivedAt header
from the email, which for old Casino mails the daemon only just got
around to scanning can be weeks or months ago.
Switched the time-label in MailActivityLog to format createdAt
instead. The MailBlockedItem type now carries createdAt explicitly
(the backend has been returning it all along, the FE type just hadn't
acknowledged it). receivedAt stays in the shape for any future
"received vs blocked" comparison view but isn't used in the recent-
activity list anymore.
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.
The /api/custom-domains endpoint changed shape with the slot-pool split
in commit f2b81ee — it now returns { items, counts, limits } where it
used to return a bare CustomDomain[]. The hook was still matching
Array.isArray(res) or res.domains and silently fell back to an empty
list, so a successful POST went unreflected on the blocker page (user
reported "kein fehler aber domain taucht nicht in der liste" after
adding communications@only4subscribers.com).
Now reads items / counts / limits when present, prefers the API-driven
counts and limits over the client-side derivation (still kept as a
fallback for the stale-bundle window between deploys). Legacy bare-
array + { domains } shapes still resolve too in case a cached client
hits this code path before the new backend lands.
The user looked at the legend dashboard and saw 4/8 + 0/2 for the web
and mail buckets instead of the agreed 10/10. The hook's client-side
provisional limits were holdovers from an earlier sketch — 8/2 for
legend, 4/1 for free/pro — that never matched plan-features.ts on the
backend (5/5 and 10/10 respectively). Brought them in line so the
header counters read correctly until the API-driven values land via
the new GET /api/custom-domains response shape.
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.
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>
1. Legend-Wrapper: feste 180px-Width raus, stattdessen flex:1 + minWidth:0.
Mit Donut 200px + gap 20 + Card-paddingHorizontal 16+16 wäre 200+20+180+32=432
zu breit — kleine iPhones haben effektive Card-Width <380px. Legend ragte
raus. Jetzt: Legend nimmt verfügbaren Rest-Platz, Texte trunken bei Bedarf.
2. useMailConnectionStats: zoom IMMER wenn nonEmpty.length > 0, nicht nur
bei sparse-data-Bedingung. Bei 30-Tage-Range mit 1 Hit wurde das vorher
trotzdem als 30 leere Bars + 1 Bar gerendert (Logik nonEmpty*3<raw greift
zwar mathematisch, aber nicht aggressiv genug für wirklichen Visual-Fix).
Jetzt: trim ALWAYS auf [firstHit..lastHit] — bei 1 Hit = 1 Bar, bei 5 Hits
über 10 Tage = 10 Bars (5 mit Daten, 5 dazwischen). Konsistent visuell.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1. Donut-Legend-Rows als space-between: Name links + dot, Count rechts.
Vorher: alle Elemente eng aneinander (gap:6), Count direkt nach Name.
Jetzt: feste Legend-Width 180px, jede Row hat Name+Dot links (flex:1)
und Count rechts mit Whitespace dazwischen.
2. Per-Connection-Bar-Chart in Account-Card: sparse-data-zoom.
Vorher: bei nonEmpty.length > 0 && days <= 7 wurde gezoomt — bei 30-Tage-
Range mit nur 1-2 Hits passierte das aber NICHT → 30 leere Bars + 1 Bar
ganz rechts (Screenshot bei GMX-expanded).
Jetzt: zoom IMMER wenn nonEmpty.length * 3 < raw.length (= mehr als
2/3 der Range sind leer). Trim auf die echte Hit-Range. User sieht
damit nur die Tage mit Daten + die paar dazwischen, statt 30 leere
Slots.
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>
Mail-Page-Refactor — Privacy-friendly + DiGA-tauglich:
- Custom title pro mail-connection (z.B. "Privat-Gmail" statt voller E-Mail).
Memory-Pattern: Anonymität via Nickname jetzt auch für Mail-Adressen
sichtbar, Datenminimierung. Title nullable, Fallback auf Email-Domain.
- Schema-Migration mail_connection_title (additiv, NULL default für Bestand)
- Endpoint PATCH /api/mail-connections/:id mit title-Validation (max 60,
trim, leerer String → NULL)
- "Passwort ändern"-Collapsible → vollwertige "Einstellungen"-Sektion:
Title editieren · Email read-only · Passwort neu setzen · Verbindung
trennen (mit Confirm-Dialog)
- EditMailTitleSheet als FormSheet-Pattern für Title-Edit
- mailConnectDraft-Store kriegt Title-Feld für Pre-Fill bei Re-Open
Zwei neue Stats-Charts auf der Mail-Page:
- MailBlockedByDayChart — 30-Tage-Bar-Chart, Plain-View-Bars (Pattern wie
Sparkline-Profile), Empty-State bei 0 Cooldowns
· Backend: GET /api/mail/stats/blocked-by-day?days=30
- MailDistributionChart — Half-Donut via react-native-svg, Top-5 Connections
+ "Sonstige", rendert nicht bei ≤1 Connection
· Backend: GET /api/mail/stats/blocked-by-connection
Activity-Log mit Provider-Filter:
- Filter-Chips Mo Gmail/Outlook/iCloud/etc. über bestehendem Activity-Log
- GET /api/mail/results?provider=X (war vorher hardcoded all)
- Endpoint-Naming-Fix in useMailResults (war /api/mail/blocked, jetzt
korrekt /api/mail/results — UI-Agent hatte falschen Path geraten)
Backend-Side-Effects:
- imap-providers util resolveProviderMeta(host) — gibt {provider, label,
isCustomDomain} zurück, von 3 Endpoints konsumiert
- /api/mail/status erweitert: title, provider, providerLabel,
isCustomDomain im Account-Shape
- /api/mail/results erweitert: connection-Sub-Objekt pro Entry +
provider-Filter-Query
Open follow-ups (TODOs):
- deleteOldMailBlocked-Cron löscht <24h → Bar-Chart-Daten weg. Retention
auf 90 Tage hochsetzen oder Cron stoppen.
- POST /api/mail/connect könnte die neue connection.id im Response
mitliefern → Title-PATCH direkt ohne Extra-GET (UI-Agent-Empfehlung).
- /api/mail/status zeigt nur active Connections — paused mit Title wären
unsichtbar. Entscheiden.
18 neue i18n-Keys (mail.title_*, mail.settings_*, mail.row_*,
mail.disconnect_confirm_*, mail.stats.*, mail.filter.all) in DE + EN.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
DSGVO Art. 9 — Compliance-Gap im Mail-Connect-Flow geschlossen (Hans-Müller-DSB
hat den Gap für Gmail/iCloud/GMX identifiziert, schon vor Outlook-OAuth-Pflicht):
- Schema: mail_connections.consent_at + consent_version + consent_ip_address;
neue consent_logs-Tabelle für Audit (grant + revoke append-only)
- Endpoints:
- POST /api/mail-connections/consent (Bulk-Array für Re-Consent, partial-fail
wirft sofort = DSGVO-sicher gegen silent-skip fremder IDs)
- POST /api/mail-connections/:id mit consent-gate (412 wenn consentVersion fehlt)
- DELETE /api/mail-connections/:id mit Widerruf-Log (OAuth-Token-Revoke als
TODO für mo Phase 2)
- GET /api/mail-connections/pending-consent — listet Bestands-Connections
mit consent_at=NULL für Re-Consent-Modal
- Account-Lösch-Bug fix: deleteAllMailConnections() war in user/delete nicht
eingebunden — Verbindungen blieben als Waisen
- Frontend:
- ConnectMailSheet: neuer Consent-Step VOR Provider-Grid (view-Machine
consent → grid → form), exakter Hans-Müller-Wortlaut für Art. 9 Abs. 2
lit. a Einwilligung
- MailConsentReminderSheet: Re-Consent-Modal beim App-Open für Bestands-User
- Stores mailConsent + mailConnectDraft (letzterer fixt Bug: Email/Provider
ging verloren wenn User Browser für App-Pw-Generierung öffnete)
- 12 neue i18n-Keys mail.consent.* in DE + EN
- Versionierter Consent-Text: art9-mail-v1-2026-05-13 (Bump bei Text-Änderung
triggert Re-Consent für alle)
Outlook-OAuth Schema (Phase 0 — additiv, Endpoints kommen später):
- mail_connections: auth_method (default 'app_password' → keine Bestands-
Connection bricht), oauth_access_token, oauth_refresh_token,
oauth_token_expiry, oauth_scope
- Encryption via bestehendes server/utils/crypto.ts (AES-256-GCM, Key aus
Infisical)
- Plan-Doc backend/docs/mail-outlook-oauth-plan.md (mo)
- DSB-Review backend/docs/mail-outlook-oauth-dsgvo-review.md (Hans-Müller):
MS als Sub-AV via DPA Sep 2025, EU Data Boundary seit Feb 2025; 5 Pflicht-
Aufgaben + Anwalts-Klärung zu DPA-Anspruch ohne MS-Lizenz
Profile — Cooldown-Pattern-Analysis als Collapsible:
- CooldownPatternAnalysis: 24h-Uhrzeit-Heatmap, Mo–So-Wochentag-Histogramm,
Top-5-Reason-Wortcloud mit Stop-Words-Filter, Cancel-Rate-Anzeige
- DiGA-relevant: NLP läuft client-side, reason-Texte verlassen das Device
nicht (gut für DSB-Akte)
- useProfileData: useCooldownHistoryFull (limit=100) für Pattern-Analyse
- Neutral formuliert, kein Stigma, alle Headings als Frage
Plan-Docs (kein Code):
- backend/docs/mail-custom-keywords-plan.md — Pro/Legend Custom-Keyword-Filter
(3.25 PT MVP, user-scoped, Body-Match in Phase 2)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PostCommentsSheet:
- Fix Resize-Bug: PanResponder nur auf Grabber+Header, kein onStartShouldSetPanResponderCapture
(das stahl Touch-Events von der FlatList und brach Drag-Resize)
- Height-Limits (MAX/MIN/INITIAL) als Refs in PanResponder-Closure, damit sie nicht
auf den ersten-Render-Stand eingefroren werden
- Keyboard-Show/-Hide animiert currentHeight korrekt ohne den Resize-Referenzpunkt
zu verlieren
- Avatar in CommentRow: resolveAvatar() wenn authorAvatar vorhanden, Initialen-Fallback
sonst. Bereit sobald Backend authorAvatar in Comments-Response mitliefert.
- Alle Pressable durch TouchableOpacity ersetzt
SheetFieldStack (neu):
- Progressives Multi-Input-Pattern als FormSheet-Inhalt
- Ausgefüllte Felder werden als antippbare Chips (mit Stift-Icon) nach oben verschoben
- Aktives Feld: TextInput + →/✓-Button (letztes Feld = Checkmark)
- Validate + Normalize pro Feld, Fehleranzeige unter dem Input
- suffix-Slot für Eye-Toggle etc.
- Nach letztem Feld: Keyboard.dismiss() + children (Rest des Formulars) erscheint
Migriert auf FormSheet + SheetFieldStack:
- ConnectMailSheet: Grid-View unveraendert; Form-View (email+password) via SheetFieldStack;
Zurück/Abbrechen-Header-Buttons entfernt (Schliessen = Swipe/Backdrop)
- EditMailAccountSheet: single-password-field via SheetFieldStack; Cancel-Header-Button weg
- AddDomainSheet: domain-field via SheetFieldStack; Favicon-Preview+Warning+Checkbox+Button
als children; Cancel-Header-Button weg
- CreateRoomSheet: name+description via SheetFieldStack; Public-Toggle+JoinMode+Buttons
als children; Abbrechen-Button bleibt (kein Header-Button, design-OK)
useSheetKeyboardLift: geloescht (keine Aufrufer mehr nach Migration)
KeyboardAwareSheet bleibt (AddMacSheet + AddWindowsSheet nutzen es noch)
tsc --noEmit: gruen
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two bugs caused the domainRealtime channel to fail with CHANNEL_ERROR and
reconnect-loop every 3s (which also dragged down the notifRealtime channel via
the shared websocket close):
1. useDomainSubmissionRealtime.ts filtered domain_submissions on a column that
doesn't exist (`submitter_id`) — the actual column is `user_id`. Postgres
raised on the publication-side filter registration → CHANNEL_ERROR.
2. rebreak.user_custom_domains was never added to the supabase_realtime
publication — the channel also subscribes to that table. New migration
20260511_fix_realtime_user_custom_domains adds it.
(Diagnosis via backyard agent against the self-hosted Supabase on the Hetzner box.)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
After the cooldown elapses and forceDisable() runs (VPN off + tamper-lock
disarmed), Android's a11y service can't deactivate itself — surface a friendly
Alert routing the user to Settings → Accessibility so they can finish removing
protection. Wired into both the fetchState cooldown active→inactive transition
and the AppState 'active' check; idempotent via ref.
(Native side — disable() also disarms the tamper-lock, RebreakAccessibilityService
goes fully passive when neither tamper-locked nor enabled, syncBlocklist no longer
re-starts the VpnService when disabled — lives in the gitignored module/android dir,
not committed here.)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- protection.ts: setCooldownTestMode/getCooldownTestMode (AsyncStorage 'dev:cooldown-testmode');
requestDeactivation sends testMode:true when on (__DEV__ only)
- debug.tsx: CooldownTestModeToggle (Switch) — '40s instead of 24h, staging only'
- useProtectionState.ts: wire applyCooldownDisableIfElapsed() — fires on cooldown
active→false transition (guarded so no extra fetch per poll) + on AppState 'active';
protection actually turns off when the (test-)cooldown elapses (the 'Step 5b' auto-disable)
- DeactivationExplainerSheet.tsx: useSafeAreaInsets — header paddingTop insets.top+14,
ScrollView paddingBottom max(insets.bottom,12)+24; back btn Pressable→TouchableOpacity
- ProtectionDetailsSheet.tsx: ScrollView paddingBottom max(insets.bottom,16)+24 (was 40);
backdrop + 'Fertig' Pressable→TouchableOpacity
tsc clean. (Note: 'sheet doesn't scroll' — the bottom content was being clipped under the
home indicator; the paddingBottom fix should resolve it. Broader UI polish deferred to a
separate session — Task #10.)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- components/plan/PlanChangeSheet.tsx — upgrade/downgrade briefing per pricing-tiers.md §4
(fetches GET /api/plan/change-preview; gains/keeps/changes; recovery-safety line;
billing hint w/o purchase button; CTA row, no 'are you sure?' interstitial)
- debug.tsx: PlanOverrideToggle routes every flip through PlanChangeSheet first
- devices.tsx + protectedDevices.ts: 'degraded' status (red, inline 'protection expired —
remove the profile yourself' hint, no green checkmark); maxProtectedDevices limit hint
- mail.tsx + MailAccountCard.tsx + useMailStatus.ts: over-limit banner + paused-account
greyed-out + PausedBadge (all defensive — only shows if backend sends the field)
- blocker.tsx: free-tier transparency hint ('Grundschutz aktiv — voller Schutz: Pro/Legend')
+ custom-domain over-limit banner
- locales: plan.change.* + plan_limit.* (de + en)
tsc clean. Backend side (GET /api/plan/change-preview, paused/degraded fields) in progress
in parallel — UI built defensively to work before it lands.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>