387 Commits

Author SHA1 Message Date
chahinebrini
23b91a1a3e fix(ios-vpn): PacketTunnel Self-Heal — Blocklist-Retry bei leerem Start
startTunnel lädt blocklist.bin manchmal mit 0 Hashes (Datei wegen
Data-Protection bei gesperrtem Gerät noch nicht lesbar). Bisher blieb
Layer 1 dann tot bis zum nächsten App-Sync — in den Geräte-Logs als
~30-Min-Fenster mit 0 Hashes sichtbar (z.B. 16:22 startTunnel→0,
erst 16:55 per Darwin-Reload geheilt).

scheduleBlocklistRetryIfEmpty: lädt nach 3/10/30/60/120/300s erneut,
bis Hashes da sind (max 20 Versuche). Sobald das Gerät entsperrt ist,
wird die Datei lesbar → Self-Heal greift, ohne auf einen App-Sync zu
warten.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 19:07:31 +02:00
chahinebrini
38a74dd1ad test(custom-domains): plan-limits-Test auf gemeinsamen Slot-Pool umgeschrieben
Der Test prüfte die alte Per-Bucket-Logik (web/mail getrennt) + Free-Tier.
Angepasst an den 10/20-Pool-Refactor: customDomains als Zahl, Pro 10 /
Legend 20, gemeinsamer web+mail-Pool, LIMIT_REACHED, GET liefert
{ items, count, limit }. Free-Tier-Fälle entfernt. 45/45 grün.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 18:42:31 +02:00
chahinebrini
834e6efffc fix(chat/post): PostCard-Bilder-Ecken, Chat-Spinner-Hänger, Bild-Cache
- 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>
2026-05-22 18:40:35 +02:00
chahinebrini
704958320b refactor(domains): gemeinsamer 10/20-Slot-Pool, Free-Tier entfernt
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>
2026-05-22 18:40:28 +02:00
chahinebrini
7cc30db020 feat(blocker): VIP-Liste als Kachel-Grid + Brand-Token-Konzept
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>
2026-05-22 18:15:59 +02:00
chahinebrini
ef28f4947a fix(ios-vpn): blocklist.bin File-Protection — .completeUntilFirstUserAuthentication
Layer 1 (PacketTunnel-DNS-Sinkhole) blockte nichts: `startTunnel` lud
0 Hashes, obwohl `blocklist.bin` 330k Hashes hatte. Ursache: `syncBlocklist`
setzte `URLFileProtection.complete` — die Datei ist damit bei GESPERRTEM
Gerät unlesbar. Der PacketTunnel wird OS-getrieben (on-demand) auch während
Lock-Phasen neu gestartet → `open()` schlägt fehl → 0 Hashes → Layer 1 tot
bis zum nächsten App-Sync.

Beweis aus den Extension-Logs: `blocklist reloaded` (Darwin-Notif nach
App-Sync, Gerät entsperrt) = 329k-330k Hashes; `startTunnel → blocklist
geladen` = 0 — dieselbe Datei, dieselbe Path.

Fix: `.complete` → `.completeUntilFirstUserAuthentication` (blocklist.bin +
webContent-Cache). Bleibt at-rest verschlüsselt (DiGA-konform), ist aber nach
dem ersten Entsperren seit Boot lesbar — die korrekte Klasse für eine Datei,
die eine Network-Extension 24/7 lesen muss.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 18:15:50 +02:00
chahinebrini
fe156a5f58 feat(blocker/vip): Freigabe-Button, landabhängige VIP-Liste, Hybrid-Komposition + Add-Check
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>
2026-05-22 17:27:10 +02:00
chahinebrini
e946fbe443 fix(ios-vpn): activateUrlFilter — Tunnel-Status bis .connected pollen
Die Warte-Schleife lief nur solange status==.connecting. Direkt nach
startVPNTunnel() steht der Status aber oft noch kurz auf .disconnected →
Schleife uebersprungen → sofort false-negative 'tunnel_not_connected',
obwohl der Tunnel Sekundenbruchteile spaeter sauber connected.
Jetzt: pollen bis .connected oder .invalid oder ~6s Timeout.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 23:44:15 +02:00
chahinebrini
1aa86c2c0c fix(ios-vpn): K1/K2/H1 — Upstream-Fehler, In-Flight-Cap, Response-Layout
K2: forwardPacket schreibt jetzt bei jedem Upstream-Fehler eine synthetische
SERVFAIL ins TUN (zentrale finish(gotAnswer:)) — DNS-Client haengt nicht mehr.
H1: harter In-Flight-Cap (32) statt Connection-pro-Query — ueber dem Cap sofort
SERVFAIL; deckelt den NE-Memory-Footprint.
K1: buildErrorResponse + dnsQuestionEnd schneiden das Paket hinter der Question-
Section ab (EDNS-OPT-Muell weg), IP/UDP-Length neu berechnet.
H3: Compression-Pointer-QNAME → fail-open (.forward) statt stillem Drop.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 23:22:22 +02:00
chahinebrini
dfcee68dc8 feat(blocker): Blocker-Page-Redesign — 2 Sektionen statt 4 Bloecke
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>
2026-05-21 23:15:23 +02:00
chahinebrini
adeaf4eb75 fix(ios-vpn): Packet-Tunnel-.appex in eigene dst-13-Embed-Phase zwingen
Bug: die RebreakPacketTunnelExtension.appex wurde nach
ReBreak.app/Extensions/ statt PlugIns/ embedded → Install-Crash
AppexBundleMissingEXAppExtensionAttributesDict (klassische NSExtension
im ExtensionKit-Ordner).

Ursache (verifiziert gegen node_modules/xcode + E2E-Lauf der Plugin-Kette):
withXcodeProject-Mods laufen LIFO — withPacketTunnelTarget zuerst,
withExtensionTarget (NEURLFilter) danach. proj.addTarget() bettet die
.appex über buildPhaseObject() ein, das projektweit die erste
PBXCopyFilesBuildPhase mit Section-Comment "Copy Files" matcht. Der
NEURLFilter-Umbau ändert nur phase.name/dstSubfolderSpec, nicht den
Section-Comment → die PacketTunnel-.appex landet in der dst-16-Phase
des NEURLFilter-Targets.

Fix in withPacketTunnelTarget: die .appex aus allen Copy-Files-Phasen
herausziehen, exklusiv in die eigene dst-13-Phase legen UND deren
Section-Comment auf "Embed App Extensions" umbenennen — damit der
nachfolgende NEURLFilter-addTarget-Lookup die Phase nicht mehr matcht.
NEURLFilter-Code unangetastet (dessen Umbau matcht per .appex-Dateiname).

Verifiziert: frisches Projekt, vorhandenes URLFilter-Target, Idempotenz.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 23:13:54 +02:00
chahinebrini
0d28682749 fix(ios-vpn): PacketTunnel-Extension CFBundleVersion/Short an App angleichen
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 23:13:54 +02:00
chahinebrini
5a16cf771b feat(ios-protection): v1 NEPacketTunnelProvider DNS-Sinkhole als Layer-1
Neuer iOS-Layer-1-Filter: ein NEPacketTunnelProvider-DNS-Sinkhole — MDM-frei,
ab iOS 16, Parität zum Android-VPN-DNS-Filter. Ersetzt den Apple-seitig
blockierten NEURLFilter als Default. NEURLFilter-/PIR-Code bleibt inaktiv als
iOS-26-Upgrade-Pfad erhalten (User-Entscheidung).

Neues Extension-Target RebreakPacketTunnelExtension/:
- PacketTunnelProvider.swift — TUN-Setup (virtuelle DNS-IP 10.0.0.1, nur diese
  Route ins TUN), Read-Loop, NXDOMAIN-Sinkhole, Upstream-Forward via
  NWConnection zu 1.1.1.1, Blocklist-Reload via Darwin-Notification.
- DnsFilter.swift / HashList.swift / DomainHasher.swift — Swift-Ports der
  Android-DNS-Filter-Logik. blocklist.bin-Format (sortierte big-endian UInt64,
  SHA-256-Prefix) 1:1 beibehalten, mmap statt Heap-Load.

RebreakProtectionModule.swift:
- activateUrlFilter startet jetzt den Packet-Tunnel via NETunnelProviderManager
  (Default-Layer-1, On-Demand-Auto-Reconnect aktiv).
- NEURLFilter-Code in activateNeUrlFilter ausgelagert (inaktiv, behalten).
- getDeviceState/disable lesen bzw. stoppen den Tunnel-Status.

with-rebreak-protection-ios.js: zweites app_extension-Target, klassischer
Embed-Pfad (dstSubfolderSpec 13), packet-tunnel-provider-Entitlement + App-Group.
app.config.ts: zweites appExtensions-Target.

NICHT auf echtem Gerät verifiziert — NE-Packet-Tunnel laufen nicht im
Simulator. Ungetestete Annahmen im Code mit "UNGETESTETE ANNAHME" markiert.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 23:13:54 +02:00
chahinebrini
a713070d25 feat(protection): VIP umfasst auch approved Custom-Domains
getWebCustomDomains schliesst nur noch 'rejected' aus — 'approved' Domains
bleiben in der Layer-2-VIP (Zweitschutz, falls Layer 1 aus ist). Reihenfolge:
pending zuerst (keine Layer-1-Deckung → duerfen nie aus dem 50er-Cap fallen),
dann approved neueste-zuerst (Ueberlauf = aelteste approved, via Layer 1 gedeckt).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 22:44:19 +02:00
chahinebrini
ced749018b fix(custom-domains): web-Domains nicht am alreadyGlobal-Check abweisen
Der alreadyGlobal-Pre-Check (Domain schon in der 208k-Layer-1-Blocklist →
kein Custom-Slot) gilt jetzt nur noch fuer Mail-Typen. Fuer type='web'
uebersprungen: Web-Custom-Domains speisen die Layer-2-VIP-Liste, eine separate
Schicht — eine global (Layer 1) gelistete Domain muss in die VIP koennen,
gerade weil Layer 2 das Netz ist, wenn Layer 1 aus ist.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 22:14:18 +02:00
chahinebrini
c3390a0fed fix(blocker): webContent-Sync von URL-Filter entkoppeln
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>
2026-05-21 21:54:11 +02:00
chahinebrini
bc65c7172c fix(protection): webcontent-domains — fehlenden getWebCustomDomains-Import
Der Endpoint nutzte getWebCustomDomains ohne Import → ReferenceError → HTTP 500.
server/db/ ist nicht auto-importiert (nur server/utils/), daher expliziter
Import wie in allen anderen 15+ db/domains-Konsumenten.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 21:53:49 +02:00
chahinebrini
9dfccfcbaa fix(protection): webcontent-domains-Endpoint konsolidiert — VIP-Komposition
Ersetzt die statische v1 des Endpoints durch die per-User-VIP-Komposition:
eigene Web-Custom-Domains zuerst + globale Auffuellung → dedup → Cap 50.
v1-Reste entfernt (backend/data/, serverAssets-Eintrag) — eine Datenquelle
(backend/server/data/gambling-domains.json, direkter JSON-Import).
pnpm build:backend gruen verifiziert.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 21:21:22 +02:00
chahinebrini
72cd195d36 feat(blocker): VIP-Domain-Sektion + Cooldown-Friction beim Entfernen
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>
2026-05-21 21:12:09 +02:00
chahinebrini
cc2d963d1f feat(protection): runtime-Sync + cache-first fuer iOS Layer-2-Domain-Liste
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>
2026-05-21 20:21:54 +02:00
chahinebrini
86ed603a45 feat(protection): GET /api/protection/webcontent-domains — runtime-updatebare Layer-2-Domain-Liste
Neuer Nitro-Endpoint serviert die kuratierte Gambling-Domain-Liste pro Land
(DE/GB/FR) aus backend/data/gambling-domains.json. Auth wie alle
/api/protection/*-Routen (requireUser). 50-Domain-Cap pro Land serverseitig
erzwungen. Liste pflegen = JSON editieren + _meta.version hochzaehlen + deploy.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 20:21:54 +02:00
chahinebrini
50d3c6a8e1 fix(protection): Layer 2 — Family Controls default-on + Resource-Bundle-Pfad
- familyControlsEnabled jetzt default-on (Apple-Distribution-Entitlement
  freigegeben); nur REBREAK_ENABLE_FAMILY_CONTROLS=0 schaltet ab.
- gambling-domains.json ins Pod-Verzeichnis verschoben + Podspec resource_bundles
  auf within-pod-Pfad korrigiert — CocoaPods buendelt keine Dateien ausserhalb
  des Pod-Roots (Bundle blieb sonst leer → Layer 2 griff nicht).
- Layer 2 (webContent-Filter) damit verifiziert funktionierend.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 20:13:57 +02:00
chahinebrini
ad51ce5099 feat(protection): Layer 2 in FC-Aktivierung einhaengen + URLFilter-Extension-Version
Layer-2-webContent-Filter laeuft jetzt automatisch bei activateFamilyControls/
activate mit (Helper applyWebContentLayer). URLFilterExtension CFBundleVersion/
ShortVersion an die App angeglichen. Apple-DTS-Report einreichfertig.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 19:25:24 +02:00
chahinebrini
627ddce995 Merge branch 'feat/ios-webcontent-layer2' — iOS Schutz-Layer-2 (webContent)
WebKit webContent-Filter via ManagedSettings (MVP-Plumbing):
applyWebContentFilter/clearWebContentFilter, gebuendelte Gambling-Domain-Liste
DE/GB/FR (Starter), JS-Bridge + Hook. Braucht nur Family Controls.
Auto-Trigger-Gating bewusst offen — TODO(layer2-gating).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 18:11:23 +02:00
chahinebrini
b1e46a065a chore(assets): Android-Onboarding-Screenshots (a11y + VPN-Permission, de)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 18:09:42 +02:00
chahinebrini
2839564469 fix(custom-domains): Per-Bucket-Limit-Check via Backend counts/limits
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>
2026-05-21 18:09:42 +02:00
chahinebrini
35a71a9068 feat(presence): Online-Presence-Provider + Hooks
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 18:09:42 +02:00
chahinebrini
29bbf23405 feat(protection): iOS NEURLFilter-Spike + PIR-Server-Ops
NEURLFilter-Stack (iOS 26): Extension RebreakURLFilter -> URLFilterExtension
umbenannt, url-filter-provider-Entitlement, Bloom-Prefilter-Extension,
PIR-Client-Config (pirServerURL/pirAuthToken via Build-Env).
PIR-Server-Ops unter ops/pir-server/ (Dockerfile, build-and-deploy, Patches,
DTS-Report). backend/scripts/generate-pir-input.ts erzeugt die PIR-Datenbank.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 18:09:42 +02:00
chahinebrini
6cd3a78aaf feat(protection): iOS Layer-2 webContent-Filter (ManagedSettings) — MVP-Plumbing
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>
2026-05-21 16:07:44 +02:00
chahinebrini
c6604f02df chore(release): v0.3.4 — enable Family Controls (Apple distribution entitlement approved) 2026-05-20 07:13:13 +02:00
chahinebrini
ea152a9169 feat(debug): debug-page in TestFlight via EXPO_PUBLIC_ENABLE_DEBUG flag
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.
2026-05-20 05:42:00 +02:00
chahinebrini
5a44b8162e chore(release): v0.3.3 — FC-gating + native log-viewer + expo-image 2026-05-20 05:34:57 +02:00
chahinebrini
f318364c7e fix(protection): gate Family Controls on entitlement + native log viewer
Verifiziert via Build-11-.ipa-Inspektion + Device-Logs:
- com.apple.developer.family-controls FEHLT im signierten Distribution-Build
  (Plugin gated korrekt auf REBREAK_ENABLE_FAMILY_CONTROLS — Apple-
  Distribution-Approval pending).
- Device-Log beweist: ManagedSettingsAgent-XPC → "error 159 Sandbox
  restriction" → NSCocoaErrorDomain:4099. Kein Daemon-/Race-Problem,
  Retry zwecklos.

Root-Cause des FC-Bugs: app.config.ts hatte familyControlsEnabled hart
auf true (falscher "approved"-Kommentar) → App bot App-Lock an obwohl
sandbox-blockiert. FAMILY_CONTROLS_AVAILABLE wurde nirgends konsumiert.

Fixes:
- app.config.ts: familyControlsEnabled an REBREAK_ENABLE_FAMILY_CONTROLS
  gekoppelt (== Plugin-Gating) → in TestFlight/production false.
- blocker.tsx: iOS App-Lock-Card nur wenn FAMILY_CONTROLS_AVAILABLE.
  lockedIn akzeptiert URL-Filter allein wenn FC build-seitig fehlt.
- ProtectionSlide.tsx: Onboarding überspringt den App-Lock-Step (FC)
  wenn nicht verfügbar — URL-Filter allein = vollwertiger Schutz.

Native Log-Viewer (für NEFilter-Debug ohne Mac/Console.app):
- Swift: getProtectionLogs/clearProtectionLogs lesen SharedLogStore aus
  App-Group-UserDefaults.
- lib/protection.ts wrapper + TS-module-types.
- debug.tsx: ProtectionLogCard (iOS) — native NEFilter/FC-Logs in der
  App sichtbar, copy/clear/refresh.
2026-05-20 05:32:06 +02:00
chahinebrini
b8e4b02b88 perf(images): migrate react-native Image → expo-image (memory+disk cache)
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).
2026-05-20 04:49:11 +02:00
chahinebrini
a9015d1951 chore(release): v0.3.2 — bug-fix bundle (permission/cooldown/onboarding) 2026-05-20 04:30:24 +02:00
chahinebrini
306a8fda30 fix(blocker): cooldown-elapsed modal platform-specific (no Android text on iOS)
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.
2026-05-20 04:25:15 +02:00
chahinebrini
312c668ae9 feat(onboarding): back-button between steps + language switcher on welcome
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.
2026-05-20 04:20:22 +02:00
chahinebrini
34005803da fix(protection): cooldown-elapse must set protectionDisabledAt in state endpoint
Bug: nach Cooldown-Ablauf reaktivierte sich der Schutz automatisch.

Root-Cause: Race zwischen zwei Endpoints die abgelaufene Cooldowns auflösen.
/api/cooldown/status setzt korrekt profile.protectionDisabledAt. /api/
protection/state — alle 5s während Cooldown gepollt — machte beim Ablauf
NUR resolveCooldown(), ohne protectionDisabledAt. state.get gewann das Race
fast immer → protectionDisabledAt blieb null → protectionShouldBeActive=true
→ Frontend-Bypass-Detection reaktivierte den Schutz.

Fix: state.get.ts setzt im expired-Branch ebenfalls protectionDisabledAt +
cooldownJustResolved-Flag.
2026-05-20 04:17:24 +02:00
chahinebrini
c32eeeb070 fix(protection): NEFilter retry + FamilyControls 4099 recovery sheet
Tester reports: nach 'Don't Allow' im System-Dialog reagiert Re-Request
nicht (NEFilter), plus FamilyControls wirft NSCocoaErrorDomain:4099
(XPC-Daemon-Failure). Mehrere TestFlight-User betroffen.

Swift native:
- resetUrlFilter: 800ms delay nach remove + 3x retry-loop bei code 5
- activateFamilyControls: 3x retry-loop mit Backoff bei 4099

JS:
- PermissionDeniedSheet generic via variant prop (nefilter|family_controls)
- Blocker + Onboarding: 4099-detect → Recovery-Sheet mit 3-Step-Fallback

I18n DE/EN/FR/AR: blocker.family_controls_error.* keys
2026-05-20 03:51:33 +02:00
chahinebrini
73f70b5e28 fix(language): auto-sync from user_metadata.locale at sign-in
Bug: User mit iOS-Sprache=Arabisch sah App auf Englisch wenn
Localization.getLocales() auf seinem Setup nicht zuverlässig 'ar'
zurückgab (iOS-Region≠Sprache, App-Override etc).

Fix: bei sign-in (init() initial-getSession + onAuthStateChange für
SIGNED_IN events) wird session.user.user_metadata.locale gelesen.
Wenn AsyncStorage @rebreak/language NOCH NICHT gesetzt ist (User hat
keine explicit Choice gemacht) → silent apply der server-locale
(inkl. RTL-flip, KEIN Restart-Alert).

Respektiert User-Choice: wenn AsyncStorage gefüllt ist (z.B. User hat
manuell in Settings gewechselt), bleibt das gewinnen.
2026-05-19 22:02:34 +02:00
chahinebrini
63a1a3b550 fix(staging): export NITRO_* env-mapping for BREVO_API_KEY + hook secrets
start-staging.sh re-exports Infisical secrets als NITRO_*-prefixed envs
damit Nitro's runtimeConfig sie zur Laufzeit overrided (Default ist
Build-Time evaluated, also "" wenn nicht im Runner-Env).

Drei neue Mappings:
- BREVO_API_KEY → NITRO_BREVO_API_KEY
- HOOK_SEND_EMAIL_SECRETS → NITRO_HOOK_SEND_EMAIL_SECRETS
- MAIL_SENDER_EMAIL → NITRO_MAIL_SENDER_EMAIL

Ohne diese Mapping bleibt config.brevoApiKey leer im laufenden Backend →
send-email Hook fired 500 (siehe Smoke-Test).
2026-05-19 18:32:01 +02:00
chahinebrini
bdfcc40a6c feat(auth-hook): send-email hook for dynamic sender-name + i18n subject
GoTrue-Send-Email-Hook (v2.155+) → backend rendert Mails selbst, schickt
via Brevo Transactional API. Vorteil: pro Mail-Typ × Locale eigener
Sender-Display-Name UND Subject (GoTrue's eingebauter Mailer ist global
statisch).

Files:
- backend/server/utils/mail/templates.ts — 5 mail-types × 4 locales
  Sender-Name, Subject, Title, Body, HelpText, Privacy-Label.
  HTML-Renderer mit Nunito + Icon + DSGVO-Footer (analog public/templates).
- backend/server/utils/mail/brevo.ts — Brevo Transactional API client
  (POST /v3/smtp/email, separate REST-API-Key vom SMTP-Key)
- backend/server/api/auth-hooks/send-email.post.ts — Endpoint mit
  Standard-Webhooks Signature-Verify (HMAC-SHA256, timing-safe compare,
  multi-secret-rotation support)
- backend/nitro.config.ts — runtimeConfig: brevoApiKey,
  hookSendEmailSecrets, mailSenderEmail

Requires (außerhalb dieses commits):
- Infisical staging: BREVO_API_KEY (✓), HOOK_SEND_EMAIL_SECRETS (✓)
- docker-compose: GOTRUE_HOOK_SEND_EMAIL_{ENABLED,URI,SECRETS} (✓)
- GoTrue upgrade v2.154 → v2.189 (✓, mit auth.factor_type owner-transfer)
2026-05-19 18:04:14 +02:00
chahinebrini
a0dff80ced ui(signup): privacy-notice shield als app-icon-look
🛡️ Unicode-Schild → grünes (22c55e) rounded-md Square mit weißem
Ionicons shield-checkmark, 32x32. Konsistent mit App-Icon-Style.
2026-05-19 17:57:02 +02:00
chahinebrini
a8e094be9d fix(mail-templates): banner layout — icon left, wordmark centered
Vorher: Icon und 'ReBreak'-Wordmark untereinander (vertikal gestapelt).
Jetzt: 3-Spalten-Tabelle — Icon links (48x48), Wordmark mittig,
gleichgroßer Spacer rechts (52px) für visuelle Mitte.

Icon-Größe von 64x64 → 48x48 reduziert für besseres Höhen-Verhältnis.
2026-05-19 13:34:25 +02:00
chahinebrini
f2545738ee feat(mail-templates): Nunito font, app-icon header, DSGVO footer
- App-Icon (128x128, resized von 1024x1024, 6.7KB) im dunklen Header
  über dem 'ReBreak'-Wordmark, abgerundete Ecken (border-radius:14px)
- Nunito (Google Fonts, weights 400/600/700) für allen Body-Text,
  Inline font-family auf jedem Element wegen Gmail-Style-Stripping.
  OTP-Code bleibt monospace (Apple SF Mono / Menlo)
- DSGVO-Footer: rebreak.org + Datenschutz-Link (i18n'd: Datenschutz /
  Privacy / Confidentialité / الخصوصية)
- 5 Templates konsistent: confirmation, recovery, magic_link, invite,
  email_change
2026-05-19 11:10:49 +02:00
chahinebrini
12e140e25b refactor(signup): remove firstName/lastName fields (DSGVO data-minimization)
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).
2026-05-19 11:05:18 +02:00
chahinebrini
512949f851 fix(nitro): explicit publicAssets dir=../public so templates ship in .output
Nitro resolved default publicAssets `dir: "public"` relativ zu srcDir
(`server/`) statt zum rootDir. Folge: backend/public/templates/*.html
wurde nicht in .output/public/ kopiert → GoTrue lädt 404.

Fix: explicit `dir: "../public"` zeigt auf <projectRoot>/public/.
2026-05-19 10:58:18 +02:00
chahinebrini
5434254f74 feat(auth,mail): pw-reset OTP-flow + custom mail templates + account-switch cleanup
- Phase 3 PW-Reset: 3 screens (forgot-password → reset-otp → new-password),
  verifyOtp({type:'recovery'}), new updatePassword() action
- Custom Brevo-Mail templates (backend/public/templates/) — 5 HTMLs with
  go-template i18n (de/en/fr/ar incl. RTL for AR), OTP-only (no link),
  ReBreak branding
- signUp metadata.data.locale aus i18n.language → templates resolven Sprache
- Account-Switch-Bug fix: signOut() resettet alle 10 user-spezifischen stores
  + invalidateMe()
2026-05-19 10:49:23 +02:00
chahinebrini
b9b397b346 chore(release): v0.3.1 + eas-release.sh script
- package.json + app.config.ts (Expo source-of-truth) auf 0.3.1
- versionCode/buildNumber bleibt 10 (native files schon synced)
- eas-release.sh: build+submit für TestFlight/Play-Internal, default
  iOS, --android/--both flags, version-check-prompt vor build

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 08:11:18 +02:00
chahinebrini
5c539f8937 feat(presence,sheets,chat): tester-build polish bundle
Online-Status (Phase 1+):
- UserAvatar mit 4 Size-Variants (sm/md/lg/xl) + integrierter Online-Dot
- OnlinePresenceProvider: Supabase-Channel + Following-Filter
- ChatHeaderStatus: "Online" neutral / "vor X min" offline
- useLastSeen + Heartbeat (60s interval + AppState-background ping)
- Privatsphäre-Toggle in profile/index

Sheets:
- FormSheet Android-keyboard-fix (Dimensions.get('screen'), kein
  useWindowDimensions-Kollaps), useKeyboardHandler statt manual
  Keyboard.addListener, state-reset on re-open
- PostCommentsSheet same Pattern + close-after-submit + drag bis under
  app-header
- ConnectMailSheet form-view refactor: scrollable, AES-Banner als
  footnote, field-order email→pw→label, fixed 0.85 über alle Steps

Chat:
- DmChatBackground iOS klecks fix (G transform statt nested Svg)
- ChatInput Lyra-1:1 (keyboardWillShow, surfaceElevated bubble,
  arrow-up send, attachment links)
- dm/room/chat headers + conversation-list nutzen UserAvatar
- Foreign-Profile "Nachricht"-Button öffnet richtige DM

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 08:06:47 +02:00