fix(android-protection): explicit specialUse FGS type — Samsung/Android 16 crash loop

RebreakVpnService.onStartCommand crashed with SecurityException because Android 16's validateForegroundServiceType rejects the implicit 2-arg startForeground(). Now passes FOREGROUND_SERVICE_TYPE_SPECIAL_USE explicitly (Google's documented best practice) and guards the call so a failed foreground promotion stops the service cleanly instead of crashing the app. Verified vs reported Galaxy A54 / Android 16 signature (97% of crash events, 1-user crash loop).

Bundles pending working-tree work across native/marketing/locales/mac + graphify-out rebuild. gitignore: google-services.json + /screenshots/.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
chahinebrini 2026-06-10 22:33:28 +02:00
parent df3c4fafa3
commit 63fae25531
93 changed files with 7645 additions and 4142 deletions

3
.gitignore vendored
View File

@ -46,3 +46,6 @@ graphify-out/cache/
# graphify Hook-Backups (dated, 14 MB je) — nicht tracken # graphify Hook-Backups (dated, 14 MB je) — nicht tracken
graphify-out/20[0-9][0-9]-[0-9][0-9]-[0-9][0-9]/ graphify-out/20[0-9][0-9]-[0-9][0-9]-[0-9][0-9]/
graphify-out/graph.html graphify-out/graph.html
# Generated screenshot artifacts (Maestro/preview pipeline output — regeneratable)
/screenshots/

View File

@ -25,7 +25,7 @@
<li>Tippe auf das Profil-Icon oben rechts</li> <li>Tippe auf das Profil-Icon oben rechts</li>
<li>Wähle <strong>Einstellungen</strong></li> <li>Wähle <strong>Einstellungen</strong></li>
<li>Scrolle zu <strong>Konto</strong> und tippe auf <strong>Konto löschen</strong></li> <li>Scrolle zu <strong>Konto</strong> und tippe auf <strong>Konto löschen</strong></li>
<li>Bestätige die Löschung alle Daten werden innerhalb von 30 Tagen unwiderruflich entfernt</li> <li>Bestätige die Löschung. Alle Daten werden innerhalb von 30 Tagen unwiderruflich entfernt</li>
</ol> </ol>
</section> </section>
@ -35,7 +35,7 @@
<p class="mb-3">Falls du keinen Zugriff mehr auf die App hast, schreibe uns:</p> <p class="mb-3">Falls du keinen Zugriff mehr auf die App hast, schreibe uns:</p>
<div class="bg-elevated border border-default rounded-xl p-4 not-prose"> <div class="bg-elevated border border-default rounded-xl p-4 not-prose">
<p>E-Mail: <a href="mailto:datenschutz@rebreak.org" class="text-primary-400 hover:underline">datenschutz@rebreak.org</a></p> <p>E-Mail: <a href="mailto:datenschutz@rebreak.org" class="text-primary-400 hover:underline">datenschutz@rebreak.org</a></p>
<p class="mt-2 text-xs">Betreff: <em>Konto-Löschung ReBreak"</em></p> <p class="mt-2 text-xs">Betreff: <em>Konto-Löschung ReBreak"</em></p>
<p class="mt-2 text-xs">Bitte gib die mit deinem Konto verknüpfte E-Mail-Adresse an, damit wir dein Konto eindeutig identifizieren können.</p> <p class="mt-2 text-xs">Bitte gib die mit deinem Konto verknüpfte E-Mail-Adresse an, damit wir dein Konto eindeutig identifizieren können.</p>
</div> </div>
<p class="text-xs italic mt-3">Deine Anfrage wird innerhalb von 30 Tagen bearbeitet. Wir senden dir eine Bestätigung sobald die Löschung abgeschlossen ist.</p> <p class="text-xs italic mt-3">Deine Anfrage wird innerhalb von 30 Tagen bearbeitet. Wir senden dir eine Bestätigung sobald die Löschung abgeschlossen ist.</p>

View File

@ -20,7 +20,7 @@
<span <span
class="inline-block bg-amber-500/15 text-amber-400 text-xs font-semibold px-2.5 py-1 rounded-full mb-8" class="inline-block bg-amber-500/15 text-amber-400 text-xs font-semibold px-2.5 py-1 rounded-full mb-8"
> >
Beta iOS ist die Hauptplattform Beta, iOS ist die Hauptplattform
</span> </span>
<!-- Download Button --> <!-- Download Button -->
@ -95,7 +95,7 @@ const apkUrl = "/downloads/rebreak-android-latest.apk";
useSeoMeta({ useSeoMeta({
title: "Rebreak für Android APK Download", title: "Rebreak für Android APK Download",
description: "Lade die Rebreak Gambling-Recovery App als APK für Android herunter. Beta-Version iOS ist die Hauptplattform.", description: "Lade die Rebreak Gambling-Recovery App als APK für Android herunter. Beta-Version, iOS ist die Hauptplattform.",
}); });
definePageMeta({ definePageMeta({

View File

@ -8,7 +8,7 @@
</div> </div>
<div> <div>
<h1 class="text-2xl font-bold tracking-tight">RebreakMagic</h1> <h1 class="text-2xl font-bold tracking-tight">RebreakMagic</h1>
<p class="text-sm text-gray-400">Lock-Modus für iPhone ohne Reset</p> <p class="text-sm text-gray-400">Lock-Modus für iPhone, ohne Reset</p>
</div> </div>
</div> </div>
@ -20,14 +20,14 @@
<span <span
class="inline-block bg-amber-500/15 text-amber-400 text-xs font-semibold px-2.5 py-1 rounded-full mb-6" class="inline-block bg-amber-500/15 text-amber-400 text-xs font-semibold px-2.5 py-1 rounded-full mb-6"
> >
Beta Companion-App für ReBreak iOS Beta, Companion-App für ReBreak iOS
</span> </span>
<!-- Why "Magic"? --> <!-- Why "Magic"? -->
<div class="bg-indigo-950/30 border border-indigo-800/30 rounded-2xl p-5 mb-6"> <div class="bg-indigo-950/30 border border-indigo-800/30 rounded-2xl p-5 mb-6">
<p class="text-sm text-indigo-200 leading-relaxed"> <p class="text-sm text-indigo-200 leading-relaxed">
<strong>Warum Magic"?</strong> Normalerweise muss ein iPhone komplett zurückgesetzt werden, <strong>Warum Magic"?</strong> Normalerweise muss ein iPhone komplett zurückgesetzt werden,
um in den Lock-Modus (Supervised-Mode) zu wechseln alle Daten weg. RebreakMagic schafft um in den Lock-Modus (Supervised-Mode) zu wechseln, und alle Daten sind weg. RebreakMagic schafft
das <strong>ohne Reset</strong>: Fotos, Apps, Chats, Settings bleiben. In ~2 Minuten. das <strong>ohne Reset</strong>: Fotos, Apps, Chats, Settings bleiben. In ~2 Minuten.
</p> </p>
</div> </div>
@ -62,7 +62,7 @@
<div> <div>
<p class="font-medium text-sm text-white">Erste Öffnung erlauben</p> <p class="font-medium text-sm text-white">Erste Öffnung erlauben</p>
<p class="text-xs text-gray-400 mt-0.5"> <p class="text-xs text-gray-400 mt-0.5">
App ist (noch) unsigniert Rechtsklick Öffnen Bestätigen. Macht macOS einmalig nötig. App ist (noch) unsigniert: Rechtsklick Öffnen Bestätigen. Macht macOS einmalig nötig.
</p> </p>
</div> </div>
</li> </li>
@ -103,7 +103,7 @@
<p class="text-xs text-gray-400 leading-relaxed"> <p class="text-xs text-gray-400 leading-relaxed">
Drei Wege, geordnet nach Aufwand: 1) deine Vertrauensperson (Trustee) entsperrt aus der ReBreak-App. Drei Wege, geordnet nach Aufwand: 1) deine Vertrauensperson (Trustee) entsperrt aus der ReBreak-App.
2) iPhone erneut am Mac mit RebreakMagic anschließen und Reset" wählen. 2) iPhone erneut am Mac mit RebreakMagic anschließen und Reset" wählen.
3) Werks-Reset des iPhones (letzter Notausweg alle Daten weg). 3) Werks-Reset des iPhones (letzter Notausweg, alle Daten weg).
Das ist Designprinzip: der Schutz soll genau dem Impuls standhalten, der ihn loswerden will. Das ist Designprinzip: der Schutz soll genau dem Impuls standhalten, der ihn loswerden will.
</p> </p>
</div> </div>
@ -137,7 +137,7 @@ const dmgUrl = "/downloads/RebreakMagic-latest.dmg";
useSeoMeta({ useSeoMeta({
title: "RebreakMagic für Mac Lock-Modus ohne Reset", title: "RebreakMagic für Mac Lock-Modus ohne Reset",
description: description:
"Companion-App für ReBreak iOS. Aktiviert den Lock-Modus deines iPhones in ~2 Minuten per USB-Kabel — ohne Werks-Reset, ohne Datenverlust.", "Companion-App für ReBreak iOS. Aktiviert den Lock-Modus deines iPhones in ~2 Minuten per USB-Kabel, ohne Werks-Reset und ohne Datenverlust.",
}); });
definePageMeta({ definePageMeta({

View File

@ -20,14 +20,14 @@
<span <span
class="inline-block bg-amber-500/15 text-amber-400 text-xs font-semibold px-2.5 py-1 rounded-full mb-6" class="inline-block bg-amber-500/15 text-amber-400 text-xs font-semibold px-2.5 py-1 rounded-full mb-6"
> >
Beta für Pro &amp; Legend Beta, für Pro &amp; Legend
</span> </span>
<!-- Was es macht --> <!-- Was es macht -->
<div class="bg-indigo-950/30 border border-indigo-800/30 rounded-2xl p-5 mb-6"> <div class="bg-indigo-950/30 border border-indigo-800/30 rounded-2xl p-5 mb-6">
<p class="text-sm text-indigo-200 leading-relaxed"> <p class="text-sm text-indigo-200 leading-relaxed">
<strong>Systemweiter Schutz.</strong> ReBreak für Windows blockt Glücksspielseiten <strong>Systemweiter Schutz.</strong> ReBreak für Windows blockt Glücksspielseiten
auf <strong>DNS-Ebene</strong> in jedem Browser, jeder App. Auch Chrome, Edge und auf <strong>DNS-Ebene</strong>, in jedem Browser und jeder App. Auch Chrome, Edge und
Brave, die normalerweise das System-DNS umgehen, werden zuverlässig gefiltert. Brave, die normalerweise das System-DNS umgehen, werden zuverlässig gefiltert.
Ein Hintergrunddienst schützt die Einstellung vor dem Abschalten. Ein Hintergrunddienst schützt die Einstellung vor dem Abschalten.
</p> </p>
@ -69,7 +69,7 @@
<span class="w-7 h-7 shrink-0 rounded-full bg-indigo-600 flex items-center justify-center text-sm font-bold">3</span> <span class="w-7 h-7 shrink-0 rounded-full bg-indigo-600 flex items-center justify-center text-sm font-bold">3</span>
<div> <div>
<p class="font-medium text-sm text-white">Fertig</p> <p class="font-medium text-sm text-white">Fertig</p>
<p class="text-xs text-gray-400 mt-0.5">Der Schutz läuft ab sofort im Hintergrund auch nach einem Neustart.</p> <p class="text-xs text-gray-400 mt-0.5">Der Schutz läuft ab sofort im Hintergrund, auch nach einem Neustart.</p>
</div> </div>
</li> </li>
</ol> </ol>
@ -91,7 +91,7 @@
<h3 class="font-bold text-base mb-3 text-white">Wie wird der Schutz wieder gelöst?</h3> <h3 class="font-bold text-base mb-3 text-white">Wie wird der Schutz wieder gelöst?</h3>
<p class="text-xs text-gray-400 leading-relaxed"> <p class="text-xs text-gray-400 leading-relaxed">
Bewusst nicht per Klick: Der Schutz lässt sich nur mit Wartezeit (Cooldown) wieder Bewusst nicht per Klick: Der Schutz lässt sich nur mit Wartezeit (Cooldown) wieder
pausieren das schützt dich genau vor dem Impuls, der ihn im Drang-Moment loswerden will. pausieren. Das schützt dich vor dem Impuls, der ihn im Drang-Moment loswerden will.
Bei Abo-Ende oder Account-Löschung wird der Schutz ordnungsgemäß freigegeben. Bei Abo-Ende oder Account-Löschung wird der Schutz ordnungsgemäß freigegeben.
</p> </p>
</div> </div>
@ -126,7 +126,7 @@ const exeUrl = "/downloads/RebreakMagic-Setup.exe";
useSeoMeta({ useSeoMeta({
title: "ReBreak für Windows Glücksspiel-Schutz für deinen PC", title: "ReBreak für Windows Glücksspiel-Schutz für deinen PC",
description: description:
"Blockt Glücksspielseiten systemweit auf deinem Windows-PC in jedem Browser, auch Chrome und Edge. Per DNS-over-HTTPS, mit Tamper-Schutz im Hintergrund.", "Blockt Glücksspielseiten systemweit auf deinem Windows-PC, in jedem Browser, auch Chrome und Edge. Per DNS-over-HTTPS, mit Tamper-Schutz im Hintergrund.",
}); });
definePageMeta({ definePageMeta({

View File

@ -164,7 +164,7 @@
<h2 class="text-xl font-bold text-highlighted mb-3">§ 7 Verfügbarkeit und Wartung</h2> <h2 class="text-xl font-bold text-highlighted mb-3">§ 7 Verfügbarkeit und Wartung</h2>
<p> <p>
Der Anbieter bemüht sich um größtmögliche Verfügbarkeit (Best-Effort). Eine konkrete Der Anbieter bemüht sich um größtmögliche Verfügbarkeit (Best-Effort). Eine konkrete
Verfügbarkeitsgarantie besteht nicht. Wartungsfenster werden soweit möglich Verfügbarkeitsgarantie besteht nicht. Wartungsfenster werden, soweit möglich,
rechtzeitig angekündigt. rechtzeitig angekündigt.
</p> </p>
</section> </section>

View File

@ -282,14 +282,14 @@ const quotes = [
initials: "VF", initials: "VF",
}, },
{ {
text: "Bis du das Unbewusste bewusst machst, wird es dein Leben lenken und du wirst es Schicksal nennen.", text: "Bis du das Unbewusste bewusst machst, wird es dein Leben lenken, und du wirst es Schicksal nennen.",
author: "Carl Gustav Jung", author: "Carl Gustav Jung",
role: "Psychiater & Begründer der analytischen Psychologie", role: "Psychiater & Begründer der analytischen Psychologie",
image: "https://upload.wikimedia.org/wikipedia/commons/thumb/0/00/CGJung.jpg/200px-CGJung.jpg", image: "https://upload.wikimedia.org/wikipedia/commons/thumb/0/00/CGJung.jpg/200px-CGJung.jpg",
initials: "CJ", initials: "CJ",
}, },
{ {
text: "Sucht ist keine Charakterschwäche. Sie ist eine Erkrankung des Gehirns und sie ist behandelbar.", text: "Sucht ist keine Charakterschwäche. Sie ist eine Erkrankung des Gehirns, und sie ist behandelbar.",
author: "Nora Volkow", author: "Nora Volkow",
role: "Neurowissenschaftlerin, Direktorin des NIDA", role: "Neurowissenschaftlerin, Direktorin des NIDA",
image: "https://upload.wikimedia.org/wikipedia/commons/thumb/9/9b/Nora_Volkow2.jpg/200px-Nora_Volkow2.jpg", image: "https://upload.wikimedia.org/wikipedia/commons/thumb/9/9b/Nora_Volkow2.jpg/200px-Nora_Volkow2.jpg",

View File

@ -27,6 +27,7 @@
"vue-router": "^4.5.1" "vue-router": "^4.5.1"
}, },
"devDependencies": { "devDependencies": {
"@iconify-json/simple-icons": "^1.2.86",
"@nuxt/devtools": "latest", "@nuxt/devtools": "latest",
"typescript": "^5.9.3" "typescript": "^5.9.3"
} }

View File

@ -10,6 +10,9 @@ struct MacRegistrationView: View {
@State private var successMessage: String? @State private var successMessage: String?
@State private var profileInstalled = false @State private var profileInstalled = false
@State private var checkingProfile = false @State private var checkingProfile = false
@State private var isVerifying = false
@State private var verifyStatus = ""
@State private var backendActive = false
var body: some View { var body: some View {
VStack(spacing: 24) { VStack(spacing: 24) {
@ -67,12 +70,20 @@ struct MacRegistrationView: View {
.frame(maxWidth: 400) .frame(maxWidth: 400)
} }
if isVerifying {
verifyingCard
}
HStack(spacing: 12) { HStack(spacing: 12) {
Button("← Abbrechen") { model.returnToHub() } Button("← Abbrechen") { model.returnToHub() }
.buttonStyle(.bordered) .buttonStyle(.bordered)
.disabled(isRegistering || isInstallingProfile) .disabled(isRegistering || isInstallingProfile || isVerifying)
if model.magicRegistration == nil { if isVerifying {
// Während der Verifikation kein Aktions-Button der Flow läuft
// automatisch durch und navigiert bei Erfolg zur Übersicht.
EmptyView()
} else if model.magicRegistration == nil {
Button("Mac registrieren") { handleRegistration() } Button("Mac registrieren") { handleRegistration() }
.buttonStyle(.borderedProminent) .buttonStyle(.borderedProminent)
.disabled(isRegistering || macInfo == nil || isInstallingProfile) .disabled(isRegistering || macInfo == nil || isInstallingProfile)
@ -141,6 +152,20 @@ struct MacRegistrationView: View {
.frame(maxWidth: 400) .frame(maxWidth: 400)
} }
private var verifyingCard: some View {
HStack(spacing: 10) {
ProgressView()
.controlSize(.small)
Text(verifyStatus.isEmpty ? "Wird geprüft…" : verifyStatus)
.font(.callout)
.foregroundStyle(.secondary)
}
.padding()
.background(Color.blue.opacity(0.08))
.cornerRadius(8)
.frame(maxWidth: 400)
}
@ViewBuilder @ViewBuilder
private func successCard(_ message: String) -> some View { private func successCard(_ message: String) -> some View {
HStack(spacing: 8) { HStack(spacing: 8) {
@ -216,26 +241,89 @@ struct MacRegistrationView: View {
isInstallingProfile = true isInstallingProfile = true
errorMessage = nil errorMessage = nil
// 1. Profil herunterladen + System Settings Profile öffnen.
do { do {
try await MacProfileInstaller.downloadAndInstall(registration: registration) try await MacProfileInstaller.downloadAndInstall(registration: registration)
await MainActor.run {
isInstallingProfile = false
successMessage = "System Settings → Profile geöffnet. Bitte dort „Installieren“ klicken und Admin-Passwort eingeben."
}
// Re-check profile status nach kurzer Wartezeit (User muss in UI bestätigen)
try? await Task.sleep(nanoseconds: 3_000_000_000)
await checkProfileStatus()
} catch { } catch {
await MainActor.run { await MainActor.run {
isInstallingProfile = false isInstallingProfile = false
errorMessage = "Profil-Installation fehlgeschlagen: \(error.localizedDescription)" errorMessage = "Profil-Installation fehlgeschlagen: \(error.localizedDescription)"
} }
return
}
await MainActor.run {
isInstallingProfile = false
isVerifying = true
successMessage = "System Settings → Profile geöffnet. Bitte dort „Installieren“ klicken und Admin-Passwort eingeben."
verifyStatus = "Warte auf Profil-Installation…"
}
// 2. Warten bis das Profil tatsächlich installiert ist (User klickt
// in System Settings Installieren" + gibt Admin-PW ein).
let profileOK = await pollUntilProfileInstalled(timeoutSeconds: 180)
guard profileOK else {
await MainActor.run {
isVerifying = false
verifyStatus = ""
errorMessage = "Profil noch nicht installiert. Bitte in System Settings → Profile auf „Installieren“ klicken und es erneut versuchen."
}
return
}
await MainActor.run {
profileInstalled = true
verifyStatus = "Prüfe Schutz-Status am Server…"
}
// 3. Nebenbei: serverseitigen Binding-Status bestätigen.
let backendOK = await pollBackendActive(token: registration.dnsToken, attempts: 5)
await MainActor.run {
backendActive = backendOK
isVerifying = false
verifyStatus = ""
}
guard backendOK else {
await MainActor.run {
errorMessage = "Profil installiert, aber der Server bestätigt den Schutz noch nicht. Du kannst es später in der Übersicht prüfen."
}
return
}
// 4. Erfolg anzeigen, dann zurück zur Übersicht.
await MainActor.run {
successMessage = "✓ Mac geschützt — Glücksspiel-Domains werden jetzt blockiert."
}
try? await Task.sleep(nanoseconds: 1_800_000_000)
await MainActor.run {
model.returnToHub()
} }
} }
} }
/// Pollt lokal `profiles show` bis das ReBreak-DNS-Profil erscheint (oder Timeout).
private func pollUntilProfileInstalled(timeoutSeconds: Int) async -> Bool {
let deadline = Date().addingTimeInterval(Double(timeoutSeconds))
while Date() < deadline {
if await MacProfileInstaller.isInstalled() { return true }
try? await Task.sleep(nanoseconds: 2_000_000_000)
}
return await MacProfileInstaller.isInstalled()
}
/// Pollt `/api/magic/status?token=` bis `active=true` (oder Versuche erschöpft).
private func pollBackendActive(token: String, attempts: Int) async -> Bool {
for attempt in 0..<attempts {
if let active = try? await MagicAPIClient.shared.status(token: token), active {
return true
}
if attempt < attempts - 1 {
try? await Task.sleep(nanoseconds: 1_500_000_000)
}
}
return false
}
} }
#Preview { #Preview {

View File

@ -25,6 +25,10 @@ modules/*/ios/Pods/
*.key *.key
*.mobileprovision *.mobileprovision
# Firebase / FCM config (per-project, provided at build time — not tracked)
google-services.json
GoogleService-Info.plist
# Metro # Metro
.metro-health-check* .metro-health-check*

View File

@ -217,6 +217,8 @@ Test-User muss **vorab** auf dem Staging-Backend existieren:
## 8. Flow-Uebersicht ## 8. Flow-Uebersicht
### Bestehende Flows (Phase A)
| Flow | Was wird geprueft | Stabil? | | Flow | Was wird geprueft | Stabil? |
|---|---|---| |---|---|---|
| `auth/signin.yaml` | App startet, Login via Email+Pw, Home-Feed sichtbar | Ja (text-selektoren) | | `auth/signin.yaml` | App startet, Login via Email+Pw, Home-Feed sichtbar | Ja (text-selektoren) |
@ -230,6 +232,19 @@ Test-User muss **vorab** auf dem Staging-Backend existieren:
| `profile/demographics.yaml` | DemographicsAccordion toggle, WheelPicker oeffnet | Text-selektoren | | `profile/demographics.yaml` | DemographicsAccordion toggle, WheelPicker oeffnet | Text-selektoren |
| `settings/dark-theme.yaml` | Settings → Theme → Dunkel | Native-Menu-Limitation | | `settings/dark-theme.yaml` | Settings → Theme → Dunkel | Native-Menu-Limitation |
### Neue Flows (Phase A+, kritische Coverage)
| Flow | Prioritaet | Was wird geprueft | Stabil? | Blocker |
|---|---|---|---|---|
| `onboarding/new-user-streak-guard.yaml` | HOCH | Streak-Counter nach Login nicht 0 — Regressions-Guard fuer current_days-Bug | Ja | Kein frischer-Account-Test ohne reset-Mechanismus |
| `sos/crisis-flow.yaml` | HOECHSTE | SOS-Navigation + Lyra antwortet + Atemübung erreichbar | LLM-abhaengig | Avatar-Tap Koordinaten-Fallback |
| `blocker/activation-smoke.yaml` | HOCH | Blocker-Tab erreichbar, Protection Card + URL-Filter-Layer sichtbar | Ja | Activation-Step selbst nicht testbar (System-Dialog) |
| `calls/incoming-call-screen.yaml` | MITTEL | Chat-Tab laedt, DM-Liste zeigt keine Phantom-Call-Artefakte | Teilweise | Echter Incoming-Call braucht 2 Devices (Phase B) |
| `dm/send-message.yaml` | HOCH | DM oeffnen, Nachricht senden, Bubble erscheint | Abhaengig von E2E_TEST_PEER_NICKNAME | Peer-Nickname-Env-Var noetig |
| `dm/realtime-receive.yaml` | MITTEL | Realtime-Empfang — Nachricht erscheint ohne Reload | SCAFFOLD ONLY | Braucht externen Trigger oder 2-Device-Setup |
| `stress/dm-scroll-performance.yaml` | MITTEL | DM-Scroll auf A50, kein ANR/Crash nach 8 Swipes | Geraete-abhaengig | Braucht 50+ Messages in Test-DM |
| `stress/rapid-post-submit.yaml` | MITTEL | 3 Posts hintereinander, ComposeCard resettet jedes Mal | Ja | Posts landen in Staging-DB — manuelles Cleanup |
**Koordinaten-Fallback** = Flow nutzt `point: "x%, y%"` fuer Avatar-Button, weil kein `testID` vorhanden. **Koordinaten-Fallback** = Flow nutzt `point: "x%, y%"` fuer Avatar-Button, weil kein `testID` vorhanden.
Bricht wenn Header-Layout geaendert wird. Betroffene testIDs: `TODO_TESTIDS.md`. Bricht wenn Header-Layout geaendert wird. Betroffene testIDs: `TODO_TESTIDS.md`.
@ -237,9 +252,55 @@ Bricht wenn Header-Layout geaendert wird. Betroffene testIDs: `TODO_TESTIDS.md`.
nicht interagieren — Flow koennte an diesem Step haengen. Wenn `settings/dark-theme.yaml` immer nicht interagieren — Flow koennte an diesem Step haengen. Wenn `settings/dark-theme.yaml` immer
an "Systemstandard" haengt: bekanntes Problem, kein Maestro-Bug, sondern iOS-Restriktion. an "Systemstandard" haengt: bekanntes Problem, kein Maestro-Bug, sondern iOS-Restriktion.
**SCAFFOLD ONLY** = Flow ist dokumentiertes Intent, laeuft aber nicht im CI durch ohne
externe Infrastruktur (2. Device, API-Trigger). Nicht in den default Run aufnehmen bis Phase B.
--- ---
## 9. Phase B — Maestro Cloud (Zukunft, post-TestFlight) ## 9. Neue Env-Vars fuer Phase-A+-Flows
Bestehende Vars (`E2E_TEST_USER`, `E2E_TEST_PASSWORD`) werden weiterhin genutzt.
Zusaetzlich noetig fuer neue Flows:
| Var | Flow | Beschreibung |
|---|---|---|
| `E2E_TEST_PEER_NICKNAME` | `dm/send-message.yaml`, `dm/realtime-receive.yaml`, `stress/dm-scroll-performance.yaml` | Nickname des DM-Peers im Staging-Account des Test-Users (z.B. ein zweites CI-Test-Konto) |
Empfehlung: `claude-android-test` als Test-User + ein zweites Konto `claude-android-test-peer`
anlegen via Service-Role. Beide als DM-Kontakt seeden (ein manueller DM loest das).
---
## 10. Run-Befehle Phase A+
Nur stabile Flows (alles ausser SCAFFOLD ONLY):
```bash
maestro test \
--env=E2E_TEST_USER=claude-android-test \
--env=E2E_TEST_PASSWORD=<passwort> \
--env=E2E_TEST_PEER_NICKNAME=<peer-nickname> \
apps/rebreak-native/.maestro/onboarding/ \
apps/rebreak-native/.maestro/sos/ \
apps/rebreak-native/.maestro/blocker/ \
apps/rebreak-native/.maestro/calls/ \
apps/rebreak-native/.maestro/dm/send-message.yaml \
apps/rebreak-native/.maestro/stress/rapid-post-submit.yaml
```
Stress-Tests separat (laenger, geraete-abhaengig):
```bash
maestro test \
--env=E2E_TEST_USER=claude-android-test \
--env=E2E_TEST_PASSWORD=<passwort> \
--env=E2E_TEST_PEER_NICKNAME=<peer-nickname> \
apps/rebreak-native/.maestro/stress/
```
---
## 11. Phase B — Maestro Cloud (Zukunft, post-TestFlight)
Was bei Cloud-Wechsel geaendert werden muss: Was bei Cloud-Wechsel geaendert werden muss:

View File

@ -0,0 +1,101 @@
# blocker/activation-smoke.yaml
#
# Journey: Login → navigate to Blocker tab → assert protection card is visible →
# assert activation CTA or locked state is visible (state-dependent).
#
# What this covers:
# - Blocker tab reachable via bottom tabs
# - Protection card renders correctly (active OR inactive state)
# - LayerSwitchCard visible for the logged-in user's plan
# - The "Schutz aktivieren" CTA reachable (if filter is currently off)
#
# What this does NOT cover:
# - Actually tapping "Schutz aktivieren" — this triggers a native iOS/Android
# system permission dialog that Maestro cannot interact with reliably.
# That step requires a pre-enrolled device with permissions already granted.
# - Cooldown flow — requires prior activation + 24h state.
# - MDM-managed state — requires supervised device.
#
# Selector strategy:
# - Tab bar uses react-native-bottom-tabs (iOS native tab bar).
# Tabs are identified by their label text from de.json: tabs.blocker = "Blocker"
# - Protection card title: blocker.protection_card_title = "ReBreak-Schutz"
# - Inactive subtitle: blocker.protection_subtitle_inactive = "Tippe um den Schutz zu aktivieren"
# - Active (locked) title: blocker.protection_card_locked_title = "ReBreak-Schutz aktiv"
#
# Pre-requisite:
# - App installed. Test-user on staging.
# Env-Vars: E2E_TEST_USER, E2E_TEST_PASSWORD
appId: org.rebreak.app
---
- launchApp:
clearState: true
- waitForAnimationToEnd:
timeout: 5000
# --- Auth ---
- assertVisible:
text: "E-Mail"
- tapOn:
text: "E-Mail"
- inputText: ${E2E_TEST_USER}@rebreak.internal
- tapOn:
text: "Passwort"
- inputText: ${E2E_TEST_PASSWORD}
- tapOn:
text: "Anmelden"
- waitForAnimationToEnd:
timeout: 10000
- assertVisible:
text: "ReBreak"
# --- Navigate to Blocker tab ---
# tabs.blocker = "Blocker" (de.json line 137).
# react-native-bottom-tabs renders native iOS UITabBar. Maestro can tap tab labels.
- tapOn:
text: "Blocker"
- waitForAnimationToEnd:
timeout: 4000
# --- Assert Blocker screen has loaded ---
# blocker.title = "Blocker" (de.json line 185) — AppHeader title.
# Appears both as tab label and screen title, but after navigation the tab
# label is still there, so "Blocker" is visible at minimum once.
- assertVisible:
text: "Blocker"
# Protection card title is always shown regardless of active/inactive state.
# blocker.protection_card_title = "ReBreak-Schutz"
- assertVisible:
text: "ReBreak-Schutz"
# --- State-conditional assertion ---
# The test account may have protection active (urlFilter=true) or inactive.
# We check for EITHER the active lock title OR the inactive activation CTA.
# This makes the flow pass regardless of current filter state.
#
# If INACTIVE: blocker.protection_subtitle_inactive = "Tippe um den Schutz zu aktivieren"
# If ACTIVE (locked): blocker.protection_card_locked_title = "ReBreak-Schutz aktiv"
#
# We use assertVisible on a common element that exists in both states:
# The LayerSwitchCard is always rendered. Its URL-filter layer title:
# blocker.layers_url_filter_title = "URL-Filter"
- assertVisible:
text: "URL-Filter"
# Custom domains section is always rendered for authenticated users.
# blocker.my_filters_title = "Meine Filter" OR blocker.section_domains = "Eigene Domains"
# Using the section that appears regardless of plan tier:
# blocker.domain_section_title = "Eigene Domains"
# (scrolling may be needed if protection card is tall — attempt scroll first)
- scrollUntilVisible:
element:
text: "Eigene Domains"
direction: DOWN
timeout: 5000
- assertVisible:
text: "Eigene Domains"

View File

@ -0,0 +1,96 @@
# blocker/vpn-activate-verify.yaml
#
# Zweck: VERIFIZIERT den Android-16-FGS-Fix (systemExempted → specialUse).
# Loggt ein → Blocker → tippt WIRKLICH "VPN aktivieren" → Confirm-Sheet →
# System-VPN-Dialog "OK". Das löst RebreakVpnService.startForeground aus —
# genau die Stelle, die auf Android 16 mit systemExempted crashte.
#
# Verdikt kommt aus parallelem logcat (extern):
# FEHLER → "SecurityException: Starting FGS with type ..." (Fix kaputt)
# ERFOLG → kein FGS-SecurityException + "VPN established" / TUN aktiv
#
# Voraussetzung:
# - Android-16-Emulator (API 36.x) ODER echtes Gerät, App (Debug) installiert.
# - Test-Account auf Staging mit Schutz AUS (sonst kein "VPN aktivieren"-Button).
# - Env: E2E_TEST_USER, E2E_TEST_PASSWORD (via `infisical run -- maestro test ...`).
#
# Run:
# infisical run -- maestro test --env=E2E_LOCALE=de \
# apps/rebreak-native/.maestro/blocker/vpn-activate-verify.yaml
appId: org.rebreak.app
---
- launchApp:
clearState: true
- waitForAnimationToEnd:
timeout: 5000
# --- Login ---
- assertVisible:
text: "E-Mail"
- tapOn:
text: "E-Mail"
- inputText: ${E2E_TEST_USER}@rebreak.internal
- tapOn:
text: "Passwort"
- inputText: ${E2E_TEST_PASSWORD}
- tapOn:
text: "Anmelden"
- waitForAnimationToEnd:
timeout: 10000
- assertVisible:
text: "ReBreak"
# --- Blocker-Tab ---
- tapOn:
text: "Blocker"
- waitForAnimationToEnd:
timeout: 4000
- assertVisible:
text: "ReBreak-Schutz"
# Schutz ggf. erst antippen (Karte), damit der Setup-Flow erscheint.
- tapOn:
text: "Tippe um den Schutz zu aktivieren"
optional: true
- waitForAnimationToEnd:
timeout: 2000
# --- VPN aktivieren (DAS löst startForeground aus) ---
- tapOn:
text: "VPN aktivieren"
- waitForAnimationToEnd:
timeout: 2000
# Confirm-Sheet (Anti-Blind-Klick-Gate): Häkchen + Weiter. Optional, falls der
# Blocker-Screen anders gated als das Onboarding.
- tapOn:
text: "Ich habs verstanden"
optional: true
- tapOn:
text: "Weiter"
optional: true
- waitForAnimationToEnd:
timeout: 2000
# --- System-VPN-Consent-Dialog (außerhalb der App) ---
# Android zeigt "Verbindungsanfrage … VPN einrichten?". Button-Text variiert
# (OK / Zulassen / Allow) — wir versuchen mehrere.
- tapOn:
text: "OK"
optional: true
- tapOn:
text: "Zulassen"
optional: true
- tapOn:
text: "Allow"
optional: true
# startVpn() läuft jetzt: establish() → startForeground(specialUse). Kurz warten,
# damit der FGS-Aufruf im logcat landet.
- waitForAnimationToEnd:
timeout: 6000
# App lebt noch (kein Crash) → Blocker-Screen weiterhin sichtbar.
- assertVisible:
text: "ReBreak-Schutz"

View File

@ -0,0 +1,117 @@
# calls/incoming-call-screen.yaml
#
# Tests: CallScreen renders correctly for an INCOMING call state.
#
# Context: A series of bugs were fixed around the call screen disappearing
# immediately after appearing (Phantom-Call-Bug, stale PKPushRegistry, race
# conditions). This flow verifies the call UI stays visible and the
# accept/decline buttons are present.
#
# LIMITATION — This is a UI state verification, not a live call test.
# Triggering a real VoIP push requires a second device + server-side signaling.
# That E2E scenario (two-device call) needs the Maestro multi-device capability
# and is NOT covered here (Phase B / Cloud).
#
# APPROACH for single-device verification:
# The CallScreen renders based on useCallStore state. To reach it in a test:
# 1. Navigate to a DM with the test peer.
# 2. Tap the call button in the DM header.
# This triggers an OUTGOING call state → router.push('/call') → CallScreen.
#
# We verify:
# - The /call screen navigates to and stays visible (>250ms grace period passes).
# - The peer name area and call status subtitle render.
# - The hang-up button is present (for outgoing/connecting state).
#
# For incoming call UI verification (accept/decline buttons), this currently
# requires a real second device triggering the VoIP push or a debug-trigger
# mechanism. BLOCKED: needs test infrastructure decision from Orchestrator.
# See notes at bottom.
#
# Pre-requisite:
# - App installed. Test-user exists. A second user (the "test peer") must
# exist in the staging DB and be a DM contact of the test user.
# - E2E_TEST_PEER_USER env var = nickname or user lookup string for the peer.
# Env-Vars: E2E_TEST_USER, E2E_TEST_PASSWORD, E2E_TEST_PEER_USER (optional)
appId: org.rebreak.app
---
- launchApp:
clearState: true
- waitForAnimationToEnd:
timeout: 5000
# --- Auth ---
- assertVisible:
text: "E-Mail"
- tapOn:
text: "E-Mail"
- inputText: ${E2E_TEST_USER}@rebreak.internal
- tapOn:
text: "Passwort"
- inputText: ${E2E_TEST_PASSWORD}
- tapOn:
text: "Anmelden"
- waitForAnimationToEnd:
timeout: 10000
- assertVisible:
text: "ReBreak"
# --- Navigate to Chat tab ---
# tabs.chat = "Chat" (de.json line 135)
- tapOn:
text: "Chat"
- waitForAnimationToEnd:
timeout: 3000
# chat.title = "Chat" — screen is loaded
- assertVisible:
text: "Chat"
# chat.dms = "Direktnachrichten" (de.json line 1065) — section header in chat list
- assertVisible:
text: "Direktnachrichten"
# --- Open a DM conversation ---
# We need to tap into an existing DM conversation. The test user should have
# at least one prior DM with a peer from staging setup.
# The peer's nickname is unknown at flow-write time — we scroll to find any
# DM entry and tap it. If no DMs exist, the flow will fail at assertVisible
# on the DM screen elements, clearly indicating the setup gap.
#
# If E2E_TEST_PEER_USER is set, tap on that user's name directly.
# Otherwise: tap first visible DM entry below the "Direktnachrichten" header.
# FRAGILE: no testID on DM list items. The DM list shows peer nicknames as
# the primary text in each row — we use text match on the first visible name.
# For now we skip to the PARTIAL SMOKE TEST below.
# PARTIAL SMOKE (does not require an existing DM):
# Navigate to the Chat tab and verify the call infrastructure is present
# by checking that the Chat screen renders without crash. Full call E2E needs
# a pre-seeded DM contact. This is noted as a BLOCKER below.
# --- BLOCKER NOTE ---
# Full incoming call test requires one of:
# A) Two-device Maestro run (Maestro Cloud multi-device matrix) — Phase B.
# B) A debug endpoint `POST /api/debug/trigger-incoming-call` that sets
# callStore state directly (requires dev-build + debug flag). Not yet built.
# C) The DM screen has a "call" button in the header → tap → outgoing call state
# → CallScreen navigates in → verify screen stays open ≥ 2s (race guard).
# This requires a known peer userId in the DM URL param.
#
# Escalate to Orchestrator: Which approach for call E2E? Proceed with option C?
# That requires E2E_TEST_PEER_USER_ID env var + the peer having a DM row
# already loaded in the Chat list.
# Verify chat tab renders cleanly (no crash = passing for now):
- assertVisible:
text: "Direktnachrichten"
# Also verify the call-related strings are not showing error states.
# chat.call_missed = "Verpasster Anruf" should NOT appear without a real missed call.
# (This is a sanity check — it fires only if the chat list pre-populates
# a missed call notification for the test user incorrectly.)
- assertNotVisible:
text: "Verpasster Anruf"

View File

@ -0,0 +1,84 @@
# dm/realtime-receive.yaml
#
# Journey: Login → open a DM → wait for an incoming message from the peer →
# assert the message bubble appears WITHOUT manual scroll.
#
# What this covers:
# - Realtime Supabase subscription (useDmRealtime) correctly fires on INSERT
# - Incoming message is appended to the local messages list
# - FlatList auto-scrolls to show the new message (scrollToBottom is called)
# - No "unread / scroll down" lag visible
#
# LIMITATION: This flow requires an EXTERNAL trigger — the test peer must
# send a message while the flow is waiting. This is not automatable with a
# single Maestro device run.
#
# Approaches:
# A) Manual: Run flow, then manually send a message from a second device
# within the 30s wait window. Not suitable for CI.
# B) API-trigger: A helper script sends a message via
# POST /api/dm/send (Service-Role) just before the flow reaches
# the waiting step. Needs coordination outside Maestro.
# C) Two-device Maestro Cloud: Device A runs dm/send-message.yaml,
# Device B runs this flow simultaneously. Phase B / Cloud.
#
# Current status: SCAFFOLD ONLY — assertVisible for incoming message
# will always timeout unless external trigger is set up.
# Leaving this flow as documented intent for Phase B.
#
# Env-Vars: E2E_TEST_USER, E2E_TEST_PASSWORD
# Optional: E2E_TEST_PEER_NICKNAME
appId: org.rebreak.app
---
- launchApp:
clearState: true
- waitForAnimationToEnd:
timeout: 5000
# --- Auth ---
- assertVisible:
text: "E-Mail"
- tapOn:
text: "E-Mail"
- inputText: ${E2E_TEST_USER}@rebreak.internal
- tapOn:
text: "Passwort"
- inputText: ${E2E_TEST_PASSWORD}
- tapOn:
text: "Anmelden"
- waitForAnimationToEnd:
timeout: 10000
- assertVisible:
text: "ReBreak"
# --- Navigate to Chat → open DM ---
- tapOn:
text: "Chat"
- waitForAnimationToEnd:
timeout: 3000
- assertVisible:
text: "Direktnachrichten"
- tapOn:
text: ${E2E_TEST_PEER_NICKNAME}
- waitForAnimationToEnd:
timeout: 4000
- assertVisible:
text: "Nachricht schreiben…"
# --- Wait for incoming realtime message ---
# External trigger must fire within this window.
# The [E2E-INCOMING] prefix is used by the helper script to identify
# test messages for cleanup after the run.
- waitForAnimationToEnd:
timeout: 30000
# Assert the incoming message bubble appeared.
# WILL FAIL without external trigger — intentional for Phase B scaffolding.
- assertVisible:
text: "[E2E-INCOMING]"

View File

@ -0,0 +1,102 @@
# dm/send-message.yaml
#
# Journey: Login → Chat tab → open first DM conversation → type a message →
# send → assert message appears in bubble list.
#
# What this covers:
# - DM screen loads without crash
# - Text input is accessible and accepts input
# - Send button (or Return key) dispatches the message
# - Optimistic message bubble appears immediately after send
# - (Realtime delivery to a second device is NOT tested here — single device)
#
# Pre-requisite:
# - Test-user has at least one existing DM conversation on staging.
# - The DM partner must exist (any user — staging DB seeded).
# - If no existing DM: create one manually first via the app, then run flow.
#
# IMPORTANT: This flow requires the test user to have a pre-existing DM.
# If the chat list is empty, the flow will correctly fail at "Direktnachrichten"
# being visible but no conversation entry to tap — making the setup gap obvious.
#
# Selector notes:
# - DM list items: no testID. Matched by peer nickname text.
# If E2E_TEST_PEER_NICKNAME is set, we tap that. Otherwise: first item below
# "Direktnachrichten" label. THIS IS FRAGILE.
# - DM screen text input: no testID. chat.placeholder = "Nachricht schreiben…"
# - Send button: no testID. Using Return key (onSubmitEditing) as fallback.
#
# Env-Vars: E2E_TEST_USER, E2E_TEST_PASSWORD
# Optional: E2E_TEST_PEER_NICKNAME (if set, tapped directly; else first DM item)
appId: org.rebreak.app
---
- launchApp:
clearState: true
- waitForAnimationToEnd:
timeout: 5000
# --- Auth ---
- assertVisible:
text: "E-Mail"
- tapOn:
text: "E-Mail"
- inputText: ${E2E_TEST_USER}@rebreak.internal
- tapOn:
text: "Passwort"
- inputText: ${E2E_TEST_PASSWORD}
- tapOn:
text: "Anmelden"
- waitForAnimationToEnd:
timeout: 10000
- assertVisible:
text: "ReBreak"
# --- Navigate to Chat tab ---
- tapOn:
text: "Chat"
- waitForAnimationToEnd:
timeout: 3000
- assertVisible:
text: "Direktnachrichten"
# --- Open DM conversation ---
# Tap into an existing DM. We need a peer to tap on.
# If E2E_TEST_PEER_NICKNAME env var is set, use it as the tap target.
# This is the cleanest path — set this var to the nickname of the staging peer.
- tapOn:
text: ${E2E_TEST_PEER_NICKNAME}
- waitForAnimationToEnd:
timeout: 4000
# DM screen loaded: partner name shown in header (AppHeader title).
# We assert the message input placeholder is visible.
# chat.placeholder = "Nachricht schreiben…" (de.json line 1072)
- assertVisible:
text: "Nachricht schreiben…"
# --- Compose and send a message ---
- tapOn:
text: "Nachricht schreiben…"
- waitForAnimationToEnd:
timeout: 1000
- inputText: "[E2E] Maestro-Testnachricht"
# Submit via Return key. dm.tsx TextInput has onSubmitEditing={handleSend}.
# This avoids needing testID on the send Pressable (currently missing).
- pressKey: Return
- waitForAnimationToEnd:
timeout: 5000
# After send: the optimistic message bubble appears immediately with the sent text.
# The bubble shows the message content. Assert the text is visible in the list.
- assertVisible:
text: "[E2E] Maestro-Testnachricht"
# Input bar resets to empty — placeholder is visible again.
- assertVisible:
text: "Nachricht schreiben…"

View File

@ -0,0 +1,92 @@
# onboarding/new-user-streak-guard.yaml
#
# REGRESSION GUARD for the current bug:
# current_days=0 for all users because the streak calculation never runs.
#
# Journey: Launch → Welcome → Privacy → Nickname (skip via DiGA-No path) →
# Plan (Pro, Dev-Trial-CTA) → Payment (Dev-Skip) → Protection (skip
# via CTABar if dialog appears) → Done → App Home → Home screen
# must show streak day > 0 (NOT "0 Tage clean" or "Starte deinen ersten Tag"
# when the test user already has a streak).
#
# Strategy: The test user (claude-android-test) has an existing account with
# onboardingStep='done' so onboarding will NOT replay — _layout.tsx
# routes directly to /(app). We assert the streak widget shows a
# positive day count as the regression guard.
# For a brand new user test, a separate dedicated account is needed
# (see BLOCKER in notes at bottom). This flow covers the post-onboarding
# regression scenario.
#
# Pre-requisite:
# - App installed (Dev-Build OR TestFlight).
# - E2E_TEST_USER account on staging with onboardingStep='done' and streak_start set.
# - Staging backend running.
# Env-Vars: E2E_TEST_USER, E2E_TEST_PASSWORD
#
# Expected outcome: After login, the home screen streak widget shows a day count
# that is NOT "0 Tage" (i.e. current_days is being calculated).
# If it shows "0 Tage clean" or "Starte deinen ersten Tag" when
# the account has a real streak — the regression is present.
#
# IMPORTANT: Text selectors use hardcoded de.json values. If device locale ≠ 'de',
# run with a German locale device or force-set language to 'de' before test.
appId: org.rebreak.app
---
- launchApp:
clearState: true
- waitForAnimationToEnd:
timeout: 5000
# --- Auth ---
- assertVisible:
text: "E-Mail"
- tapOn:
text: "E-Mail"
- inputText: ${E2E_TEST_USER}@rebreak.internal
- tapOn:
text: "Passwort"
- inputText: ${E2E_TEST_PASSWORD}
- tapOn:
text: "Anmelden"
- waitForAnimationToEnd:
timeout: 10000
# --- Home screen reached ---
- assertVisible:
text: "ReBreak"
# The streak widget is in the Home screen (app/index.tsx or a HomeStreakCard).
# home.streak_days_one = "Tag clean"
# home.streak_days_other = "Tage clean"
# home.streak_start = "Starte deinen ersten Tag"
#
# REGRESSION CHECK: if current_days is never calculated, the widget always shows
# "0 Tage clean". We assert "Tage clean" is visible (which is true for any count
# including 0) and additionally assert "Starte deinen ersten Tag" is NOT visible
# (which would only appear for day=0 / null streak start).
#
# This catches the "always 0" bug: when current_days=0 the UI renders
# "0 Tage clean" or "Starte deinen ersten Tag" even for users with real streaks.
- assertVisible:
text: "clean"
# The key assertion: "Starte deinen ersten Tag" must NOT be visible for an
# established test-user. If this fires — the streak calculation bug is present.
- assertNotVisible:
text: "Starte deinen ersten Tag"
# Secondary guard: assert that "0 Tage" is not the streak count.
# "0 Tage clean" is the de.json rendering for 0 days via streak_days_other.
# NOTE: Maestro assertNotVisible matches substring — "0 Tage" catches both
# "0 Tage clean" (streak_days_other) and any "0 Tag clean" variant.
- assertNotVisible:
text: "0 Tage"
# NOTE FOR FULL ONBOARDING TEST (new account flow):
# A completely fresh account (onboardingStep='welcome') is needed to exercise
# the Welcome → Privacy → Nickname → Plan → Payment → Protection → Done flow.
# That requires a separate test account that is reset to onboardingStep='welcome'
# before each run (via Service-Role PATCH). This is not automated here yet.
# See TODO in SETUP.md Phase B.

View File

@ -0,0 +1,36 @@
# screens/01-onboarding.yaml
#
# Marketing-Screenshot 01: Onboarding / Willkommen
#
# Ziel: Der erste Onboarding-Screen (welcome-Step) — ruhiger Einstieg, kein Login-Formular.
#
# Strategie: clearState entfernt Auth-Session UND MMKV-State. Wenn der Test-Account
# onboardingStep='done' hat, landet er direkt im Auth-Screen (signin.tsx), nicht im
# Onboarding-Wizard. Für einen echten Onboarding-Welcome-Screen braucht man einen
# frischen Account mit onboardingStep='welcome'.
#
# Fallback: Wir landen auf dem Sign-in-Screen — der ist der tatsächliche "erste Blick"
# für neue Nutzer und zeigt schon das Rebreak-Branding sauber. Er dient hier als
# Onboarding-Äquivalent bis ein Reset-API-Endpunkt verfügbar ist.
#
# Screenshot-Output: screenshots/01-onboarding.png
# Env-Vars: keine (kein Login nötig)
appId: org.rebreak.app
---
- launchApp:
clearState: true
- waitForAnimationToEnd:
timeout: 6000
# Sign-in Screen ist der erste Screen für neue User.
# Wir prüfen ob wir direkt auf dem Auth-Screen sind (clearState = kein Token).
- assertVisible:
text: "E-Mail"
# Auf Marketing-Screenshot warten wir extra lang damit alle Animationen/Fonts laden
- waitForAnimationToEnd:
timeout: 2000
- takeScreenshot: screenshots/01-onboarding

View File

@ -0,0 +1,56 @@
# screens/02-blocker.yaml
#
# Marketing-Screenshot 02: Schutz / Blocker aktiv
#
# Ziel: Blocker-Tab mit Schutz-Status. Zeigt "ReBreak-Schutz" Card + URL-Filter Layer.
# Der Test-Account sollte den Blocker in aktivem Zustand haben für den besten Screenshot.
# Wenn inaktiv: Card zeigt "Schutz aktivieren" — immer noch verwertbar.
#
# Screenshot-Output: screenshots/02-blocker.png
# Env-Vars: E2E_TEST_USER, E2E_TEST_PASSWORD
appId: org.rebreak.app
---
- launchApp:
clearState: true
- waitForAnimationToEnd:
timeout: 5000
# --- Auth ---
- assertVisible:
text: "E-Mail"
- tapOn:
text: "E-Mail"
- inputText: ${E2E_TEST_USER}@rebreak.internal
- tapOn:
text: "Passwort"
- inputText: ${E2E_TEST_PASSWORD}
- tapOn:
text: "Anmelden"
- waitForAnimationToEnd:
timeout: 10000
- assertVisible:
text: "ReBreak"
# --- Blocker Tab ---
- tapOn:
text: "Blocker"
- waitForAnimationToEnd:
timeout: 4000
- assertVisible:
text: "ReBreak-Schutz"
# Scrollen damit Schutz-Card + URL-Filter gleichzeitig sichtbar sind
- scrollUntilVisible:
element:
text: "URL-Filter"
direction: DOWN
timeout: 5000
- waitForAnimationToEnd:
timeout: 1000
- takeScreenshot: screenshots/02-blocker

View File

@ -0,0 +1,65 @@
# screens/03-blocked.yaml
#
# Marketing-Screenshot 03: Block-Screen (gesperrte Casino-Seite)
#
# WICHTIG: Dieser Screen ist NICHT direkt per Maestro-Flow erzeugbar.
# Der Block-Screen erscheint im System-Browser (Safari / Chrome) wenn das VPN-DNS
# eine Domain blockiert und zur Rebreak-Blockpage weiterleitet — außerhalb der App.
# Maestro kann keine externen Browser-Fenster oder System-Level-UI öffnen.
#
# OPTIONEN für diesen Screenshot (manuell):
# A) VPN aktiv auf Simulator/Device → Safari öffnen → casino.com aufrufen →
# Screenshot via Cmd+S (Simulator) oder Seitenknopf+Lautstärke (Device).
# B) In-App-Blocked-Webview: Falls die App einen eigenen WebView mit Block-Page hat
# (z.B. app/help/crisis.tsx oder eine BlockedScreen-Komponente), diesen ansteuern.
# C) Platzhalter akzeptieren bis manuell erstellt.
#
# Dieser Flow versucht Option B: Öffnen des Help-Screens als nächstbestes sichtbares
# "Schutz greift ein"-Äquivalent.
#
# Screenshot-Output: screenshots/03-blocked.png
# Env-Vars: E2E_TEST_USER, E2E_TEST_PASSWORD
#
# STATUS: MANUELL — Flow läuft durch, macht Screenshot des Help/Crisis-Screens als
# Proxy. Echter Block-Screen muss manuell via Safari+aktivem VPN erstellt werden.
appId: org.rebreak.app
---
- launchApp:
clearState: true
- waitForAnimationToEnd:
timeout: 5000
# --- Auth ---
- assertVisible:
text: "E-Mail"
- tapOn:
text: "E-Mail"
- inputText: ${E2E_TEST_USER}@rebreak.internal
- tapOn:
text: "Passwort"
- inputText: ${E2E_TEST_PASSWORD}
- tapOn:
text: "Anmelden"
- waitForAnimationToEnd:
timeout: 10000
- assertVisible:
text: "ReBreak"
# Header-Dropdown öffnen für Zugriff auf Hilfe-Menü
# FRAGILE: Koordinaten-Tap — ersetzen durch testID="header-avatar-btn" wenn vorhanden
- tapOn:
id: "header-avatar-btn"
- waitForAnimationToEnd:
timeout: 2000
# Falls Dropdown kein direktes "Hilfe" hat: Abbruch und Screenshot des Dropdowns selbst
# Das Dropdown zeigt immerhin den "SOS"-Eintrag — nahegelegener Kontext
- assertVisible:
text: "SOS"
# Screenshot des Dropdown-States als Proxy für "Schutz ist da"
# Für echten Block-Screen: MANUELL via Safari mit aktivem VPN
- takeScreenshot: screenshots/03-blocked

View File

@ -0,0 +1,81 @@
# screens/04-sos-lyra.yaml
#
# Marketing-Screenshot 04: SOS / Chat mit Lyra
#
# Ziel: Lyra-Chat-Screen im SOS-Modus mit sichtbarer Nachricht und Chip-Row.
# Für vorzeigbaren Marketing-Screenshot: nach Lyra-Antwort warten, dann Screenshot.
# Die Chips ("Atemübung" etc.) müssen sichtbar sein — das beweist Live-Lyra.
#
# Timing: Groq cold start auf Staging = 612s. 20s-Wait für Response inklusive.
#
# Screenshot-Output: screenshots/04-sos-lyra.png
# Env-Vars: E2E_TEST_USER, E2E_TEST_PASSWORD
appId: org.rebreak.app
---
- launchApp:
clearState: true
- waitForAnimationToEnd:
timeout: 5000
# --- Auth ---
- assertVisible:
text: "E-Mail"
- tapOn:
text: "E-Mail"
- inputText: ${E2E_TEST_USER}@rebreak.internal
- tapOn:
text: "Passwort"
- inputText: ${E2E_TEST_PASSWORD}
- tapOn:
text: "Anmelden"
- waitForAnimationToEnd:
timeout: 10000
- assertVisible:
text: "ReBreak"
# --- SOS öffnen via Header-Dropdown ---
# FRAGILE: Avatar-Button hat kein testID → Koordinaten-Tap
# Ersetzen mit: tapOn: { id: "header-avatar-btn" } nach testID-Ergänzung
- tapOn:
id: "header-avatar-btn"
- waitForAnimationToEnd:
timeout: 2000
- assertVisible:
text: "SOS"
- tapOn:
text: "SOS"
# SOS-Screen startet RiveAvatar + Lyra-Streaming
- waitForAnimationToEnd:
timeout: 12000
- assertVisible:
text: "Was beschäftigt dich?"
# Erste Lyra-Nachricht schon sichtbar → Screenshot der leeren/Start-State für clean look
# Für vorzeigbareren Shot: eine Nachricht senden und auf Antwort warten
- tapOn:
text: "Was beschäftigt dich?"
- waitForAnimationToEnd:
timeout: 500
# Realistischer, nicht-klinischer Testtext für Marketing-Screenshot
- inputText: "Ich weiß nicht mehr weiter."
- pressKey: Return
# Warten bis Lyra antwortet und Chips erscheinen
- waitForAnimationToEnd:
timeout: 20000
# Chip "Atemübung" bestätigt aktive Lyra-Response
- assertVisible:
text: "Atemübung"
- waitForAnimationToEnd:
timeout: 1000
- takeScreenshot: screenshots/04-sos-lyra

View File

@ -0,0 +1,75 @@
# screens/05-breathing.yaml
#
# Marketing-Screenshot 05: Atemübung
#
# Ziel: BreathingDrawer geöffnet auf dem SOS-Screen.
# Der Drawer zeigt die geführte Atemübung — visuell stark, klar beruhigend.
#
# Pfad: Login → SOS → Lyra antwortet → "Atemübung"-Chip tippen → BreathingDrawer offen
#
# Screenshot-Output: screenshots/05-breathing.png
# Env-Vars: E2E_TEST_USER, E2E_TEST_PASSWORD
appId: org.rebreak.app
---
- launchApp:
clearState: true
- waitForAnimationToEnd:
timeout: 5000
# --- Auth ---
- assertVisible:
text: "E-Mail"
- tapOn:
text: "E-Mail"
- inputText: ${E2E_TEST_USER}@rebreak.internal
- tapOn:
text: "Passwort"
- inputText: ${E2E_TEST_PASSWORD}
- tapOn:
text: "Anmelden"
- waitForAnimationToEnd:
timeout: 10000
- assertVisible:
text: "ReBreak"
# --- SOS öffnen ---
# FRAGILE: Koordinaten-Tap bis testID="header-avatar-btn" gesetzt ist
- tapOn:
id: "header-avatar-btn"
- waitForAnimationToEnd:
timeout: 2000
- assertVisible:
text: "SOS"
- tapOn:
text: "SOS"
- waitForAnimationToEnd:
timeout: 12000
- assertVisible:
text: "Was beschäftigt dich?"
# "Atemübung" ist immer im initialen Chip-Set (CHIP_SETS.start in sosConstants.ts)
# Keine eigene Nachricht nötig — die Chips erscheinen beim Start des SOS-Screens
# sobald Lyra die erste Begrüßung gestreamt hat
- assertVisible:
text: "Atemübung"
# BreathingDrawer öffnen
- tapOn:
text: "Atemübung"
- waitForAnimationToEnd:
timeout: 3000
# BreathingDrawer ist offen: Header "Atemübung" sichtbar
- assertVisible:
text: "Atemübung"
- waitForAnimationToEnd:
timeout: 1500
- takeScreenshot: screenshots/05-breathing

View File

@ -0,0 +1,54 @@
# screens/06-mail-schutz.yaml
#
# Marketing-Screenshot 06: Mail-Schutz-Ansicht
#
# Ziel: Mail-Tab geöffnet. Zeigt entweder:
# - Verbundenes Postfach mit Statistiken (Mail-Schutz aktiv) — ideal
# - MailEmptyState mit "Noch keine Mails blockiert" + Connect-CTA — akzeptabel
#
# Das Mail-Tab ist über den Bottom-Tab-Bar "Mail" erreichbar.
# de.json: tabs.mail = "Mail", mail.title = "Mail-Schutz"
#
# Screenshot-Output: screenshots/06-mail-schutz.png
# Env-Vars: E2E_TEST_USER, E2E_TEST_PASSWORD
appId: org.rebreak.app
---
- launchApp:
clearState: true
- waitForAnimationToEnd:
timeout: 5000
# --- Auth ---
- assertVisible:
text: "E-Mail"
- tapOn:
text: "E-Mail"
- inputText: ${E2E_TEST_USER}@rebreak.internal
- tapOn:
text: "Passwort"
- inputText: ${E2E_TEST_PASSWORD}
- tapOn:
text: "Anmelden"
- waitForAnimationToEnd:
timeout: 10000
- assertVisible:
text: "ReBreak"
# --- Mail Tab ---
# tabs.mail = "Mail" (de.json)
- tapOn:
text: "Mail"
- waitForAnimationToEnd:
timeout: 4000
# Mail-Screen geladen: Titel "Mail-Schutz" (mail.title, de.json)
- assertVisible:
text: "Mail-Schutz"
- waitForAnimationToEnd:
timeout: 1000
- takeScreenshot: screenshots/06-mail-schutz

View File

@ -0,0 +1,60 @@
# screens/07-community.yaml
#
# Marketing-Screenshot 07: Community-Feed
#
# Ziel: Home-Feed mit ComposeCard + mindestens einigen Posts sichtbar.
# Wichtig: Posts zeigen NUR Spitznamen (Nickname), keine Klarnamen (DSGVO/Stigma-Regel).
# Der Feed muss gefüllt sein — auf Staging sollte der Test-Account Posts sehen.
#
# Screenshot-Strategie: Nach Login direkt auf Home-Feed scrollen bis Community-Posts
# sichtbar sind, dann Screenshot.
#
# Screenshot-Output: screenshots/07-community.png
# Env-Vars: E2E_TEST_USER, E2E_TEST_PASSWORD
appId: org.rebreak.app
---
- launchApp:
clearState: true
- waitForAnimationToEnd:
timeout: 5000
# --- Auth ---
- assertVisible:
text: "E-Mail"
- tapOn:
text: "E-Mail"
- inputText: ${E2E_TEST_USER}@rebreak.internal
- tapOn:
text: "Passwort"
- inputText: ${E2E_TEST_PASSWORD}
- tapOn:
text: "Anmelden"
- waitForAnimationToEnd:
timeout: 10000
- assertVisible:
text: "ReBreak"
# Home-Tab ist Default. Feed liegt direkt unter dem Header.
# Scrollen um Feed-Inhalt sichtbar zu machen (ComposeCard + Posts)
- scrollUntilVisible:
element:
text: "Was bewegt dich gerade?"
direction: UP
timeout: 4000
- assertVisible:
text: "Was bewegt dich gerade?"
# Feed etwas nach unten scrollen damit Posts sichtbar werden
# (ComposeCard ist ganz oben, Posts darunter)
- scroll:
direction: DOWN
duration: 800
- waitForAnimationToEnd:
timeout: 1500
- takeScreenshot: screenshots/07-community

View File

@ -0,0 +1,71 @@
# screens/08-streak.yaml
#
# Marketing-Screenshot 08: Streak / Profil-Stats
#
# Ziel: Profil-Screen mit StreakSection und Schutz-Abdeckungs-Donut sichtbar.
# Der Test-Account (admin) sollte eine Streak > 0 haben für vorzeigbare Zahlen.
# profile.streak_section_label = "SCHUTZ-ABDECKUNG" (de.json)
# profile.streak_days_protected = "Tage geschützt"
#
# Screenshot-Strategie: Profil öffnen, auf StreakSection scrollen.
#
# Screenshot-Output: screenshots/08-streak.png
# Env-Vars: E2E_TEST_USER, E2E_TEST_PASSWORD
appId: org.rebreak.app
---
- launchApp:
clearState: true
- waitForAnimationToEnd:
timeout: 5000
# --- Auth ---
- assertVisible:
text: "E-Mail"
- tapOn:
text: "E-Mail"
- inputText: ${E2E_TEST_USER}@rebreak.internal
- tapOn:
text: "Passwort"
- inputText: ${E2E_TEST_PASSWORD}
- tapOn:
text: "Anmelden"
- waitForAnimationToEnd:
timeout: 10000
- assertVisible:
text: "ReBreak"
# --- Profil öffnen via Header-Dropdown ---
# FRAGILE: Koordinaten-Tap bis testID="header-avatar-btn" gesetzt ist
- tapOn:
id: "header-avatar-btn"
- waitForAnimationToEnd:
timeout: 2000
- assertVisible:
text: "Profil"
- tapOn:
text: "Profil"
- waitForAnimationToEnd:
timeout: 4000
- assertVisible:
text: "Profil"
# Zur StreakSection scrollen (liegt unterhalb ProfileHeader + StatsBar)
# profile.streak_section_label = "SCHUTZ-ABDECKUNG"
- scrollUntilVisible:
element:
text: "SCHUTZ-ABDECKUNG"
direction: DOWN
timeout: 6000
- assertVisible:
text: "SCHUTZ-ABDECKUNG"
- waitForAnimationToEnd:
timeout: 1000
- takeScreenshot: screenshots/08-streak

View File

@ -0,0 +1,81 @@
# screens/09-geraete.yaml
#
# Marketing-Screenshot 09: Geräte-Übersicht (devices.tsx)
#
# Ziel: Devices-Screen mit mindestens dem aktuellen Gerät sichtbar.
# devices.section_title_this = "Dieses Gerät"
# devices.status_active = "Aktiv"
# Ideal für Legend-Account: mehrere Gerätetypen (iPhone, Mac, Android) sichtbar.
#
# Navigation: Login → Header-Dropdown → "Einstellungen" → Devices-Screen
# ODER: Direktnavigation wenn "Geräte" im Dropdown erreichbar.
# Actual route: app/devices.tsx — wird über HeaderDropdownMenu erreichbar.
# Prüfen ob es im Dropdown oder in Settings liegt.
#
# Fallback: AppHeader der devices.tsx zeigt title aus t('settings.devices_title')
# settings.devices_title = "Meine Geräte" (check de.json)
#
# Screenshot-Output: screenshots/09-geraete.png
# Env-Vars: E2E_TEST_USER, E2E_TEST_PASSWORD
appId: org.rebreak.app
---
- launchApp:
clearState: true
- waitForAnimationToEnd:
timeout: 5000
# --- Auth ---
- assertVisible:
text: "E-Mail"
- tapOn:
text: "E-Mail"
- inputText: ${E2E_TEST_USER}@rebreak.internal
- tapOn:
text: "Passwort"
- inputText: ${E2E_TEST_PASSWORD}
- tapOn:
text: "Anmelden"
- waitForAnimationToEnd:
timeout: 10000
- assertVisible:
text: "ReBreak"
# --- Profil öffnen um zu Einstellungen/Geräten zu navigieren ---
# FRAGILE: Koordinaten-Tap bis testID="header-avatar-btn" gesetzt ist
- tapOn:
id: "header-avatar-btn"
- waitForAnimationToEnd:
timeout: 2000
# Einstellungen öffnen: appHeader.settings = "Einstellungen" (de.json)
- assertVisible:
text: "Einstellungen"
- tapOn:
text: "Einstellungen"
- waitForAnimationToEnd:
timeout: 3000
# Settings-Screen: "Meine Geräte" Row oder "Geräte" Abschnitt suchen
# Scrolle bis Geräte-Eintrag sichtbar ist
- scrollUntilVisible:
element:
text: "Geräte"
direction: DOWN
timeout: 5000
- tapOn:
text: "Geräte"
- waitForAnimationToEnd:
timeout: 4000
# Devices-Screen: "Dieses Gerät" Section
- assertVisible:
text: "Dieses Gerät"
- waitForAnimationToEnd:
timeout: 1000
- takeScreenshot: screenshots/09-geraete

View File

@ -0,0 +1,85 @@
# screens/capture-05-09-verify.yaml
#
# No-Login: nimmt die zwei fehlenden Marketing-Screenshots (05-breathing,
# 09-geraete) auf UND verifiziert den Protection-Bypass-Fix (Blocker-Tab darf
# das "Schutz ist aus"-Sheet NICHT mehr zeigen).
# Voraussetzung: App auf dem Sim BEREITS eingeloggt. KEIN clearState.
# Jeder Block eigener launchApp → unabhängig + robust.
appId: org.rebreak.app
---
# ── 09 — DEVICES (Settings → Geräte) ──
- launchApp
- waitForAnimationToEnd:
timeout: 6000
- tapOn:
text: "Nicht erlauben"
optional: true
- tapOn:
text: "Später"
optional: true
- tapOn:
id: "header-avatar-btn"
optional: true
- waitForAnimationToEnd:
timeout: 1500
- tapOn:
text: "Einstellungen"
optional: true
- waitForAnimationToEnd:
timeout: 2500
- scrollUntilVisible:
element:
text: "Geräte"
direction: DOWN
timeout: 6000
optional: true
- tapOn:
text: "Geräte"
optional: true
- waitForAnimationToEnd:
timeout: 3000
- takeScreenshot: screenshots/09-geraete
# ── 05 — BREATHING (SOS → Atemübung-Chip → BreathingDrawer) ──
- launchApp
- waitForAnimationToEnd:
timeout: 5000
- tapOn:
text: "Nicht erlauben"
optional: true
- tapOn:
text: "Später"
optional: true
- tapOn:
id: "header-avatar-btn"
optional: true
- waitForAnimationToEnd:
timeout: 1500
- tapOn:
text: "SOS"
optional: true
- waitForAnimationToEnd:
timeout: 12000
- tapOn:
text: "Atemübung"
optional: true
- waitForAnimationToEnd:
timeout: 3500
- takeScreenshot: screenshots/05-breathing
# ── BLOCKER (Fix-Verify: KEIN "Später"-Tap → Sheet würde sichtbar sein) ──
- launchApp
- waitForAnimationToEnd:
timeout: 5000
- tapOn:
text: "Nicht erlauben"
optional: true
- tapOn:
text: "Blocker"
optional: true
- waitForAnimationToEnd:
timeout: 3500
- takeScreenshot: screenshots/zz-blocker-verify

View File

@ -0,0 +1,67 @@
# screens/capture-05-breathing.yaml
# launchApp → Header-Avatar → SOS (Koordinaten-Tap) → 3 Nachrichten an Lyra
# (ab Turn 3 erzwingt System-Hint + Fallback-Chips das "🫁 Atemübung"-Angebot)
# → Atemübung-Chip → BreathingDrawer. KEIN clearState.
appId: org.rebreak.app
---
- launchApp
- waitForAnimationToEnd:
timeout: 6000
- tapOn:
text: "Nicht erlauben"
optional: true
- tapOn:
text: "Später"
optional: true
- tapOn:
id: "header-avatar-btn"
optional: true
- waitForAnimationToEnd:
timeout: 2000
- tapOn:
point: "72%, 22%"
- waitForAnimationToEnd:
timeout: 9000
# ── Turn 1 ──
- tapOn:
text: "Was beschäftigt dich?"
optional: true
- inputText: "Ich bin gerade sehr gestresst und der Druck ist groß"
- tapOn:
id: "sos-send-btn"
optional: true
- waitForAnimationToEnd:
timeout: 11000
# ── Turn 2 ──
- tapOn:
text: "Was beschäftigt dich?"
optional: true
- inputText: "Es fällt mir gerade schwer, dem Drang zu widerstehen"
- tapOn:
id: "sos-send-btn"
optional: true
- waitForAnimationToEnd:
timeout: 11000
# ── Turn 3 ──
- tapOn:
text: "Was beschäftigt dich?"
optional: true
- inputText: "Ich brauche jetzt Hilfe, um runterzukommen"
- tapOn:
id: "sos-send-btn"
optional: true
- waitForAnimationToEnd:
timeout: 11000
- takeScreenshot: screenshots/05b-chips
- tapOn:
text: "Atemübung"
optional: true
- waitForAnimationToEnd:
timeout: 4000
- takeScreenshot: screenshots/05-breathing

View File

@ -0,0 +1,77 @@
#!/usr/bin/env bash
#
# No-Login-Capture für die Marketing-Screenshots.
#
# Voraussetzung: Die App ist auf dem iOS-Simulator BEREITS eingeloggt
# (z. B. via Google-OAuth manuell). Dieses Script loggt NICHT ein, es navigiert
# nur und macht Screenshots. Braucht daher KEINE Test-Creds.
#
# Erzeugt 0209 und kopiert sie nach apps/marketing/public/preview/.
#
# Aufruf:
# export PATH="$PATH:$HOME/.maestro/bin"
# bash apps/rebreak-native/.maestro/screens/capture-marketing-loggedin.sh
set -uo pipefail
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../../.." && pwd)"
FLOW="$REPO_ROOT/apps/rebreak-native/.maestro/screens/marketing-tour-loggedin.yaml"
DEST="$REPO_ROOT/apps/marketing/public/preview"
export PATH="$PATH:$HOME/.maestro/bin"
if ! command -v maestro &>/dev/null; then
echo "FEHLER: Maestro nicht im PATH. → export PATH=\"\$PATH:\$HOME/.maestro/bin\""
exit 1
fi
# ─── Status-Bar (9:41, voll) auf gebootetem Simulator ───────────────────────
UDID=$(xcrun simctl list devices booted --json 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
for r,ds in d.get('devices',{}).items():
for x in ds:
if x.get('state')=='Booted':
print(x['udid']); sys.exit()
" 2>/dev/null || true)
if [[ -n "$UDID" ]]; then
echo "Status-Bar auf Simulator $UDID setzen (9:41, voll)…"
xcrun simctl status_bar "$UDID" override \
--time "9:41" --wifiMode active --wifiBars 3 \
--cellularMode active --cellularBars 4 \
--batteryState charged --batteryLevel 100 2>/dev/null \
|| echo " Status-Bar-Override fehlgeschlagen (nicht kritisch)."
else
echo "Kein gebooteter Simulator gefunden — Status-Bar übersprungen."
fi
# ─── Flow laufen ────────────────────────────────────────────────────────────
# WICHTIG: aus REPO_ROOT starten — `takeScreenshot: screenshots/x` legt relativ
# zum Arbeitsverzeichnis ab, d.h. die PNGs landen in $REPO_ROOT/screenshots/.
echo ""
echo "Starte No-Login-Tour: $FLOW"
cd "$REPO_ROOT"
maestro test "$FLOW" || echo " (Maestro-Flow mit Fehlern beendet — vorhandene Screenshots werden trotzdem kopiert)"
# ─── Screenshots finden + kopieren ──────────────────────────────────────────
# Maestro speichert `takeScreenshot: screenshots/<name>` als $REPO_ROOT/screenshots/<name>.png
SRC="$REPO_ROOT/screenshots"
echo ""
echo "Quelle: $SRC"
mkdir -p "$DEST"
ok=0
for n in 02-blocker 03-blocked 04-sos-lyra 05-breathing 06-mail-schutz 07-community 07b-dm 08-streak 09-geraete; do
f=""
[[ -f "$SRC/$n.png" ]] && f="$SRC/$n.png"
[[ -z "$f" ]] && f=$(ls "$SRC"/*"$n"*.png 2>/dev/null | head -1 || true)
if [[ -n "$f" && -f "$f" ]]; then
cp "$f" "$DEST/$n.png" && echo "$n.png" && ok=$((ok+1))
else
echo "$n.png fehlt (Screen evtl. nicht erreicht)"
fi
done
[[ -n "$UDID" ]] && xcrun simctl status_bar "$UDID" clear 2>/dev/null || true
echo ""
echo "Fertig: $ok/8 Screenshots → $DEST"

View File

@ -0,0 +1,183 @@
#!/usr/bin/env bash
# capture-marketing.sh
#
# Erzeugt alle 9 Marketing-Screenshots und legt sie unter
# apps/marketing/public/preview/ mit den exakten Dateinamen ab.
#
# Voraussetzungen:
# 1. Maestro CLI installiert: curl -Ls "https://get.maestro.mobile.dev" | bash
# 2. iOS Simulator läuft mit der rebreak-native App (org.rebreak.app)
# z.B.: cd apps/rebreak-native && pnpm exec expo run:ios --device "iPhone 16 Pro"
# 3. E2E_TEST_USER und E2E_TEST_PASSWORD gesetzt (via Infisical oder manuell)
# 4. Staging-Backend erreichbar
#
# Aufruf:
# bash apps/rebreak-native/.maestro/screens/capture-marketing.sh
#
# Mit Credentials:
# E2E_TEST_USER=admin E2E_TEST_PASSWORD=<pw> \
# bash apps/rebreak-native/.maestro/screens/capture-marketing.sh
#
# Oder via Infisical:
# infisical run -- bash apps/rebreak-native/.maestro/screens/capture-marketing.sh
set -euo pipefail
# ─── Pfade ────────────────────────────────────────────────────────────────────
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../../.." && pwd)"
MAESTRO_DIR="$REPO_ROOT/apps/rebreak-native/.maestro"
TOUR_FLOW="$MAESTRO_DIR/screens/marketing-tour.yaml"
PREVIEW_DIR="$REPO_ROOT/apps/marketing/public/preview"
# ─── Credentials-Check ────────────────────────────────────────────────────────
if [[ -z "${E2E_TEST_USER:-}" || -z "${E2E_TEST_PASSWORD:-}" ]]; then
echo "FEHLER: E2E_TEST_USER und E2E_TEST_PASSWORD müssen gesetzt sein."
echo " Variante A: E2E_TEST_USER=admin E2E_TEST_PASSWORD=<pw> bash $0"
echo " Variante B: infisical run -- bash $0"
exit 1
fi
# ─── Maestro-Check ────────────────────────────────────────────────────────────
if ! command -v maestro &>/dev/null; then
echo "FEHLER: Maestro CLI nicht gefunden."
echo " Installieren: curl -Ls 'https://get.maestro.mobile.dev' | bash"
echo " Dann: export PATH=\"\$PATH:\$HOME/.maestro/bin\""
exit 1
fi
# ─── Zielverzeichnis sicherstellen ────────────────────────────────────────────
mkdir -p "$PREVIEW_DIR"
# ─── iOS Simulator: Status-Bar überschreiben ──────────────────────────────────
# Zeigt 9:41, volles Signal/WLAN/Akku — klassischer Apple-Screenshot-State
# Erfordert: xcrun simctl status_bar <UDID> override ...
# UDID des laufenden Simulators automatisch ermitteln
BOOTED_UDID=""
if command -v xcrun &>/dev/null; then
BOOTED_UDID=$(xcrun simctl list devices booted --json \
| python3 -c "
import json, sys
data = json.load(sys.stdin)
for runtime, devices in data.get('devices', {}).items():
for d in devices:
if d.get('state') == 'Booted':
print(d['udid'])
exit()
" 2>/dev/null || true)
fi
if [[ -n "$BOOTED_UDID" ]]; then
echo "Status-Bar für Simulator $BOOTED_UDID auf 9:41 setzen..."
xcrun simctl status_bar "$BOOTED_UDID" override \
--time "9:41" \
--wifiMode "active" \
--wifiBars 3 \
--cellularMode "active" \
--cellularBars 4 \
--batteryState "charged" \
--batteryLevel 100 \
2>/dev/null || echo " Status-Bar-Override fehlgeschlagen (nicht kritisch — Flow läuft weiter)"
else
echo "Kein gestarteter iOS-Simulator gefunden — Status-Bar-Override übersprungen."
echo " Starte den Simulator zuerst: pnpm exec expo run:ios --device 'iPhone 16 Pro'"
fi
# ─── Maestro-Flow ausführen ────────────────────────────────────────────────────
echo ""
echo "Starte Marketing-Tour-Flow..."
echo " Flow: $TOUR_FLOW"
echo " User: $E2E_TEST_USER@rebreak.internal"
echo ""
# Maestro schreibt Screenshots nach ~/.maestro/tests/<timestamp>/screenshots/
# --output schreibt JUnit-Report, Screenshot-Pfad ist immer im Default-Verzeichnis
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
MAESTRO_SCREENSHOT_BASE="$HOME/.maestro/tests"
maestro test \
--env="E2E_TEST_USER=$E2E_TEST_USER" \
--env="E2E_TEST_PASSWORD=$E2E_TEST_PASSWORD" \
"$TOUR_FLOW"
echo ""
echo "Flow abgeschlossen. Screenshots suchen..."
# ─── Screenshot-Verzeichnis finden ────────────────────────────────────────────
# Maestro legt Screenshots im neuesten Testlauf-Verzeichnis ab
LATEST_RUN=$(ls -td "$MAESTRO_SCREENSHOT_BASE"/*/ 2>/dev/null | head -1 || true)
if [[ -z "$LATEST_RUN" ]]; then
echo "FEHLER: Kein Maestro-Testlauf-Verzeichnis gefunden unter $MAESTRO_SCREENSHOT_BASE"
echo " Manuell suchen: ls ~/.maestro/tests/"
exit 1
fi
SCREENSHOTS_DIR="$LATEST_RUN/screenshots"
if [[ ! -d "$SCREENSHOTS_DIR" ]]; then
# Maestro 1.38+ legt Screenshots direkt im Lauf-Verzeichnis ab
SCREENSHOTS_DIR="$LATEST_RUN"
fi
echo "Screenshots-Quellverzeichnis: $SCREENSHOTS_DIR"
echo ""
# ─── Mapping: Maestro-Name → Marketing-Dateiname ──────────────────────────────
# takeScreenshot-Wert in marketing-tour.yaml → Zieldateiname
declare -A MAPPING=(
["screenshots/01-onboarding"]="01-onboarding.png"
["screenshots/02-blocker"]="02-blocker.png"
["screenshots/03-blocked"]="03-blocked.png"
["screenshots/04-sos-lyra"]="04-sos-lyra.png"
["screenshots/05-breathing"]="05-breathing.png"
["screenshots/06-mail-schutz"]="06-mail-schutz.png"
["screenshots/07-community"]="07-community.png"
["screenshots/08-streak"]="08-streak.png"
["screenshots/09-geraete"]="09-geraete.png"
)
# Maestro benennt Screenshots nach dem letzten Teil des takeScreenshot-Werts
# "screenshots/01-onboarding" → Datei heißt "01-onboarding.png" im Maestro-Output
COPIED=0
MISSING=0
for MAESTRO_KEY in "${!MAPPING[@]}"; do
TARGET_NAME="${MAPPING[$MAESTRO_KEY]}"
# Maestro schreibt nur den Basename ohne Verzeichnis
BASENAME=$(basename "$MAESTRO_KEY")
SOURCE_FILE="$SCREENSHOTS_DIR/${BASENAME}.png"
if [[ -f "$SOURCE_FILE" ]]; then
cp "$SOURCE_FILE" "$PREVIEW_DIR/$TARGET_NAME"
echo " OK $TARGET_NAME$SOURCE_FILE"
((COPIED++))
else
echo " FEHLT $TARGET_NAME (Quelle nicht gefunden: $SOURCE_FILE)"
((MISSING++))
fi
done
# ─── Status-Bar zurücksetzen ──────────────────────────────────────────────────
if [[ -n "$BOOTED_UDID" ]]; then
xcrun simctl status_bar "$BOOTED_UDID" clear 2>/dev/null || true
fi
# ─── Zusammenfassung ──────────────────────────────────────────────────────────
echo ""
echo "─────────────────────────────────────────"
echo " Screenshots kopiert: $COPIED / 9"
if [[ $MISSING -gt 0 ]]; then
echo " Fehlend: $MISSING"
echo ""
echo " Für fehlende Screenshots:"
echo " - 03-blocked.png: manuell via Safari + aktivem VPN erstellen"
echo " Dann: cp /path/to/screenshot.png $PREVIEW_DIR/03-blocked.png"
echo ""
echo " Alle Screenshots unter: $SCREENSHOTS_DIR"
fi
echo " Zielverzeichnis: $PREVIEW_DIR"
echo "─────────────────────────────────────────"
if [[ $MISSING -gt 0 ]]; then
exit 1
fi

View File

@ -0,0 +1,82 @@
# Onboarding-Slides via Debug-Toggle (Root-Route, nicht gated). KEIN clearState.
# Account-Step wird am Ende auf 'done' restauriert.
appId: org.rebreak.app
---
# ── 01a — WELCOME ──
- launchApp
- waitForAnimationToEnd:
timeout: 5000
- tapOn:
text: "Nicht erlauben"
optional: true
- openLink:
link: "rebreak://debug"
- tapOn:
text: "Öffnen"
optional: true
- waitForAnimationToEnd:
timeout: 2500
- scrollUntilVisible:
element:
text: "Onboarding-Step"
direction: DOWN
timeout: 8000
centerElement: true
optional: true
- tapOn:
text: "Welcome"
optional: true
- waitForAnimationToEnd:
timeout: 4000
- takeScreenshot: screenshots/01a-welcome
# ── 01b — PROTECTION ──
- openLink:
link: "rebreak://debug"
- tapOn:
text: "Öffnen"
optional: true
- waitForAnimationToEnd:
timeout: 2500
- scrollUntilVisible:
element:
text: "Onboarding-Step"
direction: DOWN
timeout: 8000
centerElement: true
optional: true
- tapOn:
text: "Pre_Protection"
optional: true
- waitForAnimationToEnd:
timeout: 4000
- takeScreenshot: screenshots/01b-protection
# ── restore done + 01c — DONE SLIDE ──
- openLink:
link: "rebreak://debug"
- tapOn:
text: "Öffnen"
optional: true
- waitForAnimationToEnd:
timeout: 2500
- scrollUntilVisible:
element:
text: "Onboarding-Step"
direction: DOWN
timeout: 8000
centerElement: true
optional: true
- tapOn:
text: "Done"
optional: true
- waitForAnimationToEnd:
timeout: 3000
- openLink:
link: "rebreak://onboarding"
- tapOn:
text: "Öffnen"
optional: true
- waitForAnimationToEnd:
timeout: 3500
- takeScreenshot: screenshots/01c-done

View File

@ -0,0 +1,100 @@
# screens/marketing-tour-loggedin.yaml
#
# No-Login-Tour (single-session, tab-basiert, sheet-resilient).
# Voraussetzung: App auf dem Simulator BEREITS eingeloggt.
#
# WICHTIG: Nach Besuch des Blocker-Tabs erscheint das "Schutz ist aus"-Sheet
# (Bypass-Detection auf dem Sim) und blockiert modal alle weiteren Taps.
# Deshalb: alle anderen Tabs ZUERST, Blocker als LETZTES. "Nicht erlauben"
# (Screen-Time-Dialog) + "Später" (Schutz-Sheet) werden überall weggetippt.
# Alle tapOn optional → kein Abbruch. Nur launch/wait/screenshot Pflicht.
appId: org.rebreak.app
---
- launchApp
- waitForAnimationToEnd:
timeout: 6000
- tapOn:
text: "Nicht erlauben"
optional: true
- tapOn:
text: "Später"
optional: true
- waitForAnimationToEnd:
timeout: 1000
# 07 — HOME / FEED
- tapOn:
text: "Home"
optional: true
- tapOn:
text: "Später"
optional: true
- waitForAnimationToEnd:
timeout: 1200
- takeScreenshot: screenshots/07-community
# 04 — COACH / LYRA
- tapOn:
text: "Coach"
optional: true
- tapOn:
text: "Später"
optional: true
- waitForAnimationToEnd:
timeout: 2500
- takeScreenshot: screenshots/04-sos-lyra
# 07b — CHAT / DM
- tapOn:
text: "Chat"
optional: true
- tapOn:
text: "Später"
optional: true
- waitForAnimationToEnd:
timeout: 2000
- takeScreenshot: screenshots/07b-dm
# 06 — MAIL
- tapOn:
text: "Mail"
optional: true
- tapOn:
text: "Später"
optional: true
- waitForAnimationToEnd:
timeout: 2500
- takeScreenshot: screenshots/06-mail-schutz
# 08 — PROFIL / STREAK (Home + Header-Avatar)
- tapOn:
text: "Home"
optional: true
- tapOn:
text: "Später"
optional: true
- waitForAnimationToEnd:
timeout: 1000
- tapOn:
id: "header-avatar-btn"
optional: true
- waitForAnimationToEnd:
timeout: 2500
- takeScreenshot: screenshots/08-streak
# 02 / 03 — BLOCKER (zuletzt; Schutz-Sheet danach wegtippen)
- tapOn:
text: "Blocker"
optional: true
- waitForAnimationToEnd:
timeout: 2000
- tapOn:
text: "Später"
optional: true
- waitForAnimationToEnd:
timeout: 1500
- takeScreenshot: screenshots/02-blocker
- takeScreenshot: screenshots/03-blocked

View File

@ -0,0 +1,340 @@
# screens/marketing-tour.yaml
#
# Maestro Marketing-Tour — alle 9 Screenshots in einem Lauf
#
# Dieser Flow führt nacheinander alle 9 Marketing-Screens an und macht je einen
# takeScreenshot. Maestro speichert Screenshots unter ~/.maestro/tests/<timestamp>/
# Das capture-marketing.sh Script benennt sie in 01-onboarding.png … 09-geraete.png um.
#
# REIHENFOLGE KRITISCH — entspricht der Reihenfolge auf der Preview-Seite:
# 01 Onboarding → Sign-in-Screen (neuer Nutzer, clearState)
# 02 Blocker → Blocker-Tab mit Schutz-Status
# 03 Blocked → MANUELL (VPN außerhalb App) — Proxy: Header-Dropdown
# 04 SOS/Lyra → SOS-Screen mit Lyra-Antwort + Chips
# 05 Breathing → BreathingDrawer geöffnet
# 06 Mail-Schutz → Mail-Tab
# 07 Community → Home-Feed mit Posts
# 08 Streak → Profil-StreakSection
# 09 Geräte → Devices-Screen
#
# Wichtig: Flow bricht nach dem letzten takeScreenshot ab — keine Assertions danach.
# Jeder Abschnitt setzt den App-State via launchApp + clearState zurück.
#
# HINWEIS 03-blocked:
# Der echte Block-Screen ist außerhalb der App (Safari + VPN). Dieser Flow macht
# einen Proxy-Screenshot des Header-Dropdowns. Für den echten Shot: manuell.
#
# FRAGILE STEPS (brauchen testID-Ergänzung für Robustheit):
# - Avatar-Tap (93%, 6%) in allen Flows → testID="header-avatar-btn"
# - SOS-Send-Button → testID="sos-send-btn"
# - Settings→Geräte Navigation
#
# Env-Vars: E2E_TEST_USER, E2E_TEST_PASSWORD
# Voraussetzung: iOS Simulator mit Status-Bar via xcrun simctl status_bar überschrieben
# (Details in capture-marketing.sh)
appId: org.rebreak.app
---
# ══════════════════════════════════════════════════════════
# 01 — ONBOARDING / WILLKOMMEN
# Sign-in-Screen als "erster Blick" für neue Nutzer
# ══════════════════════════════════════════════════════════
- launchApp:
clearState: true
- waitForAnimationToEnd:
timeout: 6000
- assertVisible:
text: "E-Mail"
- waitForAnimationToEnd:
timeout: 2000
- takeScreenshot: screenshots/01-onboarding
# ══════════════════════════════════════════════════════════
# 02 — BLOCKER AKTIV
# ══════════════════════════════════════════════════════════
- launchApp:
clearState: true
- waitForAnimationToEnd:
timeout: 5000
- assertVisible:
text: "E-Mail"
- tapOn:
text: "E-Mail"
- inputText: ${E2E_TEST_USER}@rebreak.internal
- tapOn:
text: "Passwort"
- inputText: ${E2E_TEST_PASSWORD}
- tapOn:
text: "Anmelden"
- waitForAnimationToEnd:
timeout: 10000
- assertVisible:
text: "ReBreak"
- tapOn:
text: "Blocker"
- waitForAnimationToEnd:
timeout: 4000
- assertVisible:
text: "ReBreak-Schutz"
- scrollUntilVisible:
element:
text: "URL-Filter"
direction: DOWN
timeout: 5000
- waitForAnimationToEnd:
timeout: 1000
- takeScreenshot: screenshots/02-blocker
# ══════════════════════════════════════════════════════════
# 03 — BLOCKED (Proxy: Header-Dropdown als "Schutz greift")
# HINWEIS: Echter Block-Screen → manuell via Safari + VPN
# ══════════════════════════════════════════════════════════
- launchApp:
clearState: true
- waitForAnimationToEnd:
timeout: 5000
- assertVisible:
text: "E-Mail"
- tapOn:
text: "E-Mail"
- inputText: ${E2E_TEST_USER}@rebreak.internal
- tapOn:
text: "Passwort"
- inputText: ${E2E_TEST_PASSWORD}
- tapOn:
text: "Anmelden"
- waitForAnimationToEnd:
timeout: 10000
- assertVisible:
text: "ReBreak"
# Blocker-Tab zeigt den aktivsten "Schutz greift"-Screen ohne VPN
- tapOn:
text: "Blocker"
- waitForAnimationToEnd:
timeout: 4000
- assertVisible:
text: "ReBreak-Schutz"
- takeScreenshot: screenshots/03-blocked
# ══════════════════════════════════════════════════════════
# 04 — SOS / LYRA
# ══════════════════════════════════════════════════════════
- launchApp:
clearState: true
- waitForAnimationToEnd:
timeout: 5000
- assertVisible:
text: "E-Mail"
- tapOn:
text: "E-Mail"
- inputText: ${E2E_TEST_USER}@rebreak.internal
- tapOn:
text: "Passwort"
- inputText: ${E2E_TEST_PASSWORD}
- tapOn:
text: "Anmelden"
- waitForAnimationToEnd:
timeout: 10000
- assertVisible:
text: "ReBreak"
# FRAGILE: Avatar-Tap Koordinaten bis testID="header-avatar-btn" gesetzt
- tapOn:
id: "header-avatar-btn"
- waitForAnimationToEnd:
timeout: 2000
- assertVisible:
text: "SOS"
- tapOn:
text: "SOS"
- waitForAnimationToEnd:
timeout: 12000
- assertVisible:
text: "Was beschäftigt dich?"
- tapOn:
text: "Was beschäftigt dich?"
- waitForAnimationToEnd:
timeout: 500
- inputText: "Ich weiß nicht mehr weiter."
- pressKey: Return
- waitForAnimationToEnd:
timeout: 20000
- assertVisible:
text: "Atemübung"
- waitForAnimationToEnd:
timeout: 1000
- takeScreenshot: screenshots/04-sos-lyra
# ══════════════════════════════════════════════════════════
# 05 — ATEMÜBUNG
# BreathingDrawer bleibt nach 04-sos-lyra noch offen →
# kein launchApp nötig, direkt tippen
# ══════════════════════════════════════════════════════════
- tapOn:
text: "Atemübung"
- waitForAnimationToEnd:
timeout: 3000
- assertVisible:
text: "Atemübung"
- waitForAnimationToEnd:
timeout: 1500
- takeScreenshot: screenshots/05-breathing
# ══════════════════════════════════════════════════════════
# 06 — MAIL-SCHUTZ
# ══════════════════════════════════════════════════════════
- launchApp:
clearState: true
- waitForAnimationToEnd:
timeout: 5000
- assertVisible:
text: "E-Mail"
- tapOn:
text: "E-Mail"
- inputText: ${E2E_TEST_USER}@rebreak.internal
- tapOn:
text: "Passwort"
- inputText: ${E2E_TEST_PASSWORD}
- tapOn:
text: "Anmelden"
- waitForAnimationToEnd:
timeout: 10000
- assertVisible:
text: "ReBreak"
- tapOn:
text: "Mail"
- waitForAnimationToEnd:
timeout: 4000
- assertVisible:
text: "Mail-Schutz"
- waitForAnimationToEnd:
timeout: 1000
- takeScreenshot: screenshots/06-mail-schutz
# ══════════════════════════════════════════════════════════
# 07 — COMMUNITY-FEED
# ══════════════════════════════════════════════════════════
- launchApp:
clearState: true
- waitForAnimationToEnd:
timeout: 5000
- assertVisible:
text: "E-Mail"
- tapOn:
text: "E-Mail"
- inputText: ${E2E_TEST_USER}@rebreak.internal
- tapOn:
text: "Passwort"
- inputText: ${E2E_TEST_PASSWORD}
- tapOn:
text: "Anmelden"
- waitForAnimationToEnd:
timeout: 10000
- assertVisible:
text: "ReBreak"
- scrollUntilVisible:
element:
text: "Was bewegt dich gerade?"
direction: UP
timeout: 4000
- assertVisible:
text: "Was bewegt dich gerade?"
- scroll:
direction: DOWN
duration: 800
- waitForAnimationToEnd:
timeout: 1500
- takeScreenshot: screenshots/07-community
# ══════════════════════════════════════════════════════════
# 08 — STREAK / PROFIL-STATS
# ══════════════════════════════════════════════════════════
- launchApp:
clearState: true
- waitForAnimationToEnd:
timeout: 5000
- assertVisible:
text: "E-Mail"
- tapOn:
text: "E-Mail"
- inputText: ${E2E_TEST_USER}@rebreak.internal
- tapOn:
text: "Passwort"
- inputText: ${E2E_TEST_PASSWORD}
- tapOn:
text: "Anmelden"
- waitForAnimationToEnd:
timeout: 10000
- assertVisible:
text: "ReBreak"
# FRAGILE: Koordinaten-Tap
- tapOn:
id: "header-avatar-btn"
- waitForAnimationToEnd:
timeout: 2000
- assertVisible:
text: "Profil"
- tapOn:
text: "Profil"
- waitForAnimationToEnd:
timeout: 4000
- assertVisible:
text: "Profil"
- scrollUntilVisible:
element:
text: "SCHUTZ-ABDECKUNG"
direction: DOWN
timeout: 6000
- assertVisible:
text: "SCHUTZ-ABDECKUNG"
- waitForAnimationToEnd:
timeout: 1000
- takeScreenshot: screenshots/08-streak
# ══════════════════════════════════════════════════════════
# 09 — GERÄTE-ÜBERSICHT
# ══════════════════════════════════════════════════════════
- launchApp:
clearState: true
- waitForAnimationToEnd:
timeout: 5000
- assertVisible:
text: "E-Mail"
- tapOn:
text: "E-Mail"
- inputText: ${E2E_TEST_USER}@rebreak.internal
- tapOn:
text: "Passwort"
- inputText: ${E2E_TEST_PASSWORD}
- tapOn:
text: "Anmelden"
- waitForAnimationToEnd:
timeout: 10000
- assertVisible:
text: "ReBreak"
# FRAGILE: Koordinaten-Tap
- tapOn:
id: "header-avatar-btn"
- waitForAnimationToEnd:
timeout: 2000
- assertVisible:
text: "Einstellungen"
- tapOn:
text: "Einstellungen"
- waitForAnimationToEnd:
timeout: 3000
- scrollUntilVisible:
element:
text: "Geräte"
direction: DOWN
timeout: 5000
- tapOn:
text: "Geräte"
- waitForAnimationToEnd:
timeout: 4000
- assertVisible:
text: "Dieses Gerät"
- waitForAnimationToEnd:
timeout: 1000
- takeScreenshot: screenshots/09-geraete

View File

@ -0,0 +1,119 @@
# sos/crisis-flow.yaml
#
# HIGHEST PRIORITY FLOW — this must never silently fail.
#
# Journey: Login → Header-Dropdown → SOS → Lyra chat loads → send a message →
# assert response area or chip row becomes visible (Lyra is alive).
#
# This flow is the canary for:
# - SOS navigation path not broken
# - Lyra streaming endpoint reachable from staging
# - Chat input + send mechanism functional
# - Breathing chip "Atemübung" accessible as immediate coping entry
#
# Unlike the previous urge/sos-flow.yaml which relies on a coordinate tap for
# the avatar button, this flow uses the same coordinate fallback until
# testID="header-avatar-btn" is added (see TODO_TESTIDS.md HIGH priority).
# The coordinate tap is the ONLY fragile part of this flow.
#
# Timing: SOS screen on staging needs 612s for Groq cold start. The send step
# waits 20s for Lyra's response — intentionally generous. A CI failure here
# means either backend is down or Groq key is missing/expired.
#
# Pre-requisite:
# - App installed. Test-user exists on staging.
# - Staging backend + Groq API key configured.
# Env-Vars: E2E_TEST_USER, E2E_TEST_PASSWORD
appId: org.rebreak.app
---
- launchApp:
clearState: true
- waitForAnimationToEnd:
timeout: 5000
# --- Auth ---
- assertVisible:
text: "E-Mail"
- tapOn:
text: "E-Mail"
- inputText: ${E2E_TEST_USER}@rebreak.internal
- tapOn:
text: "Passwort"
- inputText: ${E2E_TEST_PASSWORD}
- tapOn:
text: "Anmelden"
- waitForAnimationToEnd:
timeout: 10000
- assertVisible:
text: "ReBreak"
# --- Open Header Dropdown ---
# FRAGILE: Avatar Pressable has no testID → coordinate tap.
# Replace with: tapOn: { id: "header-avatar-btn" } once testID is added.
- tapOn:
point: "93%, 6%"
- waitForAnimationToEnd:
timeout: 2000
# appHeader.sosLabel = "SOS" (de.json line 119)
- assertVisible:
text: "SOS"
- tapOn:
text: "SOS"
# SOS screen loads: RiveAvatar + Lyra streaming begins.
# Cold start on Groq (staging) = 612s. We wait the full 12s before asserting.
- waitForAnimationToEnd:
timeout: 12000
# coach.placeholder = "Was beschäftigt dich?" — only exists on SOS/Urge screen.
# This confirms the SOS screen has fully loaded with the chat input visible.
- assertVisible:
text: "Was beschäftigt dich?"
# --- Send a message ---
# Tap the placeholder to focus the TextInput
- tapOn:
text: "Was beschäftigt dich?"
- waitForAnimationToEnd:
timeout: 1000
# Type a safe test message that triggers a Lyra response
- inputText: "Ich brauche gerade Hilfe."
# Send via the send button.
# FRAGILE: send Pressable has no testID → use pressKey Enter as fallback.
# The TextInput in urge.tsx submits on Return key (onSubmitEditing = handleSend).
# This is more reliable than coordinate tapping the icon button.
# Add testID="sos-send-btn" to fix (see TODO_TESTIDS.md HIGH priority).
- pressKey: Return
- waitForAnimationToEnd:
timeout: 3000
# Wait for Lyra to stream back a response (up to 20s on staging cold start)
- waitForAnimationToEnd:
timeout: 20000
# After Lyra responds, the initial chip set is visible.
# "Atemübung" is in CHIP_SETS.start (sosConstants.ts) — hardcoded, not i18n.
# This chip is ALWAYS shown as the first response. If it's not visible after 20s,
# Lyra failed to respond — the test should fail here.
- assertVisible:
text: "Atemübung"
# --- Verify Breathing entry point ---
# Tap "Atemübung" to open BreathingDrawer. This ensures the most critical
# coping mechanism (breathing exercise) is accessible from the SOS screen.
- tapOn:
text: "Atemübung"
- waitForAnimationToEnd:
timeout: 3000
# BreathingDrawer header. The breathing screen title is hardcoded in Breathing.tsx
# (not i18n). urge.sos_title = "SOS — Atemübung" (de.json line 979).
# The drawer itself renders "Atemübung" as its header text.
- assertVisible:
text: "Atemübung"

View File

@ -0,0 +1,160 @@
# stress/dm-scroll-performance.yaml
#
# Stress scenario: Open a DM conversation with many messages and scroll
# the entire history. Designed to catch FlatList perf issues on slow devices
# (Samsung A50 — primary Android test target, known slow).
#
# What this tests:
# - DM FlatList renders without ANR (App Not Responding) on large history
# - Scroll from bottom to top of message list completes
# - Scroll back to bottom (chat.scrollToBottom behavior)
# - App does not crash or freeze during scroll
# - "Scroll to bottom" action returns to latest message correctly
#
# Samsung A50 characteristics:
# - Exynos 9610 — single-core perf ~35% slower than Pixel 5
# - 4GB RAM — RN FlatList with 100+ messages + image attachments can OOM
# - Expected: 510s for initial load on large history (100 messages)
#
# Pre-requisite:
# - Test user has a DM conversation with 50+ messages (seed if not present).
# - If conversation is short: the stress value is reduced but the flow still
# validates basic scroll behavior.
# Env-Vars: E2E_TEST_USER, E2E_TEST_PASSWORD, E2E_TEST_PEER_NICKNAME
appId: org.rebreak.app
---
- launchApp:
clearState: true
# Extended wait — A50 cold boot is slow
- waitForAnimationToEnd:
timeout: 8000
# --- Auth ---
- assertVisible:
text: "E-Mail"
- tapOn:
text: "E-Mail"
- inputText: ${E2E_TEST_USER}@rebreak.internal
- tapOn:
text: "Passwort"
- inputText: ${E2E_TEST_PASSWORD}
- tapOn:
text: "Anmelden"
# Extended timeout for A50 + Staging network round-trip
- waitForAnimationToEnd:
timeout: 15000
- assertVisible:
text: "ReBreak"
# --- Navigate to Chat tab ---
- tapOn:
text: "Chat"
- waitForAnimationToEnd:
timeout: 5000
- assertVisible:
text: "Direktnachrichten"
# --- Open target DM ---
- tapOn:
text: ${E2E_TEST_PEER_NICKNAME}
# Extended load time for large message history on A50
- waitForAnimationToEnd:
timeout: 10000
# DM screen input is the load-complete signal
- assertVisible:
text: "Nachricht schreiben…"
# --- Scroll UP through history ---
# Repeat swipe-up 8 times to simulate user browsing through history.
# Each swipe = ~3 visible messages scrolled on an A50 screen.
# 8 swipes = ~24 messages reviewed without crash.
- swipe:
direction: UP
duration: 600
- waitForAnimationToEnd:
timeout: 2000
- swipe:
direction: UP
duration: 600
- waitForAnimationToEnd:
timeout: 2000
- swipe:
direction: UP
duration: 600
- waitForAnimationToEnd:
timeout: 2000
- swipe:
direction: UP
duration: 600
- waitForAnimationToEnd:
timeout: 2000
- swipe:
direction: UP
duration: 600
- waitForAnimationToEnd:
timeout: 2000
- swipe:
direction: UP
duration: 600
- waitForAnimationToEnd:
timeout: 2000
- swipe:
direction: UP
duration: 600
- waitForAnimationToEnd:
timeout: 2000
- swipe:
direction: UP
duration: 600
- waitForAnimationToEnd:
timeout: 3000
# If the app hasn't crashed by here: FlatList is stable under upward scroll.
# --- Scroll BACK DOWN to latest message ---
# Simulate user tapping "scroll to bottom" or dragging down rapidly.
- swipe:
direction: DOWN
duration: 300
- swipe:
direction: DOWN
duration: 300
- swipe:
direction: DOWN
duration: 300
- swipe:
direction: DOWN
duration: 300
- waitForAnimationToEnd:
timeout: 3000
# --- Assert the input is still accessible after scroll ---
# If the app ANR'd or crashed, this will fail.
# If the FlatList lost keyboard/input state, this will fail.
- assertVisible:
text: "Nachricht schreiben…"
# --- Send a message after scroll to verify input still works ---
- tapOn:
text: "Nachricht schreiben…"
- inputText: "[E2E-STRESS] Scroll-Test abgeschlossen"
- pressKey: Return
- waitForAnimationToEnd:
timeout: 8000
# Message bubble appeared = send path survived after heavy scroll
- assertVisible:
text: "[E2E-STRESS] Scroll-Test abgeschlossen"

View File

@ -0,0 +1,105 @@
# stress/rapid-post-submit.yaml
#
# Stress scenario: Submit multiple community posts in rapid succession.
# Designed to catch race conditions in the post submission pipeline:
# - Duplicate optimistic inserts
# - ComposeCard not resetting between posts
# - React Query invalidation firing before previous request resolves
# - "Post konnte nicht veröffentlicht werden" error appearing incorrectly
#
# Samsung A50 note: The submit → reset cycle takes ~13s on A50 due to:
# - API round-trip to staging (network)
# - React Query invalidation → re-render
# - ComposeCard animation (expand/collapse)
# Each post is given 8s timeout. 3 posts = ~24s total on A50.
#
# Pre-requisite: Test-user exists on staging. Posts created are real (staging DB).
# Cleanup: Delete [E2E-STRESS] posts from staging DB manually or via Service-Role.
# Env-Vars: E2E_TEST_USER, E2E_TEST_PASSWORD
appId: org.rebreak.app
---
- launchApp:
clearState: true
- waitForAnimationToEnd:
timeout: 8000
# --- Auth ---
- assertVisible:
text: "E-Mail"
- tapOn:
text: "E-Mail"
- inputText: ${E2E_TEST_USER}@rebreak.internal
- tapOn:
text: "Passwort"
- inputText: ${E2E_TEST_PASSWORD}
- tapOn:
text: "Anmelden"
- waitForAnimationToEnd:
timeout: 15000
- assertVisible:
text: "ReBreak"
# --- Scroll to ComposeCard at top ---
- scrollUntilVisible:
element:
text: "Was bewegt dich gerade?"
direction: UP
timeout: 5000
# === POST 1 ===
- tapOn:
text: "Was bewegt dich gerade?"
- waitForAnimationToEnd:
timeout: 1000
- inputText: "[E2E-STRESS] Post 1 — Rapid-Submit-Test"
- assertVisible:
text: "Teilen"
- tapOn:
text: "Teilen"
- waitForAnimationToEnd:
timeout: 8000
# ComposeCard must reset after submit — placeholder visible again
- assertVisible:
text: "Was bewegt dich gerade?"
# === POST 2 ===
- tapOn:
text: "Was bewegt dich gerade?"
- waitForAnimationToEnd:
timeout: 1000
- inputText: "[E2E-STRESS] Post 2 — Rapid-Submit-Test"
- assertVisible:
text: "Teilen"
- tapOn:
text: "Teilen"
- waitForAnimationToEnd:
timeout: 8000
- assertVisible:
text: "Was bewegt dich gerade?"
# === POST 3 ===
- tapOn:
text: "Was bewegt dich gerade?"
- waitForAnimationToEnd:
timeout: 1000
- inputText: "[E2E-STRESS] Post 3 — Rapid-Submit-Test"
- assertVisible:
text: "Teilen"
- tapOn:
text: "Teilen"
- waitForAnimationToEnd:
timeout: 8000
- assertVisible:
text: "Was bewegt dich gerade?"
# --- Verify no error state ---
# community.post_failed = "Post konnte nicht veröffentlicht werden."
# If any of the 3 posts failed with this error, the flow failed.
- assertNotVisible:
text: "Post konnte nicht veröffentlicht werden."

View File

@ -0,0 +1,65 @@
# Voice-Calls — Debug-Zustand (2026-06-05)
Momentaufnahme beim Live-Debugging mit 2 Geräten (iOS + Android, beide über Metro).
Ziel: Kinderkrankheiten der Call-Funktion per Teile-und-Herrsche fixen.
## Architektur (Ist-Zustand)
Zwei Zustell-Pfade zum Callee — **beide feuern IMMER parallel** (Caller macht beides):
1. **Realtime** `call-ring:<calleeId>``useIncomingCalls``receiveIncoming()` + `router.push('/call')`.
Greift nur wenn die App lebt + Realtime subscribed (FG, oder Android kurz nach BG).
2. **Push** `POST /api/calls/ring``sendCallRingPush`:
- **iOS + voipToken** → VoIP-PushKit → `AppDelegate.didReceiveIncomingPush``RNCallKeep.reportNewIncomingCall` (CallKit) → JS-Forward via `useCallKeepEvents`.
- **sonst** (Android, iOS ohne voipToken) → Expo-Push (channel `calls`, `data.type='call'`). Reagiert **nur auf Tap** (`addNotificationResponseReceivedListener`).
Kern-Spannung: im FG entsteht Doppel-UI (in-app `/call` **und** CallKit/ConnectionService).
iOS-BG ist durch Apple auf CallKit festgenagelt (PushKit-Pflicht). Android-BG ist frei wählbar.
## Verhaltens-Matrix
| # | Anruf | Callee-State | Erwartet | Beobachtet | Hypothese (Root Cause) |
|---|-------|--------------|----------|------------|------------------------|
| 1 | Android→iOS | FG | `/call` bleibt, klingelt | `/call` erscheint, **verschwindet nach ~Sek** | VoIP-Push feuert auch im FG → CallKit übernimmt → App→`inactive` → CallKit-`endCall` feuert mit `appState≠active``onEnd`-Guard (`appState==='active'`) greift NICHT → `declineCall()` schließt `/call`. **(Logs nötig)** |
| 2 | iOS→Android | FG | korrekt | **korrekt** | — |
| 3 | Android→iOS | BG/locked | CallKit-Incoming (Apple) | **nichts**; nach Auflegen „verpasster Anruf"-Push; Telefon-App zeigt kein ReBreak-Audio | VoIP-Push erreicht Gerät nicht / CallKit nicht reported. Verdacht A: `voipToken=null` in DB → Fallback Expo-Push (im BG silent). Verdacht B: VoIP gesendet, aber APNs-Fehler (env/topic/cert). „Verpasster Anruf"-Push = Chat-DM aus `logCallToChat`. **(Logs nötig)** |
| 4 | iOS→Android | BG (App lebt) | ReBreak `/call` | **Android Telecom-native Incoming-UI** | Realtime im BG → `receiveIncoming``displayIncomingCall` → ConnectionService = System-Telecom-UI. „Working as coded", aber Design-Mismatch: gewünscht ist Custom-`/call` (Full-Screen-Intent). |
## Divide & Conquer — Phasen
- **Phase 0 — Diagnose** (non-destruktiv): 4 Szenarien durchspielen + Logs einsammeln
(`[call-ring] voip=?`, `[voip-push]`, `[VoIP] didReceiveIncomingPush`, `[callkeep] end … appState=`).
Bestätigt #1 und #3.
- **Phase 1 — iOS FG (#1)**: `/call` autoritativ machen; CallKit im FG unterdrücken / sauber
programmatisch beenden mit „programmatic-end"-Flag; `onEnd`-Guard reparieren (`inactive` = FG).
- **Phase 2 — iOS BG (#3)**: VoIP-Pfad fixen — voipToken-Registrierung + APNs env/topic/cert
verifizieren, bis CallKit im BG zuverlässig erscheint.
- **Phase 3 — Android BG (#4)**: Entscheidung Custom-Full-Screen-Intent vs Telecom-UI; ggf. umbauen.
- **Phase 4 — Aufräumen**: Doppel-Push/Doppel-Log, Missed-Call-Handling, einheitlicher Teardown.
## ✅ BESTÄTIGTE Root Cause #3 (2026-06-05, via idevicesyslog + git)
Build 76 wurde aus dem **alten** `with-voip-pushkit-ios.js` kompiliert, wo die PushKit-Registry
eine **lokale Variable** war (`let voipRegistry = PKPushRegistry(queue: nil)`) → deallociert
sofort nach `didFinishLaunching` → eingehende VoIP-Pushes erreichen `didReceiveIncomingPush` nie
→ kein `reportNewIncomingCall` → kein CallKit-Wake. Token-Registrierung klappt trotzdem
(Sync-Callback), daher `voip=1` am Server, aber Pushes verpuffen.
**Beweise (idevicesyslog iPhone Air, Build 76):**
- `[voip] register event fired, token: 33899c2b…` ✅ (Token-Reg ok)
- `[voip-push] sent env=sandbox` am Server ✅ (Push verlässt Server)
- **`[VoIP] didReceiveIncomingPush` / `reportNewIncomingCall` FEHLEN komplett** ❌
- CallKit-Fehler `requesttransaction Code=4` (unknown UUID) beim End-Versuch = Call war CallKit nie bekannt
- „verpasster Anruf"-Push = Chat-DM aus `logCallToChat` beim späteren FG-Realtime-Replay, NICHT der Ring
**Fix:** bereits geschrieben (uncommitted) — Registry als `self.voipRegistry` retainen.
Liegt im `ios/ReBreak/AppDelegate.swift` (Jun 4 20:35), aber **nie in einen Build kompiliert**.
→ Dev-Rebuild (`expo run:ios --device`) kompiliert den Fix. Danach verifizieren dass
`[VoIP] didReceiveIncomingPush` feuert. Offen bleibt sekundärer Verdacht: Doppel-Registry
durch JS `registerVoipToken()` (Library erzeugt 2. unretained Registry) — empirisch nach Rebuild prüfen.
## Apple-Constraint (kein Bug)
Auf iOS **Background/locked** ist die Incoming-UI zwingend CallKit (PushKit verlangt
`reportNewIncomingCall` in derselben Run-Loop, sonst killt iOS die App). Der Custom-`/call`-Screen
kann dort erst NACH „Annehmen" erscheinen — nicht als Klingel-UI.

View File

@ -1,6 +1,39 @@
# Changelog # Changelog
All notable changes to rebreak-native will be documented in this file. All notable changes to rebreak-native will be documented in this file.
## v0.4.5 (Build 90 / versionCode 71) — 2026-06-10\n\n# Next Release — Notes
- Fix: the "Protection is off" sheet and the "Protection tampered" push no longer
fire on devices where protection was never activated locally (fresh installs,
new devices, simulators). The bypass state now requires a local
"was active here" flag, set after a successful activation — instead of relying
on the account-wide backend default. Real bypasses (VPN/profile removed on a
device that had protection) are still detected.
- Copy: blocklist size updated 208k → 300k across all locales (de/en/fr/ar).
- Lyra avatar: richer, more present reactions. While you type or record a voice
message, Lyra shows a note-taking pose (like a therapist listening) and keeps
it while she prepares her reply, then reacts to the tone of the conversation.
- Fix (Android 16): activating VPN protection no longer crashes the app. The
DNS-filter foreground service used a service type that Android 16 rejects at
start; switched to the correct `specialUse` type so the filter starts reliably
on Android 14, 15 and 16.
- Onboarding: the protection step can no longer trap you. If protection can't be
activated on your device (or activation fails), a "set up later" option now
appears so you can finish onboarding and enable protection anytime from the
Protection screen.
- Fix (Android, esp. Samsung): protection no longer silently dies when the phone
puts the app to sleep. Setup now includes a "turn off battery optimization"
step — without it, aggressive power management could unbind the accessibility
lock so protection stopped enforcing while still looking active. On Samsung a
hint guides you to the stronger "Unrestricted" battery setting.
- Fix (Android): the protection status is now honest. If the accessibility
service isn't actually running, the app no longer shows "fully protected" — it
correctly shows the step as open and prompts you to re-enable it.
- Android setup polish: activating the VPN now shows a short spinner until it's
confirmed (the step no longer looks stuck for a minute), and enabling the
accessibility lock no longer needs a second tap.
- Stability: realtime updates no longer log spurious errors while the app is in
the background (the websocket close on suspend is expected and now silent).\n
## v0.4.4 (Build 90 / versionCode 70) — 2026-06-08\n\n# Next Release ## v0.4.4 (Build 90 / versionCode 70) — 2026-06-08\n\n# Next Release
## Improved ## Improved

View File

@ -1,9 +1,11 @@
# Next Release — Notes # Next Release
- Fix: the "Protection is off" sheet and the "Protection tampered" push no longer ## Fixes
fire on devices where protection was never activated locally (fresh installs,
new devices, simulators). The bypass state now requires a local - **Android: fixed crash loop on app open (Samsung / Android 1416).** The VPN
"was active here" flag, set after a successful activation — instead of relying protection service crashed with `SecurityException` in `startForeground()`
on the account-wide backend default. Real bypasses (VPN/profile removed on a because Android 16's `validateForegroundServiceType` rejects the implicit
device that had protection) are still detected. 2-arg call. We now pass the `specialUse` foreground-service type explicitly
- Copy: blocklist size updated 208k → 300k across all locales (de/en/fr/ar). (Google's documented best practice) and guard the call so a failed
foreground promotion can never crash the app again. Verified against the
reported Galaxy A54 / Android 16 crash signature.

View File

@ -36,7 +36,7 @@ export default ({ config }: ConfigContext): ExpoConfig => ({
ios: { ios: {
supportsTablet: true, supportsTablet: true,
bundleIdentifier: MAIN_BUNDLE, bundleIdentifier: MAIN_BUNDLE,
buildNumber: "90", buildNumber: "92",
// Apple Sign-In Entitlement — Pflicht für expo-apple-authentication nativen // Apple Sign-In Entitlement — Pflicht für expo-apple-authentication nativen
// signInAsync()-Flow. Ohne flag generiert Expo's prebuild den // signInAsync()-Flow. Ohne flag generiert Expo's prebuild den
// com.apple.developer.applesignin-Entitlement nicht in die .entitlements. // com.apple.developer.applesignin-Entitlement nicht in die .entitlements.
@ -62,7 +62,7 @@ export default ({ config }: ConfigContext): ExpoConfig => ({
android: { android: {
package: "org.rebreak.app", package: "org.rebreak.app",
versionCode: 70, versionCode: 73,
// Firebase / FCM-v1-Credentials: Pflicht ab Expo SDK 53 für Android-Push. // Firebase / FCM-v1-Credentials: Pflicht ab Expo SDK 53 für Android-Push.
// Enthält client-config für beide Packages (org.rebreak.app + .dev) und // Enthält client-config für beide Packages (org.rebreak.app + .dev) und
// ist NICHT geheim (API-Key per Package-Signing-Fingerprint restricted) — // ist NICHT geheim (API-Key per Package-Signing-Fingerprint restricted) —
@ -92,6 +92,13 @@ export default ({ config }: ConfigContext): ExpoConfig => ({
"BIND_TELECOM_CONNECTION_SERVICE", "BIND_TELECOM_CONNECTION_SERVICE",
"FOREGROUND_SERVICE_MICROPHONE", "FOREGROUND_SERVICE_MICROPHONE",
"FOREGROUND_SERVICE_PHONE_CALL", "FOREGROUND_SERVICE_PHONE_CALL",
// Pflicht für den VPN-DNS-Filter-Foregroundservice (Typ specialUse) ab
// Android 14; ohne diese Permission wirft startForeground auf Android 16
// eine SecurityException. Play braucht dazu eine einmalige Special-Use-Decl.
"FOREGROUND_SERVICE_SPECIAL_USE",
// Akku-Ausnahme anfordern: ohne sie schläfert Samsung & Co. die App ein →
// a11y-Tamper-Lock wird entbunden → Schutz fällt still aus.
"REQUEST_IGNORE_BATTERY_OPTIMIZATIONS",
"USE_FULL_SCREEN_INTENT", "USE_FULL_SCREEN_INTENT",
// Nutzungszugriff: erlaubt das Erkennen des aktuellen Settings-Screens // Nutzungszugriff: erlaubt das Erkennen des aktuellen Settings-Screens
// (UsageStatsManager) → state-aware a11y-Setup-Guide. User muss es manuell // (UsageStatsManager) → state-aware a11y-Setup-Guide. User muss es manuell

View File

@ -103,6 +103,7 @@ export default function BlockerScreen() {
const appDeletionLockActive = (state?.layers.appDeletionLock ?? familyControlsActive) === true; const appDeletionLockActive = (state?.layers.appDeletionLock ?? familyControlsActive) === true;
const nefilterActive = state?.layers.nefilterActive === true; const nefilterActive = state?.layers.nefilterActive === true;
const deviceAdminActive = state?.layers.deviceAdmin === true; const deviceAdminActive = state?.layers.deviceAdmin === true;
const batteryUnrestricted = state?.layers.batteryUnrestricted === true;
// "lockedIn" = beide Layer aktiv: URL-Filter (echter Schutz) UND App-Lock // "lockedIn" = beide Layer aktiv: URL-Filter (echter Schutz) UND App-Lock
// (Hardening). Family-Controls ALLEINE = kein Schutz, nur denyAppRemoval — // (Hardening). Family-Controls ALLEINE = kein Schutz, nur denyAppRemoval —
// ohne URL-Filter sieht der User trotzdem Glücksspielseiten. Daher BEIDE // ohne URL-Filter sieht der User trotzdem Glücksspielseiten. Daher BEIDE
@ -114,7 +115,11 @@ export default function BlockerScreen() {
// Profile + non-removable App enforced, FC-Toggle ist irrelevant. // Profile + non-removable App enforced, FC-Toggle ist irrelevant.
// nefilterActive → Schutz via System-Profil, kein VPN-Toggle nötig → locked-in // nefilterActive → Schutz via System-Profil, kein VPN-Toggle nötig → locked-in
const lockedIn = Platform.OS === 'android' const lockedIn = Platform.OS === 'android'
? (urlFilterActive && appDeletionLockActive && deviceAdminActive) // Akku-Ausnahme MUSS dabei sein — ohne sie schläfert Samsung & Co. den
// a11y-Service ein (Lock entbunden → Schutz fällt still aus). Sonst zeigt der
// Banner „geschützt" + versteckt den (offenen) Akku-Step. Konsistent mit der
// Onboarding-`allDone`.
? (urlFilterActive && appDeletionLockActive && deviceAdminActive && batteryUnrestricted)
: (nefilterActive || urlFilterActive) && (mdmManaged || nefilterActive || appDeletionLockActive || !FAMILY_CONTROLS_AVAILABLE); : (nefilterActive || urlFilterActive) && (mdmManaged || nefilterActive || appDeletionLockActive || !FAMILY_CONTROLS_AVAILABLE);
const urlFilterActiveRef = useRef(urlFilterActive); const urlFilterActiveRef = useRef(urlFilterActive);
@ -171,6 +176,19 @@ export default function BlockerScreen() {
} }
} }
// ─── Akku-Ausnahme (gegen Samsung-Sleep, der den a11y-Lock entbindet) ──
async function handleRequestBattery() {
// System-Dialog „Akku-Optimierung ignorieren?" (ein Tap). Der State zieht via
// AppState-'active'-Refresh nach, wenn der User zurückkommt.
return protection.requestIgnoreBatteryOptimizations();
}
async function handleOpenAppDetails() {
// Samsung-Sonderweg: App-Detail → Akku „Uneingeschränkt" + raus aus
// „Schlafende Apps" (deckt der AOSP-Whitelist-Dialog nicht ab).
return protection.openAppDetailsSettings();
}
// ─── Activate-Handler pro Layer ────────────────────────────────────── // ─── Activate-Handler pro Layer ──────────────────────────────────────
async function handleActivateUrlFilter() { async function handleActivateUrlFilter() {
@ -363,9 +381,12 @@ export default function BlockerScreen() {
vpnActive={urlFilterActive} vpnActive={urlFilterActive}
accessibilityLocked={appDeletionLockActive} accessibilityLocked={appDeletionLockActive}
deviceAdminActive={deviceAdminActive} deviceAdminActive={deviceAdminActive}
batteryUnrestricted={batteryUnrestricted}
onActivateVpn={handleActivateUrlFilter} onActivateVpn={handleActivateUrlFilter}
onActivateAccessibility={handleActivateFamilyControls} onActivateAccessibility={handleActivateFamilyControls}
onRequestDeviceAdmin={handleRequestDeviceAdmin} onRequestDeviceAdmin={handleRequestDeviceAdmin}
onRequestBattery={handleRequestBattery}
onOpenAppDetails={handleOpenAppDetails}
colors={colors} colors={colors}
t={t} t={t}
/> />

View File

@ -14,12 +14,13 @@ import { SafeAreaView } from 'react-native-safe-area-context';
import { useRouter } from 'expo-router'; import { useRouter } from 'expo-router';
import { Ionicons } from '@expo/vector-icons'; import { Ionicons } from '@expo/vector-icons';
import { useColors } from '../lib/theme'; import { useColors } from '../lib/theme';
import { useMe, invalidateMe, type Plan } from '../hooks/useMe'; import { useMe, invalidateMe, type Plan, type OnboardingStep } from '../hooks/useMe';
import { apiFetch } from '../lib/api'; import { apiFetch } from '../lib/api';
import { PlanChangeSheet } from '../components/plan/PlanChangeSheet'; import { PlanChangeSheet } from '../components/plan/PlanChangeSheet';
import { getCooldownTestMode, setCooldownTestMode, protection } from '../lib/protection'; import { getCooldownTestMode, setCooldownTestMode, protection } from '../lib/protection';
import { useRealtimeDebugStore, type LogEntry } from '../stores/realtimeDebug'; import { useRealtimeDebugStore, type LogEntry } from '../stores/realtimeDebug';
import { supabase } from '../lib/supabase'; import { supabase } from '../lib/supabase';
import { RiveAvatar, EMOTION_ANIMATIONS, EXISTING_TIMELINES, type Emotion } from '../components/RiveAvatar';
export default function DebugScreen() { export default function DebugScreen() {
const router = useRouter(); const router = useRouter();
@ -107,6 +108,8 @@ export default function DebugScreen() {
</View> </View>
</View> </View>
<LyraEmotionPreviewCard />
{me ? ( {me ? (
<PlanOverrideToggle <PlanOverrideToggle
colors={colors} colors={colors}
@ -152,6 +155,131 @@ export default function DebugScreen() {
); );
} }
// ─── Lyra Emotion Preview (Rive-State-Tester) ──────────────────────────────
/**
* Tippe einen Emotion-State an der Avatar spielt die zugehörige Timeline aus
* `assets/lyra-avatar.riv`. So lassen sich neue States testen, OHNE die echten
* Trigger (Tippen / Atmen / Rückfall-Text / LLM-Generierung) auszulösen.
*
* Workflow zum Selber-Bauen:
* 1. `lyra-avatar.riv` im Rive-Editor (rive.app) öffnen, Timeline exakt mit dem
* unten angezeigten Namen anlegen (Contract Tippfehler = stiller Ausfall).
* 2. Als `.riv` exportieren, nach `apps/rebreak-native/assets/lyra-avatar.riv`
* legen (überschreiben).
* 3. iOS-Dev-Build neu laden (Metro `r`) Avatar zeigt sofort die neue Anim.
* (Android braucht Rebuild: Raw-Resource wird erst beim prebuild gemirrort.)
*
* Zeigt für noch nicht im .riv vorhandene States den statischen Idle-Frame.
*/
const PREVIEW_EMOTIONS: Emotion[] = [
'idle',
'happy',
'empathy',
'thinking',
'listening',
'calm',
'sad',
'joy',
'confusion',
'surprise',
];
function LyraEmotionPreviewCard() {
const colors = useColors();
const [emotion, setEmotion] = useState<Emotion>('idle');
const wanted = EMOTION_ANIMATIONS[emotion] ?? '—';
const exists = EXISTING_TIMELINES.has(wanted);
return (
<View
style={{
backgroundColor: colors.surface,
borderRadius: 14,
borderWidth: 1,
borderColor: 'rgba(0,0,0,0.05)',
padding: 14,
marginBottom: 12,
}}
>
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 10, marginBottom: 12 }}>
<View
style={{
width: 36,
height: 36,
borderRadius: 11,
backgroundColor: colors.surfaceElevated,
alignItems: 'center',
justifyContent: 'center',
}}
>
<Ionicons name="happy-outline" size={18} color={colors.textMuted} />
</View>
<View style={{ flex: 1 }}>
<Text style={{ fontSize: 14, color: colors.text, fontFamily: 'Nunito_700Bold' }}>
Lyra Emotion Preview
</Text>
<Text
style={{
fontSize: 12,
color: colors.textMuted,
fontFamily: 'Nunito_400Regular',
marginTop: 3,
lineHeight: 17,
}}
>
Rive-State-Tester Timeline aus lyra-avatar.riv
</Text>
</View>
</View>
<View style={{ alignItems: 'center', gap: 8, marginBottom: 14 }}>
<RiveAvatar emotion={emotion} size="lg" showLabel />
<Text
style={{
fontSize: 11,
color: exists ? colors.success : colors.textMuted,
fontFamily: 'Menlo',
textAlign: 'center',
}}
>
{emotion} &quot;{wanted}&quot;
{exists ? ' ✓ im .riv' : ' → Idle (Timeline fehlt noch)'}
</Text>
</View>
<View style={{ flexDirection: 'row', flexWrap: 'wrap', gap: 8 }}>
{PREVIEW_EMOTIONS.map((em) => {
const isActive = em === emotion;
return (
<TouchableOpacity
key={em}
onPress={() => setEmotion(em)}
activeOpacity={0.7}
style={{
paddingVertical: 8,
paddingHorizontal: 14,
borderRadius: 10,
backgroundColor: isActive ? colors.brandOrange : colors.surfaceElevated,
}}
>
<Text
style={{
fontSize: 13,
fontFamily: 'Nunito_700Bold',
color: isActive ? '#ffffff' : colors.textMuted,
}}
>
{em}
</Text>
</TouchableOpacity>
);
})}
</View>
</View>
);
}
// ─── Redirect-Test Card (Layer-1-Bypass-Repro) ───────────────────────────── // ─── Redirect-Test Card (Layer-1-Bypass-Repro) ─────────────────────────────
/** /**
@ -924,7 +1052,10 @@ function PlanOverrideToggle({
// ─── Onboarding Reset ────────────────────────────────────────────────────── // ─── Onboarding Reset ──────────────────────────────────────────────────────
const ONBOARDING_STEPS = ['welcome', 'nickname', 'block', 'done'] as const; // Nur die im aktuellen Duo-Flow relevanten Sprungziele. `pre_protection` springt
// per slideFromStep direkt zum Protection-Step (überspringt Welcome/Nickname/Plan)
// — das ist der schnellste Weg, den Schutz-Onboarding-Flow zu testen.
const ONBOARDING_STEPS = ['welcome', 'pre_protection', 'done'] as const;
type OnboardingStepValue = (typeof ONBOARDING_STEPS)[number]; type OnboardingStepValue = (typeof ONBOARDING_STEPS)[number];
function OnboardingResetToggle({ function OnboardingResetToggle({
@ -932,7 +1063,7 @@ function OnboardingResetToggle({
currentStep, currentStep,
}: { }: {
colors: import('../lib/theme').ColorScheme; colors: import('../lib/theme').ColorScheme;
currentStep: OnboardingStepValue; currentStep: OnboardingStep;
}) { }) {
const router = useRouter(); const router = useRouter();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
@ -946,18 +1077,12 @@ function OnboardingResetToggle({
body: { step }, body: { step },
}); });
invalidateMe(); invalidateMe();
if (step === 'welcome') { // done → zurück in die App; alles andere (welcome, pre_protection) → /onboarding,
// /onboarding/welcome existiert nicht mehr (Duo-Rewrite → /onboarding/index.tsx). // wo slideFromStep den passenden Resume-Slide auflöst.
// Korrekter Pfad ist /onboarding (Expo Router löst index.tsx auto auf). if (step === 'done') {
router.replace('/onboarding');
} else if (step === 'nickname') {
// Legacy-Stage — Duo-Flow navigiert intern; /onboarding triggert via
// slideFromStep den Resume zur Nickname-Slide.
router.replace('/onboarding');
} else if (step === 'block') {
router.replace('/(app)/blocker');
} else if (step === 'done') {
router.replace('/(app)'); router.replace('/(app)');
} else {
router.replace('/onboarding');
} }
} catch (e: unknown) { } catch (e: unknown) {
Alert.alert('Fehler', e instanceof Error ? e.message : String(e)); Alert.alert('Fehler', e instanceof Error ? e.message : String(e));

View File

@ -1,209 +1,78 @@
import { useEffect, useRef } from 'react'; import { useEffect, useState } from 'react';
import { Animated, Dimensions, Image, Text, TouchableOpacity, View } from 'react-native'; import { Image, Text, TouchableOpacity, View } from 'react-native';
import Svg, { Defs, RadialGradient, Rect, Stop } from 'react-native-svg'; import Animated, {
Easing,
useAnimatedStyle,
useSharedValue,
withDelay,
withTiming,
} from 'react-native-reanimated';
import { useRouter } from 'expo-router'; import { useRouter } from 'expo-router';
import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useAuthStore } from '../stores/auth'; import { useAuthStore } from '../stores/auth';
import { BrandSplash } from '../components/BrandSplash';
const { width: SW, height: SH } = Dimensions.get('window'); const EASE_OUT = Easing.out(Easing.cubic);
export default function LandingScreen() { export default function LandingScreen() {
const router = useRouter(); const router = useRouter();
const insets = useSafeAreaInsets(); const insets = useSafeAreaInsets();
const { t } = useTranslation(); const { t } = useTranslation();
// Reaktiver Routing-Fix für „eingeloggt bleiben": wenn beim Cold-Start (oder
// nach einem `router.replace('/')` aus dem LockScreen-Sign-Out) bereits eine
// gültige Session in AsyncStorage liegt, überspringen wir das Landing und
// schicken den User direkt in `(app)`.
const session = useAuthStore((s) => s.session); const session = useAuthStore((s) => s.session);
const loading = useAuthStore((s) => s.loading); const loading = useAuthStore((s) => s.loading);
const [splashDone, setSplashDone] = useState(false);
const ctaOpacity = useSharedValue(0);
const ctaTranslateY = useSharedValue(14);
useEffect(() => { useEffect(() => {
if (!loading && session) { if (!loading && session) {
router.replace('/(app)'); router.replace('/(app)');
} }
}, [loading, session, router]); }, [loading, session, router]);
const glowTopOpacity = useRef(new Animated.Value(0.5)).current; function handleSplashDone() {
const glowCenterOpacity = useRef(new Animated.Value(0)).current; setSplashDone(true);
const glowCenterScale = useRef(new Animated.Value(0.6)).current; ctaOpacity.value = withTiming(1, { duration: 380, easing: EASE_OUT });
ctaTranslateY.value = withTiming(0, { duration: 380, easing: EASE_OUT });
}
const nameOpacity = useRef(new Animated.Value(0)).current; const ctaStyle = useAnimatedStyle(() => ({
const nameTranslateY = useRef(new Animated.Value(12)).current; opacity: ctaOpacity.value,
transform: [{ translateY: ctaTranslateY.value }],
}));
const logoOpacity = useRef(new Animated.Value(0)).current;
const logoScale = useRef(new Animated.Value(0.82)).current;
const logoTranslateY = useRef(new Animated.Value(8)).current;
const logoPulse = useRef(new Animated.Value(1)).current;
const taglineOpacity = useRef(new Animated.Value(0)).current;
const taglineTranslateY = useRef(new Animated.Value(8)).current;
const ctaOpacity = useRef(new Animated.Value(0)).current;
const ctaTranslateY = useRef(new Animated.Value(10)).current;
const footerOpacity = useRef(new Animated.Value(0)).current;
useEffect(() => {
Animated.loop(
Animated.sequence([
Animated.timing(glowTopOpacity, { toValue: 0.9, duration: 2000, useNativeDriver: true }),
Animated.timing(glowTopOpacity, { toValue: 0.5, duration: 2000, useNativeDriver: true }),
]),
).start();
const ease = (toValue: number, duration: number) => ({ toValue, duration, useNativeDriver: true });
Animated.parallel([
Animated.timing(glowCenterOpacity, ease(1, 900)),
Animated.timing(glowCenterScale, ease(1, 900)),
]).start();
setTimeout(() => {
Animated.parallel([
Animated.timing(nameOpacity, ease(1, 600)),
Animated.timing(nameTranslateY, ease(0, 600)),
]).start();
}, 300);
setTimeout(() => {
Animated.parallel([
Animated.timing(logoOpacity, ease(1, 650)),
Animated.spring(logoScale, { toValue: 1, useNativeDriver: true, friction: 6, tension: 80 }),
Animated.timing(logoTranslateY, ease(0, 650)),
]).start();
}, 700);
setTimeout(() => {
Animated.loop(
Animated.sequence([
Animated.timing(logoPulse, { toValue: 1.04, duration: 1300, useNativeDriver: true }),
Animated.timing(logoPulse, { toValue: 1, duration: 1300, useNativeDriver: true }),
]),
).start();
}, 1100);
setTimeout(() => {
Animated.parallel([
Animated.timing(taglineOpacity, ease(1, 550)),
Animated.timing(taglineTranslateY, ease(0, 550)),
]).start();
}, 1300);
setTimeout(() => {
Animated.parallel([
Animated.timing(ctaOpacity, ease(1, 500)),
Animated.timing(ctaTranslateY, ease(0, 500)),
Animated.timing(footerOpacity, ease(1, 600)),
]).start();
}, 1700);
}, [
glowTopOpacity, glowCenterOpacity, glowCenterScale,
nameOpacity, nameTranslateY, logoOpacity, logoScale, logoTranslateY,
logoPulse, taglineOpacity, taglineTranslateY, ctaOpacity, ctaTranslateY, footerOpacity,
]);
// Early-return MUSS nach allen Hooks stehen (Rules of Hooks) — sonst wirft
// React "Rendered fewer hooks than expected" wenn sich loading/session zwischen
// Renders ändert.
if (loading || session) return null; if (loading || session) return null;
if (!splashDone) {
return <BrandSplash onDone={handleSplashDone} />;
}
return ( return (
<View style={{ flex: 1, backgroundColor: '#0f172a', overflow: 'hidden' }}> <View style={{ flex: 1, backgroundColor: '#0f172a', overflow: 'hidden' }}>
{/* Top breathing glow */} <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center', paddingHorizontal: 24, gap: 48 }}>
<Animated.View <View style={{ alignItems: 'center', gap: 20 }}>
pointerEvents="none"
style={{ position: 'absolute', top: 0, left: 0, right: 0, height: SH * 0.5, opacity: glowTopOpacity }}
>
<Svg width="100%" height="100%">
<Defs>
<RadialGradient id="topGlowL" cx="50%" cy="0%" rx="70%" ry="100%" fx="50%" fy="0%">
<Stop offset="0%" stopColor="#1e3a8a" stopOpacity="1" />
<Stop offset="100%" stopColor="#1e3a8a" stopOpacity="0" />
</RadialGradient>
</Defs>
<Rect width="100%" height="100%" fill="url(#topGlowL)" />
</Svg>
</Animated.View>
{/* Center indigo halo */}
<Animated.View
pointerEvents="none"
style={{
position: 'absolute', top: 0, left: 0, right: 0, bottom: 0,
opacity: glowCenterOpacity, transform: [{ scale: glowCenterScale }],
}}
>
<Svg width="100%" height="100%">
<Defs>
<RadialGradient id="centerHaloL" cx="50%" cy="45%" rx="55%" ry="55%">
<Stop offset="0%" stopColor="#6366f1" stopOpacity="0.22" />
<Stop offset="100%" stopColor="#6366f1" stopOpacity="0" />
</RadialGradient>
</Defs>
<Rect width="100%" height="100%" fill="url(#centerHaloL)" />
</Svg>
</Animated.View>
{/* Content */}
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center', gap: 20, paddingHorizontal: 24 }}>
<Animated.Text
style={{
fontFamily: 'Nunito_800ExtraBold',
fontSize: 48,
letterSpacing: -1,
color: '#ffffff',
textAlign: 'center',
marginBottom: 8,
opacity: nameOpacity,
transform: [{ translateY: nameTranslateY }],
}}
>
{t('appHeader.appName')}
</Animated.Text>
<Animated.View
style={{
opacity: logoOpacity,
transform: [
{ scale: Animated.multiply(logoScale, logoPulse) as any },
{ translateY: logoTranslateY },
],
}}
>
<Image <Image
source={require('../assets/icon.png')} source={require('../assets/icon.png')}
style={{ width: 160, height: 160, borderRadius: 28 }} style={{ width: 96, height: 96, borderRadius: 22 }}
resizeMode="contain" resizeMode="contain"
/> />
</Animated.View> <Text
style={{
fontFamily: 'Nunito_800ExtraBold',
fontSize: 40,
letterSpacing: -1,
color: '#ffffff',
}}
>
{t('appHeader.appName')}
</Text>
</View>
<Animated.Text <Animated.View style={[{ alignSelf: 'stretch', gap: 12 }, ctaStyle]}>
style={{
fontFamily: 'Nunito_600SemiBold',
fontSize: 20,
letterSpacing: 0.2,
color: 'rgba(255,255,255,0.90)',
textAlign: 'center',
marginTop: 4,
opacity: taglineOpacity,
transform: [{ translateY: taglineTranslateY }],
}}
>
{t('splash.tagline')}
</Animated.Text>
<Animated.View
style={{
alignSelf: 'stretch',
gap: 12,
marginTop: 16,
opacity: ctaOpacity,
transform: [{ translateY: ctaTranslateY }],
}}
>
<TouchableOpacity <TouchableOpacity
onPress={() => router.push('/signin')} onPress={() => router.push('/signin')}
activeOpacity={0.85} activeOpacity={0.85}
@ -237,8 +106,7 @@ export default function LandingScreen() {
</Animated.View> </Animated.View>
</View> </View>
{/* Footer */} <Text
<Animated.Text
style={{ style={{
position: 'absolute', position: 'absolute',
bottom: insets.bottom + 16, bottom: insets.bottom + 16,
@ -250,11 +118,10 @@ export default function LandingScreen() {
textTransform: 'uppercase', textTransform: 'uppercase',
color: 'rgba(255,255,255,0.28)', color: 'rgba(255,255,255,0.28)',
textAlign: 'center', textAlign: 'center',
opacity: footerOpacity,
}} }}
> >
{t('splash.madeInGermany')} {t('splash.madeInGermany')}
</Animated.Text> </Text>
</View> </View>
); );
} }

View File

@ -244,14 +244,15 @@ export default function CoachScreen() {
function handleInputChange(text: string) { function handleInputChange(text: string) {
setInput(text); setInput(text);
if (text.length > 0) { if (text.length > 0) {
setEmotion((e) => (e === 'thinking' ? e : 'happy')); // Lyra hört aufmerksam zu, während getippt wird (nicht während sie denkt).
setEmotion((e) => (e === 'thinking' ? e : 'listening'));
if (typingTimer.current) clearTimeout(typingTimer.current); if (typingTimer.current) clearTimeout(typingTimer.current);
typingTimer.current = setTimeout(() => { typingTimer.current = setTimeout(() => {
setEmotion((e) => (e === 'happy' ? 'idle' : e)); setEmotion((e) => (e === 'listening' ? 'idle' : e));
}, 2500); }, 2500);
} else { } else {
if (typingTimer.current) clearTimeout(typingTimer.current); if (typingTimer.current) clearTimeout(typingTimer.current);
setEmotion((e) => (e === 'happy' ? 'idle' : e)); setEmotion((e) => (e === 'listening' ? 'idle' : e));
} }
} }
@ -416,6 +417,7 @@ export default function CoachScreen() {
await rec.startAsync(); await rec.startAsync();
recordingRef.current = rec; recordingRef.current = rec;
setIsRecording(true); setIsRecording(true);
setEmotion('listening'); // Lyra hört zu, während aufgenommen wird
startRecordingTimer(); startRecordingTimer();
} catch { } catch {
micHeld.current = false; micHeld.current = false;
@ -430,6 +432,7 @@ export default function CoachScreen() {
recordingRef.current = null; recordingRef.current = null;
micHeld.current = false; micHeld.current = false;
stopRecordingTimer(); stopRecordingTimer();
setEmotion('idle'); // Aufnahme verworfen → zurück in den Ruhezustand
setTrashFlash(true); setTrashFlash(true);
setTimeout(() => { setTimeout(() => {
setTrashFlash(false); setTrashFlash(false);
@ -459,7 +462,7 @@ export default function CoachScreen() {
if (!uri) return; if (!uri) return;
setIsTranscribing(true); setIsTranscribing(true);
setEmotion('happy'); setEmotion('listening'); // verarbeitet die Stimme weiter (vor dem "thinking" in handleVoiceSend)
try { try {
// Backend erwartet base64-Audio in JSON-Body (NICHT FormData): // Backend erwartet base64-Audio in JSON-Body (NICHT FormData):

View File

@ -733,12 +733,17 @@ export default function SOSScreen() {
function startBreathing() { function startBreathing() {
// Laufendes Lyra-TTS stoppen — sonst spricht sie in die Atemübung rein // Laufendes Lyra-TTS stoppen — sonst spricht sie in die Atemübung rein
stopSpeaking(); stopSpeaking();
// Lyra atmet ruhig mit (Header-Avatar bleibt während des Drawers sichtbar).
// Pending Emotion-Reset abbrechen, sonst springt sie nach 4-6s aus 'calm'.
if (emotionTimer.current) clearTimeout(emotionTimer.current);
setEmotion('calm');
setIsBreathing(true); setIsBreathing(true);
setChipSet('none'); setChipSet('none');
} }
async function handleBreathingDone() { async function handleBreathingDone() {
setIsBreathing(false); setIsBreathing(false);
setEmotion('idle'); // raus aus 'calm' — die after_breathing-Antwort setzt danach die erkannte Emotion
setBreathingDone(true); setBreathingDone(true);
breathingCountRef.current += 1; breathingCountRef.current += 1;
setChipSet('after_breathing'); setChipSet('after_breathing');
@ -1290,7 +1295,7 @@ export default function SOSScreen() {
editable={!thinking} editable={!thinking}
/> />
{input.trim() !== '' && ( {input.trim() !== '' && (
<Pressable style={[st.sendBtn, thinking && { opacity: 0.4 }]} onPress={handleSend} disabled={thinking || !input.trim()}> <Pressable testID="sos-send-btn" style={[st.sendBtn, thinking && { opacity: 0.4 }]} onPress={handleSend} disabled={thinking || !input.trim()}>
<Ionicons name="send" size={16} color="#fff" /> <Ionicons name="send" size={16} color="#fff" />
</Pressable> </Pressable>
)} )}

View File

@ -0,0 +1,14 @@
# Deprecated Assets
These files are kept in place to avoid breaking native build configuration.
Do NOT delete without first updating app.config.ts and running `expo prebuild --clean`.
## splash.png
- What: old chain-break logo on navy gradient — used as the native OS-level splash screen image
- Referenced in: app.config.ts → `splash.image`
- Replaced by: animated in-app `BrandSplash` component (`components/BrandSplash.tsx`) which runs after the native splash hides
- TODO (Orchestrator / Zied): update `app.config.ts` to use `assets/icon.png` as the native splash image (matching the in-app logo), then `expo prebuild --clean` + rebuild. A transparent logo variant (logo mark only, no rounded-square background) would be ideal for the native splash — currently not available; icon.png on #0f172a background is the fallback.
## adaptive-icon.png / adaptive-foreground.png / rebreak_android.png
- What: older Android adaptive icon variants
- Status: may still be referenced in app.config.ts android block — verify before removing

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

112
apps/rebreak-native/clean.sh Executable file
View File

@ -0,0 +1,112 @@
#!/usr/bin/env bash
# clean.sh — systematisches Reclaiming von Dev-Caches (macOS).
#
# Problem: lokale Builds + globale Toolchain-Caches (Gradle, Xcode DerivedData,
# iOS-Simulatoren) wuchern bis die Platte voll ist und Builds/Emulator failen
# ("not enough disk space"). deploy.sh räumt nur die PROJEKT-Build-Dirs;
# globale Caches verwaltet sonst niemand. Dieses Script schließt die Lücke.
#
# Tiers:
# (default) project: android/build, android/app/build, android/.gradle, ios/build
# + Xcode DerivedData (komplett) + nicht-verfügbare iOS-Sims.
# Alles regeneriert sich beim nächsten Build (kein Daten-Verlust).
# --deep zusätzlich: globaler Gradle-Cache (~/.gradle/caches) + ios/Pods.
# Frei mehr Platz, aber der NÄCHSTE Build lädt Deps/Pods neu (langsamer).
# --check nur Diagnose (df + Top-Verbraucher), löscht nichts.
#
# Nutzung:
# ./clean.sh # sicheres Reclaiming
# ./clean.sh --deep # aggressiv (global gradle + pods)
# ./clean.sh --check # nur anschauen
set -uo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ANDROID_DIR="$SCRIPT_DIR/android"
IOS_DIR="$SCRIPT_DIR/ios"
MODE="safe"
GUARD_GB="${2:-8}"
case "${1:-}" in
--deep) MODE="deep" ;;
--check) MODE="check" ;;
--guard) MODE="guard" ;;
"" ) MODE="safe" ;;
*) echo "Unbekannte Option: $1 (erlaubt: --deep, --check, --guard [GB])"; exit 1 ;;
esac
free_gb() { df -g / 2>/dev/null | awk 'NR==2 {print $4}'; }
human() { du -sh "$1" 2>/dev/null | awk '{print $1}'; }
# --guard [GB]: stiller Pre-Build-Check. Warnt nur, wenn frei < Schwelle (8G).
# Wird von dev.sh/deploy.sh vor dem Build aufgerufen — bricht NIE ab (exit 0).
if [ "$MODE" = "guard" ]; then
FREE="$(free_gb)"
if [ "${FREE:-99}" -lt "$GUARD_GB" ]; then
echo "⚠️ Nur ${FREE}G frei (< ${GUARD_GB}G) — Build/Emulator könnte an Speicher scheitern."
echo " Aufräumen: ./clean.sh (mehr: ./clean.sh --deep)"
fi
exit 0
fi
echo "── Disk-Status ────────────────────────────────────────────"
df -h / | sed -n '1p;2p'
BEFORE_GB="$(free_gb)"
echo
report_targets() {
echo "Top-Verbraucher (Dev-Caches):"
for d in "$ANDROID_DIR/app/build" "$ANDROID_DIR/build" "$ANDROID_DIR/.gradle" \
"$IOS_DIR/build" "$IOS_DIR/Pods" "$HOME/.gradle/caches" \
"$HOME/Library/Developer/Xcode/DerivedData" \
"$HOME/Library/Developer/CoreSimulator"; do
[ -e "$d" ] && printf " %-60s %s\n" "$d" "$(human "$d")"
done
}
if [ "$MODE" = "check" ]; then
report_targets
echo; echo "free: ${BEFORE_GB}G — (nur Diagnose, nichts gelöscht)"
exit 0
fi
report_targets
echo
# Gradle-Daemon stoppen, sonst sind android/build & .gradle gelockt.
echo "→ Gradle-Daemon stoppen…"
( cd "$ANDROID_DIR" 2>/dev/null && ./gradlew --stop >/dev/null 2>&1 ) || true
pkill -f "GradleDaemon" 2>/dev/null || true
rmrf() { [ -e "$1" ] && { echo " rm $1 ($(human "$1"))"; rm -rf "$1"; } || true; }
echo "→ Projekt-Build-Artefakte…"
rmrf "$ANDROID_DIR/app/build"
rmrf "$ANDROID_DIR/build"
rmrf "$ANDROID_DIR/.gradle"
rmrf "$IOS_DIR/build"
echo "→ Xcode DerivedData…"
rmrf "$HOME/Library/Developer/Xcode/DerivedData"
echo "→ nicht-verfügbare iOS-Simulatoren…"
xcrun simctl delete unavailable >/dev/null 2>&1 || true
if [ "$MODE" = "deep" ]; then
echo "→ [deep] globaler Gradle-Cache (~/.gradle/caches)…"
rmrf "$HOME/.gradle/caches"
echo "→ [deep] ios/Pods (pod install nötig danach)…"
rmrf "$IOS_DIR/Pods"
fi
echo
echo "── Ergebnis ───────────────────────────────────────────────"
df -h / | sed -n '2p'
AFTER_GB="$(free_gb)"
echo "frei vorher: ${BEFORE_GB}G → nachher: ${AFTER_GB}G (≈ $((AFTER_GB - BEFORE_GB))G zurückgewonnen)"
echo
echo "Hinweis: nächster Android-Build ist langsamer (Caches regenerieren)."
if [ "$MODE" = "deep" ]; then
echo " iOS braucht 'pod install' (ios/Pods gelöscht)."
fi
exit 0

View File

@ -111,6 +111,7 @@ export function AppHeader({ notifCount, showBack, title }: Props = {}) {
schluckt auf Android manchmal width/height 0×0 + overflow:hidden schluckt auf Android manchmal width/height 0×0 + overflow:hidden
Avatar unsichtbar (vgl. Mac-CTA-Fix 7d04e42). */} Avatar unsichtbar (vgl. Mac-CTA-Fix 7d04e42). */}
<TouchableOpacity <TouchableOpacity
testID="header-avatar-btn"
onPress={() => setMenuOpen(true)} onPress={() => setMenuOpen(true)}
hitSlop={{ top: 4, bottom: 4, left: 4, right: 4 }} hitSlop={{ top: 4, bottom: 4, left: 4, right: 4 }}
activeOpacity={0.7} activeOpacity={0.7}

View File

@ -1,409 +1,199 @@
import { useEffect, useRef } from 'react'; import { useEffect } from 'react';
import { Animated, Dimensions, Image, Text, View } from 'react-native'; import { Dimensions, Image, View } from 'react-native';
import Animated, {
Easing,
runOnJS,
useAnimatedStyle,
useReducedMotion,
useSharedValue,
withDelay,
withSequence,
withTiming,
} from 'react-native-reanimated';
import Svg, { Defs, RadialGradient, Rect, Stop } from 'react-native-svg'; import Svg, { Defs, RadialGradient, Rect, Stop } from 'react-native-svg';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
// Phase-Timings (ms ab Mount) — 1:1 portiert aus apps/rebreak/app/components/AppSplash.vue const { height: SH } = Dimensions.get('window');
const T_GLOW = 0;
const T_NAME = 300;
const T_LOGO = 700;
const T_PULSE = 1100;
const T_TAGLINE = 1300;
const T_SUB = 1700;
const T_HOLD_END = 3200;
const T_LEAVE_DUR = 500;
const { width: SW, height: SH } = Dimensions.get('window'); const EASE_OUT = Easing.out(Easing.cubic);
const EASE_IN = Easing.in(Easing.cubic);
type ParticleConfig = { type Props = {
size: number; onDone?: () => void;
top?: number;
bottom?: number;
left?: number;
right?: number;
duration: number;
delay: number;
}; };
const PARTICLES: ParticleConfig[] = [ export function BrandSplash({ onDone }: Props) {
{ size: 180, top: -40, left: -60, duration: 7000, delay: 0 },
{ size: 120, bottom: SH * 0.1, right: -30, duration: 9000, delay: 1500 },
{ size: 80, top: SH * 0.35, left: SW * 0.08, duration: 11000, delay: 800 },
{ size: 60, bottom: SH * 0.2, left: SW * 0.2, duration: 8000, delay: 2200 },
{ size: 100, top: SH * 0.15, right: SW * 0.1, duration: 10000, delay: 400 },
];
function Particle({ config }: { config: ParticleConfig }) {
const translateY = useRef(new Animated.Value(0)).current;
const scale = useRef(new Animated.Value(1)).current;
const opacity = useRef(new Animated.Value(0.6)).current;
useEffect(() => {
const animate = () => {
Animated.loop(
Animated.sequence([
Animated.parallel([
Animated.timing(translateY, {
toValue: 18,
duration: config.duration,
useNativeDriver: true,
}),
Animated.timing(scale, {
toValue: 1.1,
duration: config.duration,
useNativeDriver: true,
}),
Animated.timing(opacity, {
toValue: 1,
duration: config.duration,
useNativeDriver: true,
}),
]),
Animated.parallel([
Animated.timing(translateY, {
toValue: 0,
duration: config.duration,
useNativeDriver: true,
}),
Animated.timing(scale, {
toValue: 1,
duration: config.duration,
useNativeDriver: true,
}),
Animated.timing(opacity, {
toValue: 0.6,
duration: config.duration,
useNativeDriver: true,
}),
]),
]),
).start();
};
const t = setTimeout(animate, config.delay);
return () => clearTimeout(t);
}, [config, translateY, scale, opacity]);
return (
<Animated.View
pointerEvents="none"
style={{
position: 'absolute',
width: config.size,
height: config.size,
borderRadius: config.size / 2,
backgroundColor: 'rgba(99, 102, 241, 0.12)',
top: config.top,
bottom: config.bottom,
left: config.left,
right: config.right,
opacity,
transform: [{ translateY }, { scale }],
}}
/>
);
}
export function BrandSplash() {
const { t } = useTranslation(); const { t } = useTranslation();
// Phase-Opacity-Animationen const reducedMotion = useReducedMotion();
const containerOpacity = useRef(new Animated.Value(1)).current;
const glowCenterOpacity = useRef(new Animated.Value(0)).current;
const glowCenterScale = useRef(new Animated.Value(0.6)).current;
const glowTopOpacity = useRef(new Animated.Value(0.5)).current;
const nameOpacity = useRef(new Animated.Value(0)).current; const containerOpacity = useSharedValue(1);
const nameTranslateY = useRef(new Animated.Value(12)).current; const logoOpacity = useSharedValue(0);
const logoScale = useSharedValue(reducedMotion ? 1 : 0.88);
const logoOpacity = useRef(new Animated.Value(0)).current; const nameOpacity = useSharedValue(0);
const logoScale = useRef(new Animated.Value(0.82)).current; const nameTranslateY = useSharedValue(reducedMotion ? 0 : 10);
const logoTranslateY = useRef(new Animated.Value(8)).current; const taglineOpacity = useSharedValue(0);
const logoPulse = useRef(new Animated.Value(1)).current; const taglineTranslateY = useSharedValue(reducedMotion ? 0 : 8);
const glowOpacity = useSharedValue(0);
const taglineOpacity = useRef(new Animated.Value(0)).current; const footerOpacity = useSharedValue(0);
const taglineTranslateY = useRef(new Animated.Value(8)).current;
const subOpacity = useRef(new Animated.Value(0)).current;
const subTranslateY = useRef(new Animated.Value(6)).current;
const footerOpacity = useRef(new Animated.Value(0)).current;
useEffect(() => { useEffect(() => {
// Top-glow breath loop (4s alternating) — startet sofort if (reducedMotion) {
Animated.loop( const dur = 500;
Animated.sequence([ const cfg = { duration: dur, easing: EASE_OUT };
Animated.timing(glowTopOpacity, { logoOpacity.value = withTiming(1, cfg);
toValue: 0.9, nameOpacity.value = withTiming(1, cfg);
duration: 2000, taglineOpacity.value = withTiming(1, cfg);
useNativeDriver: true, footerOpacity.value = withTiming(1, cfg);
containerOpacity.value = withDelay(
2700,
withTiming(0, { duration: 400, easing: EASE_IN }, (finished) => {
if (finished && onDone) runOnJS(onDone)();
}), }),
Animated.timing(glowTopOpacity, { );
toValue: 0.5, return;
duration: 2000, }
useNativeDriver: true,
}),
]),
).start();
const ease = (toValue: number, duration: number) => ({ const revealCfg = (duration: number) => ({ duration, easing: EASE_OUT });
toValue,
duration,
useNativeDriver: true,
});
// Phase 1: glow center bloom (T=0) glowOpacity.value = withTiming(1, revealCfg(1000));
Animated.parallel([
Animated.timing(glowCenterOpacity, ease(1, 900)),
Animated.timing(glowCenterScale, ease(1, 900)),
]).start();
// Phase 2: Name fade-in (T=300) logoOpacity.value = withDelay(100, withTiming(1, revealCfg(700)));
setTimeout(() => { logoScale.value = withDelay(100, withTiming(1, revealCfg(750)));
Animated.parallel([
Animated.timing(nameOpacity, ease(1, 600)),
Animated.timing(nameTranslateY, ease(0, 600)),
]).start();
}, T_NAME);
// Phase 3: Logo bouncy scale-in (T=700) nameOpacity.value = withDelay(250, withTiming(1, revealCfg(600)));
setTimeout(() => { nameTranslateY.value = withDelay(250, withTiming(0, revealCfg(600)));
Animated.parallel([
Animated.timing(logoOpacity, ease(1, 650)),
Animated.spring(logoScale, {
toValue: 1,
useNativeDriver: true,
friction: 6,
tension: 80,
}),
Animated.timing(logoTranslateY, ease(0, 650)),
]).start();
}, T_LOGO);
// Phase 3b: Logo breathing pulse (T=1100) taglineOpacity.value = withDelay(500, withTiming(1, revealCfg(550)));
setTimeout(() => { taglineTranslateY.value = withDelay(500, withTiming(0, revealCfg(550)));
Animated.loop(
Animated.sequence([
Animated.timing(logoPulse, {
toValue: 1.04,
duration: 1300,
useNativeDriver: true,
}),
Animated.timing(logoPulse, {
toValue: 1,
duration: 1300,
useNativeDriver: true,
}),
]),
).start();
}, T_PULSE);
// Phase 4: Tagline (T=1300) footerOpacity.value = withDelay(700, withTiming(1, revealCfg(500)));
setTimeout(() => {
Animated.parallel([
Animated.timing(taglineOpacity, ease(1, 550)),
Animated.timing(taglineTranslateY, ease(0, 550)),
]).start();
}, T_TAGLINE);
// Phase 5: Sub-text + Footer (T=1700) containerOpacity.value = withDelay(
setTimeout(() => { 3300,
Animated.parallel([ withTiming(0, { duration: 450, easing: EASE_IN }, (finished) => {
Animated.timing(subOpacity, ease(1, 500)), if (finished && onDone) runOnJS(onDone)();
Animated.timing(subTranslateY, ease(0, 500)), }),
Animated.timing(footerOpacity, ease(1, 600)), );
]).start(); }, []);
}, T_SUB);
// Phase 7: whole-screen fade-out (T=3200, dauert 500ms) const containerStyle = useAnimatedStyle(() => ({
const fadeOutTimer = setTimeout(() => { opacity: containerOpacity.value,
Animated.timing(containerOpacity, { }));
toValue: 0,
duration: T_LEAVE_DUR,
useNativeDriver: true,
}).start();
}, T_HOLD_END);
return () => clearTimeout(fadeOutTimer); const glowStyle = useAnimatedStyle(() => ({
}, [ opacity: glowOpacity.value,
glowTopOpacity, }));
glowCenterOpacity,
glowCenterScale, const logoStyle = useAnimatedStyle(() => ({
nameOpacity, opacity: logoOpacity.value,
nameTranslateY, transform: [{ scale: logoScale.value }],
logoOpacity, }));
logoScale,
logoTranslateY, const nameStyle = useAnimatedStyle(() => ({
logoPulse, opacity: nameOpacity.value,
taglineOpacity, transform: [{ translateY: nameTranslateY.value }],
taglineTranslateY, }));
subOpacity,
subTranslateY, const taglineStyle = useAnimatedStyle(() => ({
footerOpacity, opacity: taglineOpacity.value,
containerOpacity, transform: [{ translateY: taglineTranslateY.value }],
]); }));
const footerStyle = useAnimatedStyle(() => ({
opacity: footerOpacity.value,
}));
return ( return (
<Animated.View <Animated.View style={[{ flex: 1, backgroundColor: '#0f172a', overflow: 'hidden' }, containerStyle]}>
style={{
flex: 1,
backgroundColor: '#0f172a',
opacity: containerOpacity,
overflow: 'hidden',
}}
>
{/* Top breathing radial-gradient ellipse (#1e3a8a auf transparent) */}
<Animated.View <Animated.View
pointerEvents="none" pointerEvents="none"
style={{ style={[{ position: 'absolute', top: 0, left: 0, right: 0, height: SH * 0.55 }, glowStyle]}
position: 'absolute',
top: 0,
left: 0,
right: 0,
height: SH * 0.5,
opacity: glowTopOpacity,
}}
> >
<Svg width="100%" height="100%"> <Svg width="100%" height="100%">
<Defs> <Defs>
<RadialGradient <RadialGradient id="splashTopGlow" cx="50%" cy="0%" rx="70%" ry="100%" fx="50%" fy="0%">
id="topGlow" <Stop offset="0%" stopColor="#1e3a8a" stopOpacity="0.9" />
cx="50%"
cy="0%"
rx="70%"
ry="100%"
fx="50%"
fy="0%"
>
<Stop offset="0%" stopColor="#1e3a8a" stopOpacity="1" />
<Stop offset="100%" stopColor="#1e3a8a" stopOpacity="0" /> <Stop offset="100%" stopColor="#1e3a8a" stopOpacity="0" />
</RadialGradient> </RadialGradient>
</Defs> </Defs>
<Rect width="100%" height="100%" fill="url(#topGlow)" /> <Rect width="100%" height="100%" fill="url(#splashTopGlow)" />
</Svg> </Svg>
</Animated.View> </Animated.View>
{/* Center indigo halo — bloomt rein wenn Logo erscheint */}
<Animated.View <Animated.View
pointerEvents="none" pointerEvents="none"
style={{ style={[{ position: 'absolute', top: 0, left: 0, right: 0, bottom: 0 }, glowStyle]}
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
opacity: glowCenterOpacity,
transform: [{ scale: glowCenterScale }],
}}
> >
<Svg width="100%" height="100%"> <Svg width="100%" height="100%">
<Defs> <Defs>
<RadialGradient id="centerHalo" cx="50%" cy="52%" rx="55%" ry="55%"> <RadialGradient id="splashAccentHalo" cx="50%" cy="50%" rx="45%" ry="45%">
<Stop offset="0%" stopColor="#6366f1" stopOpacity="0.22" /> <Stop offset="0%" stopColor="#2E7FD4" stopOpacity="0.14" />
<Stop offset="100%" stopColor="#6366f1" stopOpacity="0" /> <Stop offset="100%" stopColor="#2E7FD4" stopOpacity="0" />
</RadialGradient> </RadialGradient>
</Defs> </Defs>
<Rect width="100%" height="100%" fill="url(#centerHalo)" /> <Rect width="100%" height="100%" fill="url(#splashAccentHalo)" />
</Svg> </Svg>
</Animated.View> </Animated.View>
{/* Floating particles (5 Stück) */} <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center', gap: 20, paddingHorizontal: 16 }}>
{PARTICLES.map((p, i) => (
<Particle key={i} config={p} />
))}
{/* Content-Column */}
<View
style={{
flex: 1,
alignItems: 'center',
justifyContent: 'center',
gap: 20,
paddingHorizontal: 16,
}}
>
{/* App-Name */}
<Animated.Text <Animated.Text
style={{ style={[
fontFamily: 'Nunito_800ExtraBold', {
fontSize: 48, fontFamily: 'Nunito_800ExtraBold',
letterSpacing: -1, fontSize: 48,
color: '#ffffff', letterSpacing: -1,
textAlign: 'center', color: '#ffffff',
marginBottom: 8, textAlign: 'center',
opacity: nameOpacity, marginBottom: 4,
transform: [{ translateY: nameTranslateY }], },
}} nameStyle,
]}
> >
{t('appHeader.appName')} {t('appHeader.appName')}
</Animated.Text> </Animated.Text>
{/* Logo (mit Pulse + Bouncy Entry) */} <Animated.View style={logoStyle}>
<Animated.View
style={{
opacity: logoOpacity,
transform: [
{ scale: Animated.multiply(logoScale, logoPulse) as any },
{ translateY: logoTranslateY },
],
}}
>
<Image <Image
source={require('../assets/icon.png')} source={require('../assets/icon.png')}
style={{ style={{ width: 152, height: 152, borderRadius: 26 }}
width: 160,
height: 160,
borderRadius: 28,
}}
resizeMode="contain" resizeMode="contain"
/> />
</Animated.View> </Animated.View>
{/* Tagline */}
<Animated.Text <Animated.Text
style={{ style={[
fontFamily: 'Nunito_600SemiBold', {
fontSize: 20, fontFamily: 'Nunito_600SemiBold',
letterSpacing: 0.2, fontSize: 19,
color: 'rgba(255, 255, 255, 0.90)', letterSpacing: 0.1,
textAlign: 'center', color: 'rgba(255,255,255,0.88)',
marginTop: 4, textAlign: 'center',
opacity: taglineOpacity, marginTop: 4,
transform: [{ translateY: taglineTranslateY }], },
}} taglineStyle,
]}
> >
{t('splash.tagline')} {t('splash.tagline')}
</Animated.Text> </Animated.Text>
{/* Sub-text */}
<Animated.Text
style={{
fontFamily: 'Nunito_400Regular',
fontSize: 14,
letterSpacing: 0.6,
color: 'rgba(255, 255, 255, 0.55)',
textAlign: 'center',
opacity: subOpacity,
transform: [{ translateY: subTranslateY }],
}}
>
{t('splash.subtitle')}
</Animated.Text>
</View> </View>
{/* Footer */}
<Animated.Text <Animated.Text
style={{ style={[
position: 'absolute', {
bottom: 32, position: 'absolute',
left: 0, bottom: 32,
right: 0, left: 0,
fontFamily: 'Nunito_400Regular', right: 0,
fontSize: 11, fontFamily: 'Nunito_400Regular',
letterSpacing: 1.5, fontSize: 11,
textTransform: 'uppercase', letterSpacing: 1.5,
color: 'rgba(255, 255, 255, 0.28)', textTransform: 'uppercase',
textAlign: 'center', color: 'rgba(255,255,255,0.28)',
opacity: footerOpacity, textAlign: 'center',
}} },
footerStyle,
]}
> >
{t('splash.madeInGermany')} {t('splash.madeInGermany')}
</Animated.Text> </Animated.Text>

View File

@ -1,7 +1,7 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { View, Text, Platform } from 'react-native'; import { View, Text, Platform } from 'react-native';
import { Asset } from 'expo-asset'; import { Asset } from 'expo-asset';
import Rive, { Fit, Alignment } from 'rive-react-native'; import Rive, { Fit, Alignment, RNRiveErrorType, type RNRiveError } from 'rive-react-native';
// Android: Rive akzeptiert NUR raw-resource oder url, kein file:// uri. // Android: Rive akzeptiert NUR raw-resource oder url, kein file:// uri.
// Asset liegt als android/app/src/main/res/raw/lyra_avatar.riv (gebundelt // Asset liegt als android/app/src/main/res/raw/lyra_avatar.riv (gebundelt
@ -40,24 +40,86 @@ function preloadRiveAsset(): Promise<string | null> {
preloadRiveAsset(); preloadRiveAsset();
// Supported emotions sind durch state-machine im .riv-file definiert. // Supported emotions sind durch state-machine im .riv-file definiert.
// Neue states: nur EMOTION_ANIMATIONS + EMOTION_LABELS erweitern, kein weiterer Code-Change nötig. // Neue states: nur EMOTION_ANIMATIONS + EMOTION_LABELS (+ ggf. TWO_PHASE/SUSTAINED)
export type SupportedEmotion = 'idle' | 'happy' | 'thinking' | 'empathy'; // erweitern, kein weiterer Code-Change nötig.
export type SupportedEmotion =
| 'idle'
| 'happy'
| 'empathy'
| 'thinking'
| 'listening'
| 'calm'
| 'sad'
| 'joy'
| 'confusion'
| 'surprise';
export type Emotion = SupportedEmotion | (string & {}); export type Emotion = SupportedEmotion | (string & {});
// Direkte Animation-Namen aus der .riv-Datei (1:1 mit Nuxt BenAvatar.vue). // Direkte Timeline-Namen aus der .riv-Datei (Code-Contract — siehe
// "happy" hat zwei Phasen: erst Übergangs-Animation, dann Loop. // docs/RIVE_ANIMATOR_BRIEF.md). Der Animator MUSS die States exakt so benennen,
const EMOTION_ANIMATIONS: Record<string, string> = { // sonst spielt nichts (silent, kein Error). Bis die erweiterte .riv geliefert
// wird, existieren die neuen Timelines noch nicht → der Avatar zeigt für diese
// States den statischen Idle-Frame (kein Crash). "thinking" ersetzt den alten
// "WALK"-Platzhalter und animiert daher erst mit der neuen .riv wieder.
export const EMOTION_ANIMATIONS: Record<string, string> = {
idle: 'Idle Loop', idle: 'Idle Loop',
happy: 'idle to Pose 1', // Reuse vorhandener Hasen-Posen (0 Rive-Arbeit). Namen, die es noch NICHT in
thinking: 'WALK', // der .riv gibt (calm/sad/joy/confusion/surprise), fallen via safeAnim auf Idle.
empathy: '01 Wave 1', happy: '01 Wave 2', // fröhliches Winken (war ungenutzt)
empathy: '01 Wave 1', // sanftes Winken
thinking: 'Pose 1 loop', // Klemmbrett: arbeitet an deiner Eingabe (direkt Loop, kein Zucken)
listening: 'Pose 1 loop', // Therapeut notiert beim Zuhören (direkt Loop, kein Zucken)
calm: 'calm',
sad: 'sad',
joy: 'joy',
confusion: 'confusion',
surprise: 'surprise',
}; };
// Timelines, die AKTUELL wirklich in lyra-avatar.riv existieren. Wird ein Name an
// die native Rive-View gegeben, der NICHT existiert, crasht die App hart — onError
// fängt das NICHT zuverlässig ab (empirisch verifiziert, der native Runtime stürzt
// tiefer ab). Deshalb mappen wir jeden unbekannten Namen VOR dem Rendern auf den
// garantiert vorhandenen Idle-Loop. → Beim Anlegen einer neuen Timeline in der
// .riv hier den exakten Namen ergänzen, dann animiert der zugehörige State.
export const EXISTING_TIMELINES = new Set<string>([
'Idle Loop',
'idle to Pose 1',
'Pose 1 loop',
'01 Wave 1',
'01 Wave 2',
'WALK',
'Kedip',
]);
function safeAnim(name: string): string {
return EXISTING_TIMELINES.has(name) ? name : EMOTION_ANIMATIONS.idle;
}
// Mehrphasige States: Intro-Timeline läuft einmal, danach Loop-Timeline. Aktuell
// leer — der Remount beim Intro→Loop-Wechsel verursacht ein sichtbares Zucken,
// daher spielen thinking/listening direkt die Loop-Pose. Mechanismus bleibt für
// künftige '<state> intro' + '<state> loop'-States verfügbar.
const TWO_PHASE: Record<string, { loop: string; introMs: number }> = {};
// Sustained = bleibt aktiv bis die Emotion explizit wechselt (z.B. solange Lyra
// "denkt", "zuhört" oder mitatmet). Alle anderen sind One-Shot-Reaktionen und
// fallen nach SETTLE_MS in den Idle-Loop zurück, damit der Avatar lebendig bleibt
// und nie auf dem letzten Frame einfriert.
const SUSTAINED = new Set<string>(['idle', 'thinking', 'listening', 'calm']);
const SETTLE_MS = 2600;
const EMOTION_LABELS: Record<string, string> = { const EMOTION_LABELS: Record<string, string> = {
idle: 'bereit', idle: 'bereit',
happy: 'froh für dich', happy: 'froh für dich',
thinking: 'überlegt ...',
empathy: 'versteht dich', empathy: 'versteht dich',
thinking: 'überlegt ...',
listening: 'hört zu',
calm: 'atmet mit dir',
sad: 'fühlt mit dir',
joy: 'freut sich für dich',
confusion: 'fragt nach',
surprise: 'überrascht',
}; };
const SIZE_PX: Record<'sm' | 'md' | 'lg', number> = { const SIZE_PX: Record<'sm' | 'md' | 'lg', number> = {
@ -79,7 +141,7 @@ export function RiveAvatar({ emotion, size = 'md', showLabel = false, fallback =
const resolvedEmotion = EMOTION_ANIMATIONS[emotion] !== undefined ? emotion : fallback; const resolvedEmotion = EMOTION_ANIMATIONS[emotion] !== undefined ? emotion : fallback;
// Aktuelle Animation als deklarativer State (kein imperatives ref.play()). // Aktuelle Animation als deklarativer State (kein imperatives ref.play()).
const [currentAnim, setCurrentAnim] = useState<string>(EMOTION_ANIMATIONS[resolvedEmotion] ?? EMOTION_ANIMATIONS.idle); const [currentAnim, setCurrentAnim] = useState<string>(safeAnim(EMOTION_ANIMATIONS[resolvedEmotion] ?? EMOTION_ANIMATIONS.idle));
// Lokale URI für die .riv-Datei — geht über expo-asset damit der File // Lokale URI für die .riv-Datei — geht über expo-asset damit der File
// im App-Sandbox gecached wird statt jedes Mal von Metro zu streamen. // im App-Sandbox gecached wird statt jedes Mal von Metro zu streamen.
@ -98,24 +160,43 @@ export function RiveAvatar({ emotion, size = 'md', showLabel = false, fallback =
}, [riveUri]); }, [riveUri]);
useEffect(() => { useEffect(() => {
if (resolvedEmotion === 'happy') { const wanted = EMOTION_ANIMATIONS[resolvedEmotion] ?? EMOTION_ANIMATIONS.idle;
// 2-Phasen-Flow: Übergang (~900ms) → Loop (1:1 wie Nuxt-BenAvatar) const base = safeAnim(wanted);
setCurrentAnim('idle to Pose 1'); setCurrentAnim(base);
const t = setTimeout(() => setCurrentAnim('Pose 1 loop'), 900);
// Mehrphasig (z.B. happy: 'idle to Pose 1' → 'Pose 1 loop'): Intro einmal
// spielen, dann in den Loop blenden. Nur wenn BEIDE Timelines wirklich
// existieren — sonst bleibt's bei base (kein Sprung auf einen toten Namen).
const phase = TWO_PHASE[resolvedEmotion];
if (phase && EXISTING_TIMELINES.has(wanted) && EXISTING_TIMELINES.has(phase.loop)) {
const t = setTimeout(() => setCurrentAnim(phase.loop), phase.introMs);
return () => clearTimeout(t); return () => clearTimeout(t);
} }
const anim = EMOTION_ANIMATIONS[resolvedEmotion] ?? EMOTION_ANIMATIONS.idle;
setCurrentAnim(anim); // One-Shots (empathy/sad/joy/confusion/surprise) frieren sonst auf dem
// Intro-Emotions wie 'empathy' ('01 Wave 1') / 'thinking' ('WALK') sind // letzten Frame ein → nach SETTLE_MS zurück in den Idle-Loop. Sustained-
// One-Shots — ohne Übergang frieren sie auf dem letzten Frame ein und wirken // States (idle/happy/thinking/listening/calm) bleiben aktiv bis Emotion-Wechsel.
// "nicht animiert". Nach dem Intro in den Idle Loop fallen, damit der Avatar if (!SUSTAINED.has(resolvedEmotion) && base !== EMOTION_ANIMATIONS.idle) {
// lebendig bleibt (idle loopt selbst). const t = setTimeout(() => setCurrentAnim(EMOTION_ANIMATIONS.idle), SETTLE_MS);
if (anim !== EMOTION_ANIMATIONS.idle) {
const t = setTimeout(() => setCurrentAnim(EMOTION_ANIMATIONS.idle), 2600);
return () => clearTimeout(t); return () => clearTimeout(t);
} }
}, [resolvedEmotion]); }, [resolvedEmotion]);
// Crash-Guard: rive-react-native crasht NATIV (App-Absturz, kein JS-Error),
// wenn animationName nicht in der .riv existiert — z.B. ein neuer Emotion-State,
// der noch nicht gebaut wurde. Sobald onError gesetzt ist, schaltet die Lib auf
// isUserHandlingErrors und ruft statt zu crashen diesen Handler. Wir fallen dann
// auf den garantiert vorhandenen Idle-Loop zurück (Guard verhindert Endlos-Reset).
const handleRiveError = (err: RNRiveError) => {
if (__DEV__) console.warn('[RiveAvatar]', err?.type, '—', err?.message);
if (
err?.type === RNRiveErrorType.IncorrectAnimationName &&
currentAnim !== EMOTION_ANIMATIONS.idle
) {
setCurrentAnim(EMOTION_ANIMATIONS.idle);
}
};
return ( return (
<View style={{ alignItems: 'center', gap: 4 }}> <View style={{ alignItems: 'center', gap: 4 }}>
<View <View
@ -149,6 +230,7 @@ export function RiveAvatar({ emotion, size = 'md', showLabel = false, fallback =
key={currentAnim} key={currentAnim}
resourceName={ANDROID_RIVE_RESOURCE} resourceName={ANDROID_RIVE_RESOURCE}
autoplay autoplay
onError={handleRiveError}
animationName={currentAnim} animationName={currentAnim}
fit={Fit.Cover} fit={Fit.Cover}
alignment={Alignment.Center} alignment={Alignment.Center}
@ -160,6 +242,7 @@ export function RiveAvatar({ emotion, size = 'md', showLabel = false, fallback =
key={currentAnim} key={currentAnim}
source={{ uri: riveUri }} source={{ uri: riveUri }}
autoplay autoplay
onError={handleRiveError}
animationName={currentAnim} animationName={currentAnim}
fit={Fit.Cover} fit={Fit.Cover}
alignment={Alignment.Center} alignment={Alignment.Center}

View File

@ -263,38 +263,51 @@ function SetupStep3({
type AndroidSetupFlowProps = { type AndroidSetupFlowProps = {
vpnActive: boolean; vpnActive: boolean;
// VPN-Aktivierung läuft → Spinner statt CTA, bis der Layer-State bestätigt ist.
vpnActivating?: boolean;
// = a11y-Service enabled UND Tamper-Lock armed (appDeletionLock). Nur "enabled" // = a11y-Service enabled UND Tamper-Lock armed (appDeletionLock). Nur "enabled"
// reicht NICHT — der a11y-Service ist ohne armed-Flag komplett passiv. // reicht NICHT — der a11y-Service ist ohne armed-Flag komplett passiv.
accessibilityLocked: boolean; accessibilityLocked: boolean;
deviceAdminActive: boolean; deviceAdminActive: boolean;
// Akku-Ausnahme (gegen Samsung-Sleep, der den a11y-Lock entbindet → Schutz weg).
batteryUnrestricted: boolean;
onActivateVpn: () => Promise<{ enabled: boolean; error?: string }>; onActivateVpn: () => Promise<{ enabled: boolean; error?: string }>;
onActivateAccessibility: () => Promise<unknown>; onActivateAccessibility: () => Promise<unknown>;
onRequestDeviceAdmin: () => Promise<{ launched: boolean }>; onRequestDeviceAdmin: () => Promise<{ launched: boolean }>;
onRequestBattery: () => Promise<{ opened: boolean; alreadyIgnored?: boolean }>;
onOpenAppDetails: () => Promise<{ opened: boolean }>;
colors: ReturnType<typeof import('../../lib/theme').useColors>; colors: ReturnType<typeof import('../../lib/theme').useColors>;
t: ReturnType<typeof import('react-i18next').useTranslation>['t']; t: ReturnType<typeof import('react-i18next').useTranslation>['t'];
}; };
export function AndroidSetupFlow({ export function AndroidSetupFlow({
vpnActive, vpnActive,
vpnActivating,
accessibilityLocked, accessibilityLocked,
deviceAdminActive, deviceAdminActive,
batteryUnrestricted,
onActivateVpn, onActivateVpn,
onActivateAccessibility, onActivateAccessibility,
onRequestDeviceAdmin, onRequestDeviceAdmin,
onRequestBattery,
onOpenAppDetails,
colors, colors,
t, t,
}: AndroidSetupFlowProps) { }: AndroidSetupFlowProps) {
// Reihenfolge KRITISCH: VPN → Geräteadmin → a11y. a11y MUSS zuletzt, weil der // Reihenfolge KRITISCH: VPN → Geräteadmin → Akku-Ausnahme → a11y. a11y MUSS
// Tamper-Lock (sobald armed) die Geräteadmin-Seite blockt — sonst kann der // zuletzt, weil der Tamper-Lock (sobald armed) Settings-Seiten blockt. Die
// User den Admin gar nicht mehr aktivieren. // Akku-Ausnahme MUSS VOR a11y: sonst schläfert Samsung den frisch aktivierten
// a11y-Service wieder ein → Lock entbunden → Schutz wertlos.
const vpnDone = vpnActive; const vpnDone = vpnActive;
const adminDone = deviceAdminActive; const adminDone = deviceAdminActive;
const batteryDone = batteryUnrestricted;
const a11yDone = accessibilityLocked; const a11yDone = accessibilityLocked;
return ( return (
<View style={{ gap: 10 }}> <View style={{ gap: 10 }}>
<AndroidStep1 <AndroidStep1
done={vpnDone} done={vpnDone}
pending={!!vpnActivating && !vpnDone}
onActivate={onActivateVpn} onActivate={onActivateVpn}
colors={colors} colors={colors}
t={t} t={t}
@ -307,9 +320,18 @@ export function AndroidSetupFlow({
colors={colors} colors={colors}
t={t} t={t}
/> />
{/* Display-Step 3 = a11y / ReBreak-Schutz (Komponente AndroidStep2, i18n android_step2_*). */} {/* Display-Step 3 = Akku-Ausnahme (gegen Samsung-Sleep, der a11y entbindet). */}
<AndroidStep2 <AndroidStepBattery
unlocked={adminDone} unlocked={adminDone}
done={batteryDone}
onRequest={onRequestBattery}
onOpenDetails={onOpenAppDetails}
colors={colors}
t={t}
/>
{/* Display-Step 4 = a11y / ReBreak-Schutz (Komponente AndroidStep2, i18n android_step2_*). */}
<AndroidStep2
unlocked={batteryDone}
done={a11yDone} done={a11yDone}
onActivate={onActivateAccessibility} onActivate={onActivateAccessibility}
colors={colors} colors={colors}
@ -321,11 +343,13 @@ export function AndroidSetupFlow({
function AndroidStep1({ function AndroidStep1({
done, done,
pending,
onActivate, onActivate,
colors, colors,
t, t,
}: { }: {
done: boolean; done: boolean;
pending?: boolean;
onActivate: () => Promise<{ enabled: boolean; error?: string }>; onActivate: () => Promise<{ enabled: boolean; error?: string }>;
colors: ReturnType<typeof import('../../lib/theme').useColors>; colors: ReturnType<typeof import('../../lib/theme').useColors>;
t: ReturnType<typeof import('react-i18next').useTranslation>['t']; t: ReturnType<typeof import('react-i18next').useTranslation>['t'];
@ -347,7 +371,12 @@ function AndroidStep1({
unlocked unlocked
colors={colors} colors={colors}
> >
{!done && ( {!done && (pending ? (
<View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: 8, paddingVertical: 11, marginTop: 10 }}>
<ActivityIndicator size="small" color={colors.brandOrange} />
<Text style={{ fontSize: 13, fontFamily: 'Nunito_600SemiBold', color: colors.textMuted }}>{t('blocker.activating')}</Text>
</View>
) : (
<TouchableOpacity <TouchableOpacity
onPress={handlePress} onPress={handlePress}
activeOpacity={0.8} activeOpacity={0.8}
@ -358,7 +387,7 @@ function AndroidStep1({
: <Text style={{ fontSize: 14, fontFamily: 'Nunito_700Bold', color: '#fff' }}>{t('blocker.android_step1_cta')}</Text> : <Text style={{ fontSize: 14, fontFamily: 'Nunito_700Bold', color: '#fff' }}>{t('blocker.android_step1_cta')}</Text>
} }
</TouchableOpacity> </TouchableOpacity>
)} ))}
</SetupStepCard> </SetupStepCard>
); );
} }
@ -386,12 +415,12 @@ function AndroidStep2({
return ( return (
<SetupStepCard <SetupStepCard
stepNumber={3} stepNumber={4}
title={t('blocker.android_step2_title')} title={t('blocker.android_step2_title')}
subtitle={done ? t('blocker.android_step2_subtitle_done') : t('blocker.android_step2_subtitle_pending')} subtitle={done ? t('blocker.android_step2_subtitle_done') : t('blocker.android_step2_subtitle_pending')}
done={done} done={done}
unlocked={unlocked} unlocked={unlocked}
lockedHint={unlocked ? undefined : t('blocker.setup_step_locked_hint', { step: 2 })} lockedHint={unlocked ? undefined : t('blocker.setup_step_locked_hint', { step: 3 })}
colors={colors} colors={colors}
> >
{unlocked && !done && ( {unlocked && !done && (
@ -434,6 +463,67 @@ function AndroidStep2({
); );
} }
function AndroidStepBattery({
unlocked,
done,
onRequest,
onOpenDetails,
colors,
t,
}: {
unlocked: boolean;
done: boolean;
onRequest: () => Promise<{ opened: boolean; alreadyIgnored?: boolean }>;
onOpenDetails: () => Promise<{ opened: boolean }>;
colors: ReturnType<typeof import('../../lib/theme').useColors>;
t: ReturnType<typeof import('react-i18next').useTranslation>['t'];
}) {
const [busy, setBusy] = useState(false);
async function handlePress() {
if (done || busy) return;
setBusy(true);
try { await onRequest(); } finally { setBusy(false); }
}
return (
<SetupStepCard
stepNumber={3}
title={t('blocker.android_battery_title')}
subtitle={done ? t('blocker.android_battery_subtitle_done') : t('blocker.android_battery_subtitle_pending')}
done={done}
unlocked={unlocked}
lockedHint={unlocked ? undefined : t('blocker.setup_step_locked_hint', { step: 2 })}
colors={colors}
>
{unlocked && !done && (
<View style={{ gap: 8, marginTop: 10 }}>
<View style={{ backgroundColor: colors.surfaceElevated, borderRadius: 12, padding: 12 }}>
<Text style={{ fontSize: 13, fontFamily: 'Nunito_600SemiBold', color: colors.text, lineHeight: 18 }}>
{t('blocker.android_battery_body')}
</Text>
</View>
<TouchableOpacity
onPress={handlePress}
activeOpacity={0.8}
style={{ backgroundColor: colors.brandOrange, borderRadius: 10, paddingVertical: 10, alignItems: 'center', marginTop: 2 }}
>
{busy
? <ActivityIndicator size="small" color="#fff" />
: <Text style={{ fontSize: 14, fontFamily: 'Nunito_700Bold', color: '#fff' }}>{t('blocker.android_battery_cta')}</Text>
}
</TouchableOpacity>
<TouchableOpacity onPress={() => { void onOpenDetails(); }} activeOpacity={0.7} style={{ paddingVertical: 4, alignItems: 'center' }}>
<Text style={{ fontSize: 12, fontFamily: 'Nunito_600SemiBold', color: colors.textMuted, textAlign: 'center', lineHeight: 16 }}>
{t('blocker.android_battery_samsung_hint')}
</Text>
</TouchableOpacity>
</View>
)}
</SetupStepCard>
);
}
function AndroidStep3({ function AndroidStep3({
unlocked, unlocked,
done, done,

View File

@ -58,7 +58,16 @@ export function ProtectionSlide({
const [permissionDeniedOpen, setPermissionDeniedOpen] = useState(false); const [permissionDeniedOpen, setPermissionDeniedOpen] = useState(false);
const [familyControlsErrorOpen, setFamilyControlsErrorOpen] = useState(false); const [familyControlsErrorOpen, setFamilyControlsErrorOpen] = useState(false);
const [confirmStep, setConfirmStep] = useState<ConfirmStep | null>(null); const [confirmStep, setConfirmStep] = useState<ConfirmStep | null>(null);
// Fallback-Ausweg: erscheint, wenn der Schutz auf diesem Gerät nicht aktiviert
// werden kann (Timeout oder fehlgeschlagener Versuch) — sonst hängt der User
// im Onboarding fest (z.B. Android-16-VPN-Crash). Schutz später im Blocker.
const [showSkip, setShowSkip] = useState(false);
// VPN-Aktivierung läuft (Android): Spinner am VPN-Step bis der Layer-State
// bestätigt ist. Der Protection-State pollt sonst zu selten → der Step bliebe
// ~1min „offen", obwohl der Tunnel längst läuft.
const [vpnPending, setVpnPending] = useState(false);
const finishedRef = useRef(false); const finishedRef = useRef(false);
const armingRef = useRef(false);
// a11y-Explainer NUR beim ersten Tap zeigen — danach (Settings-Rückkehr → // a11y-Explainer NUR beim ersten Tap zeigen — danach (Settings-Rückkehr →
// nochmal tippen zum Armen) direkt durch, sonst nervt das Sheet doppelt. // nochmal tippen zum Armen) direkt durch, sonst nervt das Sheet doppelt.
const a11yExplainerShownRef = useRef(false); const a11yExplainerShownRef = useRef(false);
@ -80,11 +89,17 @@ export function ProtectionSlide({
const appDeletionLockActive = (state?.layers.appDeletionLock ?? familyControlsActive) === true; const appDeletionLockActive = (state?.layers.appDeletionLock ?? familyControlsActive) === true;
const nefilterActive = state?.layers.nefilterActive === true; const nefilterActive = state?.layers.nefilterActive === true;
const deviceAdminActive = state?.layers.deviceAdmin === true; const deviceAdminActive = state?.layers.deviceAdmin === true;
// a11y-SERVICE an (≠ Tamper-Lock armed). Trennung kommt direkt aus dem nativen
// getDeviceState ({accessibility} vs {tamperLock}).
const a11yServiceOn = state?.layers.accessibility === true;
// Akku-Ausnahme: ohne sie schläfert Samsung & Co. die App ein → a11y-Service
// wird entbunden → Lock wertlos. Daher Pflicht-Step im Android-Flow.
const batteryUnrestricted = state?.layers.batteryUnrestricted === true;
// "Fertig" == blocker.tsx lockedIn. Eine Quelle der Wahrheit. // "Fertig" == blocker.tsx lockedIn. Eine Quelle der Wahrheit.
const allDone = const allDone =
Platform.OS === 'android' Platform.OS === 'android'
? urlFilterActive && appDeletionLockActive && deviceAdminActive ? urlFilterActive && appDeletionLockActive && deviceAdminActive && batteryUnrestricted
: (nefilterActive || urlFilterActive) && : (nefilterActive || urlFilterActive) &&
(mdmManaged || nefilterActive || appDeletionLockActive || !FAMILY_CONTROLS_AVAILABLE); (mdmManaged || nefilterActive || appDeletionLockActive || !FAMILY_CONTROLS_AVAILABLE);
@ -99,6 +114,24 @@ export function ProtectionSlide({
onDone(); onDone();
} }
// Notausgang: Schutz noch nicht aktiv, aber User soll nicht festsitzen.
function handleSkipProtection() {
Alert.alert(
t('onboarding.protection_skip.title'),
t('onboarding.protection_skip.body'),
[
{ text: t('common.cancel'), style: 'cancel' },
{
text: t('onboarding.protection_skip.confirm'),
style: 'destructive',
onPress: () => {
void finishProtectionStep();
},
},
],
);
}
// Foreground-Return → State neu laden. a11y/Geräteadmin/Bildschirmzeit werden in // Foreground-Return → State neu laden. a11y/Geräteadmin/Bildschirmzeit werden in
// den System-Settings gesetzt; beim Zurückkommen pollen wir den neuen Layer-State, // den System-Settings gesetzt; beim Zurückkommen pollen wir den neuen Layer-State,
// damit die Cards umschalten und "Weiter" freigeschaltet wird. // damit die Cards umschalten und "Weiter" freigeschaltet wird.
@ -109,12 +142,65 @@ export function ProtectionSlide({
return () => sub.remove(); return () => sub.remove();
}, [refresh]); }, [refresh]);
// Sicherheitsnetz: Ist der Schutz nach 30 s noch nicht aktiv (z.B. weil die
// Aktivierung auf diesem OS scheitert), den "Später einrichten"-Ausweg zeigen.
// (Failed-Aktivierungen blenden ihn sofort ein, siehe Handler unten.)
useEffect(() => {
if (allDone) {
setShowSkip(false);
return;
}
const id = setTimeout(() => setShowSkip(true), 30000);
return () => clearTimeout(id);
}, [allDone]);
// Nach VPN-Freigabe schneller nachpollen, bis urlFilter aktiv ist (sonst ~1min,
// bis der Step grün wird). Spinner läuft, solange vpnPending. Deckel ~60s.
useEffect(() => {
if (urlFilterActive) {
setVpnPending(false);
return;
}
if (!vpnPending) return;
let ticks = 0;
const id = setInterval(() => {
ticks += 1;
refresh();
if (ticks >= 20) {
clearInterval(id);
setVpnPending(false);
}
}, 3000);
return () => clearInterval(id);
}, [vpnPending, urlFilterActive, refresh]);
// Auto-Arm (Android): kam der User aus den a11y-Settings zurück und hat den
// Service aktiviert (accessibility=true), war der Tamper-Lock bisher NICHT armed
// — das passierte erst beim ZWEITEN Tap auf den a11y-Button (Two-Step-Design).
// Hier ziehen wir das automatisch nach: sobald a11y-Service an + noch nicht
// armed, einmal activateFamilyControls() → geht bei aktivem a11y direkt in den
// Arm-Pfad (kein Settings-Öffnen) → Step wird grün ohne zweiten Tap.
useEffect(() => {
if (Platform.OS !== 'android') return;
if (!a11yServiceOn || appDeletionLockActive || armingRef.current) return;
armingRef.current = true;
activateFamilyControls()
.catch(() => {})
.finally(() => {
armingRef.current = false;
refresh();
});
}, [a11yServiceOn, appDeletionLockActive, activateFamilyControls, refresh]);
// ─── Handler (1:1 wie blocker.tsx) ────────────────────────────────────────── // ─── Handler (1:1 wie blocker.tsx) ──────────────────────────────────────────
async function handleActivateUrlFilter() { async function handleActivateUrlFilter() {
try { try {
const result = await activateUrlFilter(); const result = await activateUrlFilter();
if (!result.enabled) { if (!result.enabled) {
// Aktivierung fehlgeschlagen → Notausgang sofort anbieten (nicht erst nach 30s).
setShowSkip(true);
setVpnPending(false);
const isPermissionDenied = const isPermissionDenied =
Platform.OS === 'ios' && Platform.OS === 'ios' &&
typeof result.error === 'string' && typeof result.error === 'string' &&
@ -144,6 +230,8 @@ export function ProtectionSlide({
} }
return result; return result;
} catch (e: any) { } catch (e: any) {
setShowSkip(true);
setVpnPending(false);
Alert.alert(t('blocker.activation_failed_title'), e?.message ?? t('common.unknown_error')); Alert.alert(t('blocker.activation_failed_title'), e?.message ?? t('common.unknown_error'));
return { enabled: false }; return { enabled: false };
} }
@ -222,6 +310,12 @@ export function ProtectionSlide({
setConfirmStep('deviceadmin'); setConfirmStep('deviceadmin');
return { launched: false }; return { launched: false };
}; };
// Akku-Ausnahme: System-Dialog direkt öffnen (ein Tap „Zulassen"); der
// Step-Card-Text erklärt das Warum. Return-Refresh via AppState-'active'.
const gatedBattery = async () => protection.requestIgnoreBatteryOptimizations();
// Samsung-Sonderweg: App-Detail-Settings (Akku → „Uneingeschränkt" + raus aus
// „Schlafende Apps") — das deckt der reine AOSP-Whitelist-Dialog nicht ab.
const openBatteryDetails = async () => protection.openAppDetailsSettings();
const gatedApplock = async () => { const gatedApplock = async () => {
setConfirmStep('applock'); setConfirmStep('applock');
return { enabled: false }; return { enabled: false };
@ -257,6 +351,8 @@ export function ProtectionSlide({
function runConfirmedAction(step: ConfirmStep) { function runConfirmedAction(step: ConfirmStep) {
switch (step) { switch (step) {
case 'vpn': case 'vpn':
setVpnPending(true);
return handleActivateUrlFilter();
case 'urlfilter': case 'urlfilter':
return handleActivateUrlFilter(); return handleActivateUrlFilter();
case 'deviceadmin': case 'deviceadmin':
@ -285,6 +381,8 @@ export function ProtectionSlide({
primaryLabel={t('common.continue')} primaryLabel={t('common.continue')}
onPrimary={finishProtectionStep} onPrimary={finishProtectionStep}
primaryDisabled={!allDone} primaryDisabled={!allDone}
secondaryLabel={!allDone && showSkip ? t('onboarding.protection_skip.label') : undefined}
onSecondary={!allDone && showSkip ? handleSkipProtection : undefined}
/> />
} }
> >
@ -294,11 +392,15 @@ export function ProtectionSlide({
{Platform.OS === 'android' ? ( {Platform.OS === 'android' ? (
<AndroidSetupFlow <AndroidSetupFlow
vpnActive={urlFilterActive} vpnActive={urlFilterActive}
vpnActivating={vpnPending}
accessibilityLocked={appDeletionLockActive} accessibilityLocked={appDeletionLockActive}
deviceAdminActive={deviceAdminActive} deviceAdminActive={deviceAdminActive}
batteryUnrestricted={batteryUnrestricted}
onActivateVpn={gatedVpn} onActivateVpn={gatedVpn}
onActivateAccessibility={gatedA11y} onActivateAccessibility={gatedA11y}
onRequestDeviceAdmin={gatedDeviceAdmin} onRequestDeviceAdmin={gatedDeviceAdmin}
onRequestBattery={gatedBattery}
onOpenAppDetails={openBatteryDetails}
colors={colors} colors={colors}
t={t} t={t}
/> />

View File

@ -361,8 +361,8 @@ export function DemographicsAccordion({
lineHeight: 15, lineHeight: 15,
}} }}
> >
Mit deinen anonymen Daten machen wir rebreak zur ersten DiGA-zertifizierten Deine anonymen Angaben helfen uns, ReBreak gezielt zu verbessern.
Spielsucht-App. Als Dankeschön: 1 Woche Pro. Als Dankeschön: 1 Woche Pro.
</Text> </Text>
</View> </View>
</View> </View>
@ -370,7 +370,7 @@ export function DemographicsAccordion({
<FieldRow <FieldRow
label="Geburtsjahr" label="Geburtsjahr"
why="Lyra spricht dich altersgerecht an, DiGA-Berichte erkennen Risiko nach Altersgruppe." why="Lyra spricht dich altersgerecht an und erkennt altersbedingte Risikofaktoren."
filled={local.birthYear !== null} filled={local.birthYear !== null}
> >
<SelectButton <SelectButton
@ -618,7 +618,7 @@ export function DemographicsAccordion({
{local.bundesland ? ( {local.bundesland ? (
<FieldRow <FieldRow
label="Stadt" label="Stadt"
why="Lokale Beratungsstellen + anonyme DiGA-Studien." why="Hilft uns, regionale Unterschiede besser zu verstehen."
filled={!!local.city} filled={!!local.city}
indent indent
isLast isLast

View File

@ -1,6 +1,7 @@
import { View, Text, TouchableOpacity } from 'react-native'; import { View, Text, TouchableOpacity } from 'react-native';
import { Ionicons } from '@expo/vector-icons'; import { Ionicons } from '@expo/vector-icons';
import { useColors } from '../../lib/theme'; import { useColors } from '../../lib/theme';
import { RiveAvatar } from '../RiveAvatar';
type Props = { type Props = {
onDismiss?: () => void; onDismiss?: () => void;
@ -22,18 +23,8 @@ export function DigaMissionBanner({ onDismiss, onContribute }: Props) {
}} }}
> >
<View style={{ flexDirection: 'row', alignItems: 'flex-start', gap: 10 }}> <View style={{ flexDirection: 'row', alignItems: 'flex-start', gap: 10 }}>
<View <View style={{ marginTop: 2 }}>
style={{ <RiveAvatar emotion="idle" size="sm" />
width: 28,
height: 28,
borderRadius: 14,
backgroundColor: '#fef3c7',
alignItems: 'center',
justifyContent: 'center',
marginTop: 2,
}}
>
<Ionicons name="medkit-outline" size={14} color="#854d0e" />
</View> </View>
<View style={{ flex: 1 }}> <View style={{ flex: 1 }}>
<Text <Text
@ -54,9 +45,8 @@ export function DigaMissionBanner({ onDismiss, onContribute }: Props) {
lineHeight: 17, lineHeight: 17,
}} }}
> >
Rebreak strebt die Anerkennung als DiGA an. Mit ein paar anonymen Mit ein paar anonymen Angaben hilfst du uns, ReBreak gezielt
Angaben hilfst du, die Wirksamkeit zu belegen damit Krankenkassen weiterzuentwickeln. Freiwillig, anonym, jederzeit löschbar.
die App künftig erstatten können.
</Text> </Text>
<View style={{ flexDirection: 'row', gap: 8, marginTop: 12 }}> <View style={{ flexDirection: 'row', gap: 8, marginTop: 12 }}>

View File

@ -345,6 +345,9 @@ while [[ $# -gt 0 ]]; do
esac esac
done done
# Disk-Guard: warnt bei wenig freiem Speicher, bevor (große) Builds starten.
[ -x "$SCRIPT_DIR/clean.sh" ] && "$SCRIPT_DIR/clean.sh" --guard 8 || true
# ═══════════════════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════════════════
# Secrets-File auto-loading (NICHT committen — siehe .deploy-secrets.local.example) # Secrets-File auto-loading (NICHT committen — siehe .deploy-secrets.local.example)
# ═══════════════════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════════════════

View File

@ -692,6 +692,9 @@ cmd_magic() {
COMMAND="${1:-ios}" COMMAND="${1:-ios}"
shift || true shift || true
# Disk-Guard: warnt vor Builds, wenn der Mac wenig freien Speicher hat (clean.sh).
[ -x "$SCRIPT_DIR/clean.sh" ] && "$SCRIPT_DIR/clean.sh" --guard 8 || true
case "$COMMAND" in case "$COMMAND" in
ios) ios)
cmd_ios "$@" cmd_ios "$@"

View File

@ -1,5 +1,6 @@
import { useEffect } from "react"; import { useEffect } from "react";
import { supabase } from "../lib/supabase"; import { supabase } from "../lib/supabase";
import { isRealtimeErrorReal } from "../lib/realtimeStatus";
import { useDeviceApprovalStore } from "../stores/deviceApproval"; import { useDeviceApprovalStore } from "../stores/deviceApproval";
import type { RealtimeChannel } from "@supabase/supabase-js"; import type { RealtimeChannel } from "@supabase/supabase-js";
@ -59,7 +60,7 @@ export function useDeviceApprovalRealtime(enabled: boolean = true) {
) )
.subscribe((status, err) => { .subscribe((status, err) => {
if (status === "CHANNEL_ERROR" || status === "TIMED_OUT") { if (status === "CHANNEL_ERROR" || status === "TIMED_OUT") {
console.warn("[approvalRealtime] error:", status, err ?? ""); if (isRealtimeErrorReal()) console.warn("[approvalRealtime] error:", status, err ?? "");
cleanup(); cleanup();
if (reconnectTimer) clearTimeout(reconnectTimer); if (reconnectTimer) clearTimeout(reconnectTimer);
reconnectTimer = setTimeout(() => { reconnectTimer = setTimeout(() => {

View File

@ -1,5 +1,6 @@
import { useEffect } from "react"; import { useEffect } from "react";
import { supabase } from "../lib/supabase"; import { supabase } from "../lib/supabase";
import { isRealtimeErrorReal } from "../lib/realtimeStatus";
import type { RealtimeChannel } from "@supabase/supabase-js"; import type { RealtimeChannel } from "@supabase/supabase-js";
/** /**
@ -67,7 +68,7 @@ export function useDomainSubmissionRealtime(
) )
.subscribe((status, err) => { .subscribe((status, err) => {
if (status === "CHANNEL_ERROR" || status === "TIMED_OUT") { if (status === "CHANNEL_ERROR" || status === "TIMED_OUT") {
console.warn("[domainRealtime] error:", status, err ?? ""); if (isRealtimeErrorReal()) console.warn("[domainRealtime] error:", status, err ?? "");
cleanup(); cleanup();
if (reconnectTimer) clearTimeout(reconnectTimer); if (reconnectTimer) clearTimeout(reconnectTimer);
reconnectTimer = setTimeout(() => { reconnectTimer = setTimeout(() => {

View File

@ -1,4 +1,5 @@
import { useEffect } from 'react'; import { useEffect } from 'react';
import { isRealtimeErrorReal } from '../lib/realtimeStatus';
import { Platform } from 'react-native'; import { Platform } from 'react-native';
import { useRouter } from 'expo-router'; import { useRouter } from 'expo-router';
import { supabase } from '../lib/supabase'; import { supabase } from '../lib/supabase';
@ -48,7 +49,7 @@ export function useIncomingCalls(myUserId: string | undefined) {
} }
}); });
chan.subscribe((status: string, err?: any) => { chan.subscribe((status: string, err?: any) => {
console.log('[CALL/recv] call-ring subscribe status:', status, err ?? ''); if (isRealtimeErrorReal()) console.log('[CALL/recv] call-ring subscribe status:', status, err ?? '');
}); });
return () => { return () => {

View File

@ -1,5 +1,5 @@
// Parser für Lyras LLM-JSON-Antworten + Emotion-Detection. // Parser für Lyras LLM-JSON-Antworten + Emotion-Detection.
import { EMPATHY_RE, HAPPY_RE } from './sosConstants'; import { EMPATHY_RE, HAPPY_RE, JOY_RE, SAD_RE, CONFUSION_RE } from './sosConstants';
import type { Emotion } from '../components/RiveAvatar'; import type { Emotion } from '../components/RiveAvatar';
export type LyraEmotion = Emotion; export type LyraEmotion = Emotion;
@ -55,7 +55,13 @@ export function parseLyraResponse(raw: string): { message: string; chips: ChipSp
} }
export function detectEmotion(text: string): LyraEmotion { export function detectEmotion(text: string): LyraEmotion {
// Reihenfolge = Priorität. joy (große Feier) vor happy (Alltag);
// sad (Verlust/Rückfall/Scham, spiegelnd) vor empathy (allg. Schwere);
// confusion (Rückfrage) zuletzt, da neutral-tonig.
if (JOY_RE.test(text)) return 'joy';
if (HAPPY_RE.test(text)) return 'happy'; if (HAPPY_RE.test(text)) return 'happy';
if (SAD_RE.test(text)) return 'sad';
if (EMPATHY_RE.test(text)) return 'empathy'; if (EMPATHY_RE.test(text)) return 'empathy';
if (CONFUSION_RE.test(text)) return 'confusion';
return 'idle'; return 'idle';
} }

View File

@ -319,6 +319,41 @@ export const protection = {
return RebreakProtection.getDeviceState(); return RebreakProtection.getDeviceState();
}, },
// ─── Android: Akku-Ausnahme (gegen Samsung-Sleep, der den a11y-Lock killt) ──
//
// Ohne Battery-Exemption schläfert Samsung & Co. die App ein → der a11y-
// Service wird entbunden → Tamper-Lock erzwingt nichts mehr (Schutz fällt
// still aus). Daher Status + Anforderung exponieren.
/** Android: Ist die App von der Akku-Optimierung ausgenommen? (iOS: immer true) */
async isBatteryOptimizationIgnored(): Promise<boolean> {
if (Platform.OS !== "android") return true;
try {
const r = await RebreakProtection.isBatteryOptimizationIgnored();
return r.ignored === true;
} catch (e) {
console.warn("[protection] isBatteryOptimizationIgnored failed:", e);
return false;
}
},
/** Android: System-Dialog „Akku-Optimierung ignorieren?" (ein Tap „Zulassen"). */
async requestIgnoreBatteryOptimizations() {
if (Platform.OS !== "android")
return { opened: false, alreadyIgnored: true } as {
opened: boolean;
alreadyIgnored?: boolean;
};
return RebreakProtection.requestIgnoreBatteryOptimizations();
},
/** Android: App-Detail-Settings öffnen Samsung: Akku Uneingeschränkt" +
* raus aus Schlafende/Tief schlafende Apps". */
async openAppDetailsSettings() {
if (Platform.OS !== "android") return { opened: false };
return RebreakProtection.openAppDetailsSettings();
},
// ─── iOS Layer 2 — webContent-Filter (ManagedSettings) ─────────────────── // ─── iOS Layer 2 — webContent-Filter (ManagedSettings) ───────────────────
// //
// Stilles WebKit-Sicherheitsnetz: blockt eine kuratierte, länderabhängige // Stilles WebKit-Sicherheitsnetz: blockt eine kuratierte, länderabhängige
@ -589,8 +624,16 @@ export const protection = {
? ({ ? ({
...rawLayers, ...rawLayers,
urlFilter: rawLayers.vpn, urlFilter: rawLayers.vpn,
familyControls: rawLayers.tamperLock, // App-Lock gilt NUR als aktiv, wenn der a11y-Service WIRKLICH läuft —
appDeletionLock: rawLayers.tamperLock, // nicht nur weil `tamper_armed` (Pref) gesetzt ist. Sonst zeigt die UI
// "komplett geschützt", obwohl a11y (z.B. nach Reboot von Samsung
// deaktiviert; programmatisch nicht reaktivierbar) nichts mehr erzwingt
// → falsches Sicherheitsgefühl. (Analog zum bestehenden VPN-flag-Gate
// in der nativen tamperLock-Berechnung.)
familyControls:
rawLayers.tamperLock === true && rawLayers.accessibility === true,
appDeletionLock:
rawLayers.tamperLock === true && rawLayers.accessibility === true,
} as DeviceLayers) } as DeviceLayers)
: rawLayers; : rawLayers;

View File

@ -0,0 +1,19 @@
import { AppState } from "react-native";
/**
* Realtime-WebSocket-Closes beim Backgrounden sind ERWARTET, kein echter Fehler:
* Supabase schließt den Socket via `auth.stopAutoRefresh()` (Close-Code 1000 =
* Normal Closure"), sobald die App in den Hintergrund geht das feuert
* CHANNEL_ERROR/TIMED_OUT gleichzeitig auf ALLEN Channels (call-ring, notif,
* approval, domain ). Re-Subscribe passiert beim Foreground automatisch über
* den jeweiligen Reconnect-Timer.
*
* Nur im Vordergrund ist ein CHANNEL_ERROR ein echtes Problem, das geloggt
* werden soll. Im Hintergrund würde das Logging nur Spam erzeugen.
*
* (Backyard-Infra-Diagnose 2026-06-10: nginx-WS-Proxy korrekt, Realtime-Container
* gesund, nginx-Error-Log leer der Close ist rein client-seitig + erwartet.)
*/
export function isRealtimeErrorReal(): boolean {
return AppState.currentState === "active";
}

View File

@ -54,3 +54,10 @@ export const TOTAL_ROUNDS = 3;
export const EMPATHY_RE = /schwer|rückfall|traurig|schlimm|hoffnungslos|verloren|scham|schuld|verzweifelt/i; export const EMPATHY_RE = /schwer|rückfall|traurig|schlimm|hoffnungslos|verloren|scham|schuld|verzweifelt/i;
export const HAPPY_RE = /toll|super|geschafft|stark|stolz|fantastisch|prima/i; export const HAPPY_RE = /toll|super|geschafft|stark|stolz|fantastisch|prima/i;
// Stärkere Feier (Meilenstein/Streak) als das alltägliche "happy".
export const JOY_RE = /glückwunsch|meilenstein|unglaublich|wahnsinn|grandios|großer schritt|so stolz auf dich|riesig stolz/i;
// Schwere Verlust-/Rückfall-/Scham-Sprache → spiegelndes Mitgefühl statt Winken.
// Hat in detectEmotion Vorrang vor EMPATHY_RE.
export const SAD_RE = /rückfällig|verspielt|am boden|versagt|schäme|so leid|es tut mir.{0,12}leid/i;
// Lyra braucht Klärung / stellt eine Rückfrage.
export const CONFUSION_RE = /meinst du|wie genau|was genau|verstehe ich.{0,15}richtig|kannst du.{0,15}(erklär|genauer)|nicht ganz sicher|magst du.{0,15}(erzähl|erklär)/i;

View File

@ -582,9 +582,15 @@
"a11y_indicator": "ReBreak يرشدك خطوة بخطوة", "a11y_indicator": "ReBreak يرشدك خطوة بخطوة",
"a11y_step3": "فعّل المفتاح، وأكّد مربع الحوار — ثم ارجع إلى التطبيق.", "a11y_step3": "فعّل المفتاح، وأكّد مربع الحوار — ثم ارجع إلى التطبيق.",
"usage_title": "لمرة واحدة: تفعيل الإرشاد خطوة بخطوة", "usage_title": "لمرة واحدة: تفعيل الإرشاد خطوة بخطوة",
"usage_body": "قائمة إمكانية الوصول في Samsung معقّدة. لكي أرشدك خطوة بخطوة، امنح ReBreak «الوصول إلى بيانات الاستخدام» مرة واحدة — سأفتح الصفحة الآن. فعّله هناك، ارجع واضغط الحماية مرة أخرى.", "usage_body": "قائمة إمكانية الوصول معقّدة أحياناً. لكي أرشدك خطوة بخطوة، امنح ReBreak «الوصول إلى بيانات الاستخدام» مرة واحدة — سأفتح الصفحة الآن. فعّله هناك، ارجع واضغط الحماية مرة أخرى.",
"overlay_title": "لمرة واحدة: السماح بطبقة التلميح", "overlay_title": "لمرة واحدة: السماح بطبقة التلميح",
"overlay_body": "لكي يظهر تلميحي بوضوح أمام الإعدادات بدلاً من إخفائه، امنح ReBreak «العرض فوق التطبيقات الأخرى» مرة واحدة — سأفتح الصفحة. فعّله هناك، ارجع واضغط الحماية مرة أخرى." "overlay_body": "لكي يظهر تلميحي بوضوح أمام الإعدادات بدلاً من إخفائه، امنح ReBreak «العرض فوق التطبيقات الأخرى» مرة واحدة — سأفتح الصفحة. فعّله هناك، ارجع واضغط الحماية مرة أخرى."
},
"protection_skip": {
"label": "لا يعمل؟ الإعداد لاحقًا",
"title": "إعداد الحماية لاحقًا؟",
"body": "الحماية ليست مفعّلة بعد. يمكنك المتابعة وإعدادها في أي وقت من «الحماية».",
"confirm": "لاحقًا"
} }
}, },
"protection_onboarding": { "protection_onboarding": {

View File

@ -480,6 +480,7 @@
"android_step1_subtitle_pending": "Blockt 300.000+ Gambling-Seiten system-weit via DNS-Filter", "android_step1_subtitle_pending": "Blockt 300.000+ Gambling-Seiten system-weit via DNS-Filter",
"android_step1_subtitle_done": "VPN-Filter läuft", "android_step1_subtitle_done": "VPN-Filter läuft",
"android_step1_cta": "VPN aktivieren", "android_step1_cta": "VPN aktivieren",
"activating": "VPN wird aktiviert …",
"android_step2_title": "ReBreak - Schutz", "android_step2_title": "ReBreak - Schutz",
"android_step2_subtitle_pending": "", "android_step2_subtitle_pending": "",
"android_step2_subtitle_done": "Bedienungshilfe aktiv", "android_step2_subtitle_done": "Bedienungshilfe aktiv",
@ -488,6 +489,12 @@
"android_step2_instruction3": "③ Schalte den Regler ein", "android_step2_instruction3": "③ Schalte den Regler ein",
"android_step2_cta": "Bedienungshilfen öffnen", "android_step2_cta": "Bedienungshilfen öffnen",
"android_step2_note": "Wähle ReBreak in der Liste und schalte den Regler ein. Tippe danach erneut auf den Button, um den Schutz zu aktivieren.", "android_step2_note": "Wähle ReBreak in der Liste und schalte den Regler ein. Tippe danach erneut auf den Button, um den Schutz zu aktivieren.",
"android_battery_title": "Akku-Optimierung deaktivieren",
"android_battery_subtitle_pending": "Damit dein Handy den Schutz nicht einschläft",
"android_battery_subtitle_done": "Akku-Optimierung deaktiviert",
"android_battery_body": "Ohne diese Ausnahme schläfert dein Handy ReBreak ein — dann fällt der Schutz heimlich aus. Tippe unten und wähle „Zulassen“.",
"android_battery_cta": "Akku-Ausnahme erlauben",
"android_battery_samsung_hint": "Klappt nicht? Bei Samsung: App-Einstellungen → Akku → „Uneingeschränkt“ und aus „Schlafende Apps“ entfernen.",
"android_step3_title": "Geräteadministrator aktivieren", "android_step3_title": "Geräteadministrator aktivieren",
"android_step3_subtitle_pending": "Schließt die Boot-Lücke: Schutz ist sofort nach Neustart aktiv", "android_step3_subtitle_pending": "Schließt die Boot-Lücke: Schutz ist sofort nach Neustart aktiv",
"android_step3_subtitle_done": "Geräteadministrator aktiv — Schutz vollständig", "android_step3_subtitle_done": "Geräteadministrator aktiv — Schutz vollständig",
@ -683,9 +690,15 @@
"a11y_indicator": "ReBreak begleitet dich Schritt für Schritt", "a11y_indicator": "ReBreak begleitet dich Schritt für Schritt",
"a11y_step3": "Schalter ein, im Dialog bestätigen — dann zurück zur App.", "a11y_step3": "Schalter ein, im Dialog bestätigen — dann zurück zur App.",
"usage_title": "Einmalig: Schritt-Führung freischalten", "usage_title": "Einmalig: Schritt-Führung freischalten",
"usage_body": "Samsungs Bedienungshilfen-Menü ist fummelig. Damit ich dich Schritt für Schritt führen kann, gib ReBreak einmal „Nutzungszugriff“ — ich öffne gleich die Seite. Schalt ReBreak dort an, komm zurück und tipp nochmal auf den Schutz.", "usage_body": "Das Bedienungshilfen-Menü ist oft fummelig. Damit ich dich Schritt für Schritt führen kann, gib ReBreak einmal „Nutzungszugriff“ — ich öffne gleich die Seite. Schalt ReBreak dort an, komm zurück und tipp nochmal auf den Schutz.",
"overlay_title": "Einmalig: Hinweis-Overlay erlauben", "overlay_title": "Einmalig: Hinweis-Overlay erlauben",
"overlay_body": "Damit mein Hinweis sichtbar VOR den Einstellungen schwebt (statt versteckt in der Leiste), gib ReBreak einmal „Über anderen Apps anzeigen“ — ich öffne die Seite. Schalt ReBreak dort an, komm zurück und tipp nochmal auf den Schutz." "overlay_body": "Damit mein Hinweis sichtbar VOR den Einstellungen schwebt (statt versteckt in der Leiste), gib ReBreak einmal „Über anderen Apps anzeigen“ — ich öffne die Seite. Schalt ReBreak dort an, komm zurück und tipp nochmal auf den Schutz."
},
"protection_skip": {
"label": "Es klappt nicht? Später einrichten",
"title": "Schutz später einrichten?",
"body": "Der Schutz ist noch nicht aktiv. Du kommst trotzdem weiter und kannst ihn jederzeit unter „Schutz“ einrichten.",
"confirm": "Später einrichten"
} }
}, },
"protection_onboarding": { "protection_onboarding": {

View File

@ -480,6 +480,7 @@
"android_step1_subtitle_pending": "Blocks 300,000+ gambling sites system-wide via DNS filter", "android_step1_subtitle_pending": "Blocks 300,000+ gambling sites system-wide via DNS filter",
"android_step1_subtitle_done": "VPN filter running", "android_step1_subtitle_done": "VPN filter running",
"android_step1_cta": "Activate VPN", "android_step1_cta": "Activate VPN",
"activating": "Activating VPN …",
"android_step2_title": "ReBreak Protection", "android_step2_title": "ReBreak Protection",
"android_step2_subtitle_pending": "", "android_step2_subtitle_pending": "",
"android_step2_subtitle_done": "Accessibility service active", "android_step2_subtitle_done": "Accessibility service active",
@ -488,6 +489,12 @@
"android_step2_instruction3": "③ Switch the toggle on", "android_step2_instruction3": "③ Switch the toggle on",
"android_step2_cta": "Open accessibility settings", "android_step2_cta": "Open accessibility settings",
"android_step2_note": "Find ReBreak in the list and switch it on. Then tap the button again to finish activating the protection.", "android_step2_note": "Find ReBreak in the list and switch it on. Then tap the button again to finish activating the protection.",
"android_battery_title": "Turn off battery optimization",
"android_battery_subtitle_pending": "So your phone can't put the protection to sleep",
"android_battery_subtitle_done": "Battery optimization turned off",
"android_battery_body": "Without this exception your phone puts ReBreak to sleep — and the protection silently fails. Tap below and choose \"Allow\".",
"android_battery_cta": "Allow battery exception",
"android_battery_samsung_hint": "Not working? On Samsung: App settings → Battery → \"Unrestricted\", and remove it from \"Sleeping apps\".",
"android_step3_title": "Activate device administrator", "android_step3_title": "Activate device administrator",
"android_step3_subtitle_pending": "Closes the boot gap: protection is active immediately after restart", "android_step3_subtitle_pending": "Closes the boot gap: protection is active immediately after restart",
"android_step3_subtitle_done": "Device administrator active — protection complete", "android_step3_subtitle_done": "Device administrator active — protection complete",
@ -683,9 +690,15 @@
"a11y_indicator": "ReBreak guides you step by step", "a11y_indicator": "ReBreak guides you step by step",
"a11y_step3": "Turn the switch on, confirm the dialog — then come back to the app.", "a11y_step3": "Turn the switch on, confirm the dialog — then come back to the app.",
"usage_title": "One-time: enable step-by-step guidance", "usage_title": "One-time: enable step-by-step guidance",
"usage_body": "Samsungs accessibility menu is fiddly. So I can guide you step by step, give ReBreak “Usage access” once — Ill open the page now. Turn ReBreak on there, come back and tap protection again.", "usage_body": "The accessibility menu can be fiddly. So I can guide you step by step, give ReBreak “Usage access” once — Ill open the page now. Turn ReBreak on there, come back and tap protection again.",
"overlay_title": "One-time: allow the hint overlay", "overlay_title": "One-time: allow the hint overlay",
"overlay_body": "So my hint sits visibly in front of Settings (instead of hidden in the notification shade), give ReBreak “Display over other apps” once — Ill open the page. Turn ReBreak on there, come back and tap protection again." "overlay_body": "So my hint sits visibly in front of Settings (instead of hidden in the notification shade), give ReBreak “Display over other apps” once — Ill open the page. Turn ReBreak on there, come back and tap protection again."
},
"protection_skip": {
"label": "Not working? Set up later",
"title": "Set up protection later?",
"body": "Protection isnt active yet. You can still continue and set it up anytime under “Protection”.",
"confirm": "Set up later"
} }
}, },
"protection_onboarding": { "protection_onboarding": {

View File

@ -580,9 +580,15 @@
"a11y_indicator": "ReBreak te guide pas à pas", "a11y_indicator": "ReBreak te guide pas à pas",
"a11y_step3": "Active linterrupteur, confirme la boîte de dialogue — puis reviens dans lapp.", "a11y_step3": "Active linterrupteur, confirme la boîte de dialogue — puis reviens dans lapp.",
"usage_title": "Une fois : activer le guidage pas à pas", "usage_title": "Une fois : activer le guidage pas à pas",
"usage_body": "Le menu daccessibilité de Samsung est pénible. Pour te guider étape par étape, donne à ReBreak l« accès aux données dusage » une fois — jouvre la page. Active ReBreak là, reviens et touche à nouveau la protection.", "usage_body": "Le menu daccessibilité est parfois pénible. Pour te guider étape par étape, donne à ReBreak l« accès aux données dusage » une fois — jouvre la page. Active ReBreak là, reviens et touche à nouveau la protection.",
"overlay_title": "Une fois : autoriser loverlay daide", "overlay_title": "Une fois : autoriser loverlay daide",
"overlay_body": "Pour que mon indication saffiche devant les Réglages (au lieu dêtre cachée dans le tiroir), donne à ReBreak « Superposition à dautres applis » une fois — jouvre la page. Active ReBreak là, reviens et touche à nouveau la protection." "overlay_body": "Pour que mon indication saffiche devant les Réglages (au lieu dêtre cachée dans le tiroir), donne à ReBreak « Superposition à dautres applis » une fois — jouvre la page. Active ReBreak là, reviens et touche à nouveau la protection."
},
"protection_skip": {
"label": "Ça ne marche pas ? Configurer plus tard",
"title": "Configurer la protection plus tard ?",
"body": "La protection nest pas encore active. Tu peux continuer et la configurer à tout moment sous « Protection ».",
"confirm": "Plus tard"
} }
}, },
"mail": { "mail": {

View File

@ -16,6 +16,7 @@ import android.content.Intent
import android.net.VpnService import android.net.VpnService
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.os.PowerManager
import android.provider.Settings import android.provider.Settings
import android.graphics.Color import android.graphics.Color
import android.graphics.PixelFormat import android.graphics.PixelFormat
@ -429,6 +430,58 @@ class RebreakProtectionModule : Module() {
} }
} }
// Battery-Optimization: ohne Ausnahme schläfert Samsung & Co. die App ein
// → der a11y-Tamper-Lock wird entbunden → Schutz fällt still aus. Daher
// Status prüfbar + per System-Dialog (ein Tap) anforderbar machen.
AsyncFunction("isBatteryOptimizationIgnored") {
mapOf("ignored" to isIgnoringBatteryOptimizations(requireContext()))
}
AsyncFunction("requestIgnoreBatteryOptimizations") {
val ctx = requireContext()
if (isIgnoringBatteryOptimizations(ctx)) {
return@AsyncFunction mapOf("opened" to false, "alreadyIgnored" to true)
}
// Direkter System-Dialog „… Akku-Optimierung ignorieren?" (ein Tap „Zulassen").
val intent = Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS).apply {
data = Uri.parse("package:${ctx.packageName}")
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
try {
ctx.startActivity(intent)
mapOf("opened" to true)
} catch (e: Exception) {
Log.w(TAG, "requestIgnoreBatteryOptimizations: ${e.message}")
// Fallback: generische Battery-Optimization-Liste
try {
ctx.startActivity(
Intent(Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
)
mapOf("opened" to true, "fallback" to true)
} catch (e2: Exception) {
mapOf("opened" to false, "error" to (e2.message ?: ""))
}
}
}
// Öffnet die App-Detail-Settings — für Samsung der Weg zu „Akku →
// Uneingeschränkt" + raus aus „Schlafende/Tief schlafende Apps" (das deckt
// der reine AOSP-Whitelist-Dialog NICHT ab).
AsyncFunction("openAppDetailsSettings") {
val ctx = requireContext()
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
data = Uri.parse("package:${ctx.packageName}")
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
try {
ctx.startActivity(intent)
mapOf("opened" to true)
} catch (e: Exception) {
mapOf("opened" to false, "error" to (e.message ?: ""))
}
}
// Hat die App das "Über anderen Apps anzeigen"-Recht? (→ passives Guide-Overlay) // Hat die App das "Über anderen Apps anzeigen"-Recht? (→ passives Guide-Overlay)
AsyncFunction("hasOverlayPermission") { AsyncFunction("hasOverlayPermission") {
val ctx = requireContext() val ctx = requireContext()
@ -1072,8 +1125,15 @@ class RebreakProtectionModule : Module() {
// Ein armed-aber-Schutz-aus Tamper-Lock ist effektiv KEIN Lock — sonst // Ein armed-aber-Schutz-aus Tamper-Lock ist effektiv KEIN Lock — sonst
// zeigt die UI „verriegelt" ohne dass der User je rauskommt (Desync-Fall: // zeigt die UI „verriegelt" ohne dass der User je rauskommt (Desync-Fall:
// `tamper_armed` noch true, aber `filter_enabled` schon false). // `tamper_armed` noch true, aber `filter_enabled` schon false).
"tamperLock" to (isTamperLockArmed(ctx) && isEnabledFlag(ctx)), // Lock gilt nur als aktiv, wenn er WIRKLICH erzwingt: armed + VPN-flag UND
// der a11y-Service läuft live. Sonst „armed-aber-a11y-aus" → UI zeigt
// „verriegelt", obwohl nichts mehr blockt (z.B. a11y nach Reboot deaktiviert).
"tamperLock" to (isTamperLockArmed(ctx) && isEnabledFlag(ctx) && isAccessibilityServiceEnabled(ctx)),
"deviceAdmin" to isDeviceAdminEnabled(ctx), "deviceAdmin" to isDeviceAdminEnabled(ctx),
// Akku-Ausnahme: ohne sie schläfert Samsung & Co. die App ein und entbindet
// den a11y-Service → Schutz fällt still aus. Teil des State, damit die UI
// darauf hinweisen/gaten kann.
"batteryUnrestricted" to isIgnoringBatteryOptimizations(ctx),
"blocklistCount" to count, "blocklistCount" to count,
"blocklistLastSyncAt" to lastSyncAt, "blocklistLastSyncAt" to lastSyncAt,
) )
@ -1089,6 +1149,17 @@ class RebreakProtectionModule : Module() {
} }
} }
/** Ist die App von der Akku-Optimierung ausgenommen? (AOSP-Whitelist
* Samsung Schlafende Apps" ist separat, daher zusätzlich der App-Detail-Guide.) */
private fun isIgnoringBatteryOptimizations(ctx: Context): Boolean {
return try {
val pm = ctx.getSystemService(Context.POWER_SERVICE) as PowerManager
pm.isIgnoringBatteryOptimizations(ctx.packageName)
} catch (_: Exception) {
false
}
}
private fun activateSuccessResult(): Map<String, Any?> = mapOf( private fun activateSuccessResult(): Map<String, Any?> = mapOf(
"allLayersOn" to false, "allLayersOn" to false,
"missingLayers" to listOf("accessibility", "tamperLock"), "missingLayers" to listOf("accessibility", "tamperLock"),

View File

@ -6,6 +6,7 @@ import android.app.NotificationManager
import android.app.PendingIntent import android.app.PendingIntent
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.pm.ServiceInfo
import android.net.VpnService import android.net.VpnService
import android.os.Build import android.os.Build
import android.os.Handler import android.os.Handler
@ -56,7 +57,7 @@ class RebreakVpnService : VpnService() {
return START_NOT_STICKY return START_NOT_STICKY
} }
ACTION_RESTART -> { ACTION_RESTART -> {
startForeground(NOTIF_ID, buildNotification()) if (!promoteToForegroundOrStop()) return START_NOT_STICKY
stopVpn() stopVpn()
hashList.load() hashList.load()
Log.i(TAG, "blocklist reloaded (restart) — ${hashList.count()} hashes") Log.i(TAG, "blocklist reloaded (restart) — ${hashList.count()} hashes")
@ -69,7 +70,7 @@ class RebreakVpnService : VpnService() {
return START_STICKY return START_STICKY
} }
else -> { else -> {
startForeground(NOTIF_ID, buildNotification()) if (!promoteToForegroundOrStop()) return START_NOT_STICKY
hashList.load() hashList.load()
Log.i(TAG, "blocklist loaded — ${hashList.count()} hashes") Log.i(TAG, "blocklist loaded — ${hashList.count()} hashes")
// Self-Heal: war die Blockliste beim Start leer (blocklist.bin // Self-Heal: war die Blockliste beim Start leer (blocklist.bin
@ -85,6 +86,38 @@ class RebreakVpnService : VpnService() {
} }
} }
/**
* Foreground-Promotion mit EXPLIZITEM FGS-Typ (specialUse). Auf Android 16
* (Samsung Galaxy A54 u.a.) lehnt `validateForegroundServiceType` die
* implizite 2-arg-`startForeground`-Variante ab SecurityException. Mit
* explizit übergebenem Typ (Googles dokumentierte Best Practice) passt die
* Validierung.
*
* Schlägt die Promotion trotzdem fehl (OEM-FGS-Restriction), wird der
* Service sauber gestoppt statt (a) den App-Prozess zu crashen ODER (b) in
* den 5s-did-not-call-startForeground"-Timeout-Crash zu laufen. So endet die
* Crash-Schleife auf jedem Gerät.
*/
private fun promoteToForegroundOrStop(): Boolean {
return try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
startForeground(
NOTIF_ID,
buildNotification(),
ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE,
)
} else {
startForeground(NOTIF_ID, buildNotification())
}
true
} catch (e: Exception) {
Log.e(TAG, "startForeground abgelehnt (${e.javaClass.simpleName}): ${e.message}", e)
try { stopVpn() } catch (_: Exception) {}
try { stopSelf() } catch (_: Exception) {}
false
}
}
private fun startVpn() { private fun startVpn() {
if (running) return if (running) return
try { try {

View File

@ -17,9 +17,9 @@
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>XPC!</string> <string>XPC!</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>0.4.1</string> <string>0.4.6</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>87</string> <string>92</string>
<key>NSExtension</key> <key>NSExtension</key>
<dict> <dict>
<key>NSExtensionPointIdentifier</key> <key>NSExtensionPointIdentifier</key>

View File

@ -17,9 +17,9 @@
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string> <string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>0.4.1</string> <string>0.4.6</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>87</string> <string>92</string>
<key>NSExtension</key> <key>NSExtension</key>
<dict> <dict>
<key>NSExtensionPointIdentifier</key> <key>NSExtensionPointIdentifier</key>

View File

@ -17,9 +17,9 @@
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string> <string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>0.4.1</string> <string>0.4.6</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>87</string> <string>92</string>
<key>EXAppExtensionAttributes</key> <key>EXAppExtensionAttributes</key>
<dict> <dict>
<key>EXExtensionPointIdentifier</key> <key>EXExtensionPointIdentifier</key>

View File

@ -42,6 +42,12 @@ export type DeviceLayers = {
* Schliesst die Boot-Luecke: ohne aktiven Admin hat die App nach Neustart * Schliesst die Boot-Luecke: ohne aktiven Admin hat die App nach Neustart
* kurz kein Schutz-Lock bis der AccessibilityService startet. */ * kurz kein Schutz-Lock bis der AccessibilityService startet. */
deviceAdmin?: boolean; deviceAdmin?: boolean;
/** Android-only. True wenn die App von der Akku-Optimierung ausgenommen ist
* (PowerManager.isIgnoringBatteryOptimizations). Ohne Ausnahme schläfert
* Samsung & Co. die App ein a11y-Service wird entbunden Schutz fällt
* still aus. (Samsung Schlafende Apps" ist zusätzlich separat App-Detail-
* Guide.) */
batteryUnrestricted?: boolean;
// Shared // Shared
blocklistCount: number; blocklistCount: number;
blocklistLastSyncAt: string | null; blocklistLastSyncAt: string | null;

View File

@ -211,6 +211,24 @@ declare class RebreakProtectionModule extends NativeModule<RebreakProtectionEven
/** Android: Öffnet die „Nutzungsdaten-Zugriff"-Settings zum Freigeben. */ /** Android: Öffnet die „Nutzungsdaten-Zugriff"-Settings zum Freigeben. */
openUsageAccessSettings(): Promise<{ opened: boolean }>; openUsageAccessSettings(): Promise<{ opened: boolean }>;
/** Android: Ist die App von der Akku-Optimierung ausgenommen? Ohne Ausnahme
* schläfert Samsung & Co. die App ein a11y-Service entbunden Schutz weg. */
isBatteryOptimizationIgnored(): Promise<{ ignored: boolean }>;
/** Android: System-Dialog Akku-Optimierung ignorieren?" (ein Tap Zulassen").
* alreadyIgnored=true wenn bereits ausgenommen; fallback=true wenn nur die
* generische Liste geöffnet werden konnte. */
requestIgnoreBatteryOptimizations(): Promise<{
opened: boolean;
alreadyIgnored?: boolean;
fallback?: boolean;
error?: string;
}>;
/** Android: Öffnet die App-Detail-Settings für Samsung der Weg zu
* Akku Uneingeschränkt" + raus aus „Schlafende/Tief schlafende Apps". */
openAppDetailsSettings(): Promise<{ opened: boolean; error?: string }>;
/** Android: Hat die App „Über anderen Apps anzeigen" (für das passive Guide-Overlay)? */ /** Android: Hat die App „Über anderen Apps anzeigen" (für das passive Guide-Overlay)? */
hasOverlayPermission(): Promise<{ granted: boolean }>; hasOverlayPermission(): Promise<{ granted: boolean }>;

View File

@ -1,6 +1,6 @@
{ {
"name": "rebreak-native", "name": "rebreak-native",
"version": "0.4.4", "version": "0.4.7",
"private": true, "private": true,
"main": "expo-router/entry", "main": "expo-router/entry",
"scripts": { "scripts": {

View File

@ -7,11 +7,13 @@
* Was es macht: * Was es macht:
* 1) Sorgt für `xmlns:tools` auf <manifest>. * 1) Sorgt für `xmlns:tools` auf <manifest>.
* 2) Registriert <service .RebreakVpnService> mit * 2) Registriert <service .RebreakVpnService> mit
* foregroundServiceType="systemExempted" + intent-filter * foregroundServiceType="specialUse" (+ PROPERTY_SPECIAL_USE_FGS_SUBTYPE=vpn)
* android.net.VpnService + permission BIND_VPN_SERVICE. * + intent-filter android.net.VpnService + permission BIND_VPN_SERVICE.
* (`systemExempted` ist seit Android 14 der korrekte Type für * (`systemExempted` wurde verworfen: es verlangt auf Android 16/API 36 zur
* VPN-/Filter-Foreground-Services vorher war `specialUse`+content_filter * startForeground-Zeit den AKTIVEN-VPN-Zustand [anyOf android:activate_vpn],
* angedacht aber bringt mehr Probleme als Nutzen.) * der erst ~ms nach establish() kommt SecurityException/Crash. `specialUse`
* hat keine Laufzeit-Vorbedingung; braucht FOREGROUND_SERVICE_SPECIAL_USE +
* einmalige Play-Special Use"-Erklärung.)
* 3) Registriert <service .RebreakAccessibilityService> mit * 3) Registriert <service .RebreakAccessibilityService> mit
* android:permission=BIND_ACCESSIBILITY_SERVICE + intent-filter * android:permission=BIND_ACCESSIBILITY_SERVICE + intent-filter
* android.accessibilityservice.AccessibilityService + meta-data * android.accessibilityservice.AccessibilityService + meta-data
@ -68,9 +70,23 @@ function ensureVpnService(manifest) {
$: { $: {
'android:name': VPN_SERVICE_CLASS, 'android:name': VPN_SERVICE_CLASS,
'android:permission': 'android.permission.BIND_VPN_SERVICE', 'android:permission': 'android.permission.BIND_VPN_SERVICE',
'android:foregroundServiceType': 'systemExempted', // specialUse statt systemExempted: systemExempted verlangt auf Android 16
// (API 36) zur startForeground-Zeit, dass die App bereits der AKTIVE VPN ist
// (anyOf [android:activate_vpn]) — dieser Zustand kommt aber erst ~ms nach
// establish() (Race) → SecurityException. specialUse hat keine Laufzeit-
// Vorbedingung und geht deterministisch durch (braucht nur die Permission
// FOREGROUND_SERVICE_SPECIAL_USE + die PROPERTY unten + Play-Special-Use-Decl).
'android:foregroundServiceType': 'specialUse',
'android:exported': 'false', 'android:exported': 'false',
}, },
property: [
{
$: {
'android:name': 'android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE',
'android:value': 'vpn',
},
},
],
'intent-filter': [ 'intent-filter': [
{ {
action: [{ $: { 'android:name': 'android.net.VpnService' } }], action: [{ $: { 'android:name': 'android.net.VpnService' } }],

View File

@ -1,6 +1,7 @@
import { create } from "zustand"; import { create } from "zustand";
import { apiFetch } from "../lib/api"; import { apiFetch } from "../lib/api";
import { supabase } from "../lib/supabase"; import { supabase } from "../lib/supabase";
import { isRealtimeErrorReal } from "../lib/realtimeStatus";
import type { RealtimeChannel } from "@supabase/supabase-js"; import type { RealtimeChannel } from "@supabase/supabase-js";
export interface AppNotification { export interface AppNotification {
@ -120,7 +121,7 @@ export const useNotificationStore = create<NotificationState>((set, get) => ({
) )
.subscribe((status) => { .subscribe((status) => {
if (status === "CHANNEL_ERROR" || status === "TIMED_OUT") { if (status === "CHANNEL_ERROR" || status === "TIMED_OUT") {
console.warn("[notifRealtime] error:", status); if (isRealtimeErrorReal()) console.warn("[notifRealtime] error:", status);
get().stopRealtime(); get().stopRealtime();
if (reconnectTimer) clearTimeout(reconnectTimer); if (reconnectTimer) clearTimeout(reconnectTimer);
reconnectTimer = setTimeout(() => { reconnectTimer = setTimeout(() => {

View File

@ -56,9 +56,34 @@ Building Release AAB (gradlew bundleRelease)|299
Validating IPA (App-Store Connect)|93 Validating IPA (App-Store Connect)|93
Uploading zu App-Store Connect (TestFlight)|126 Uploading zu App-Store Connect (TestFlight)|126
Building Release AAB (gradlew bundleRelease)|522 Building Release AAB (gradlew bundleRelease)|522
Building xcarchive|285
Exporting Ad-Hoc IPA|20
Exporting App-Store IPA|28
Validating IPA (App-Store Connect)|70 Validating IPA (App-Store Connect)|70
Uploading zu App-Store Connect (TestFlight)|86 Uploading zu App-Store Connect (TestFlight)|86
Building Release AAB (gradlew bundleRelease)|491 Building Release AAB (gradlew bundleRelease)|491
Validating IPA (App-Store Connect)|68
Uploading zu App-Store Connect (TestFlight)|90
Building Release AAB (gradlew bundleRelease)|531
Validating IPA (App-Store Connect)|97
Uploading zu App-Store Connect (TestFlight)|75
Building Release AAB (gradlew bundleRelease)|358
Validating IPA (App-Store Connect)|70
Uploading zu App-Store Connect (TestFlight)|82
Building Release AAB (gradlew bundleRelease)|322
Validating IPA (App-Store Connect)|72
Uploading zu App-Store Connect (TestFlight)|75
Building Release AAB (gradlew bundleRelease)|335
Validating IPA (App-Store Connect)|68
Uploading zu App-Store Connect (TestFlight)|102
Building Release AAB (gradlew bundleRelease)|329
Building Release AAB (gradlew bundleRelease)|360
Building Release AAB (gradlew bundleRelease)|99
Building Release AAB (gradlew bundleRelease)|448
expo prebuild (ios)|11
Validating IPA (App-Store Connect)|71
Uploading zu App-Store Connect (TestFlight)|87
Building Release AAB (gradlew bundleRelease)|502
Building xcarchive|281
Exporting Ad-Hoc IPA|21
Exporting App-Store IPA|29
Validating IPA (App-Store Connect)|96
Uploading zu App-Store Connect (TestFlight)|80
Building Release AAB (gradlew bundleRelease)|279

View File

@ -0,0 +1,90 @@
# Session-Handoff 2026-06-07 (Windows-Schutz + Mac-DNS + Marketing-Nav)
> Für die nächste Claude-Code-Session. Stand: ~15:40, User macht gleich **Mac-Neustart**.
> Memory-Files (MEMORY.md) ergänzen diesen Stand — hier das Session-Spezifische.
---
## 🔴 SOFORT-NÄCHSTER SCHRITT (offen, blockt Mac-Entscheidung)
**Mac-DNS-Profil: Wall oder stale-state?** Nach dem Neustart:
1. `~/Downloads/rebreak-schutz-signed.mobileconfig` installieren (signiertes DNS-Profil, Platzhalter-PW egal).
2. **Installiert** → KEINE Wall, manuelles DNS-Profil reicht (heutige Fails = stale Netzwerk-Dienst vom entfernten alten Profil). RemovalPassword-Schutz steht → **kein MDM nötig**.
3. **Failt auch auf frisch gebootetem Mac** (`„VPN-Dienst konnte nicht erstellt werden"`) → echte **macOS-26-Wall** (DNS nur via MDM) → dann MDM-Pfad (NanoMDM, siehe unten).
⚠️ Wichtig: NICHT vorschnell „MDM Pflicht" sagen — das echte ReBreak-DNS-Profil hat bei diesem User VORHER manuell installiert. Beweislage unklar bis Clean-Boot-Test.
---
## ✅ Was heute fertig wurde (verifiziert)
1. **Windows-Schutz end-to-end bewiesen** (in UTM Win11-ARM64-VM):
- PowerShell DoH-Takeover → `Resolve-DnsName bet365.de` = `0.0.0.0`/NXDOMAIN
- Browser-Bypass gelöst: Chrome/Edge umgehen System-DNS via built-in-Resolver+eigenem DoH → Fix = 2 Policies (`DnsOverHttpsMode=off` + `BuiltInDnsClientEnabled=false`), beide nötig.
- **Subdomain-Bug (User-Fund):** Blocklist war Apex-only (`bet365.com` geblockt, `www.bet365.com` offen). Fix deployed (siehe unten).
2. **ReBreak Magic Windows-App** (`apps/rebreak-magic-win/`, Tauri): komplett gecodet, CI-Build **grün**, Installer gebaut: `ReBreak Magic_0.1.0_x64-setup.exe` (3,8 MB, in `/tmp/rebreak-win-installer/`). Browser-Policies im `protection-core/apply_script`.
3. **Mac Removal-Lock BEWIESEN:** Profil mit `PayloadRemovalDisallowed` + `RemovalPassword` → macOS verlangt beim Entfernen das **Removal-PW, NICHT Admin-PW** (Dock-Test-Profil). RemovalPassword hält gegen Admin.
4. **Marketing-Nav umgebaut** (`apps/marketing/app/layouts/default.vue`): Floating-Tabbar raus → Hamburger (mobil) / Header-Nav (Desktop).
---
## 🟡 Offene Threads / Entscheidungen
| Thread | Status | Nächster Schritt |
|---|---|---|
| Mac-DNS Wall-Test | **blockt** | Clean-Boot-Test (oben) |
| Mac-MDM-Pfad (falls Wall) | geplant | NanoMDM Mac-Enrollment + DNS-Push testen (rebreak-binder-Domain). Teile da: `ops/mdm/rebreak-mac-dns-filter.mobileconfig`, `backend/server/api/mdm/enroll.get.ts` |
| Backend-Deploys | **warten auf GO** | ProhibitDisablement (dns/profile), Geräte-Matrix (plan-features+magic), magic/status — alle uncommitted |
| Marketing-Content | offen | „was wir alles bieten" (iOS/Android/Mac/Windows) — braucht User-Steer (Positionierung) |
| Installer hosten + Download-Link | offen | Wohin? `rebreak.org/downloads/` lt. magic/info. Dann Download-Page um Windows ergänzen |
| Mac Browser-Policies | offen | Managed Prefs Chrome/Edge ins Mac-Profil (Chromium umgeht DNS auch auf Mac) |
| Backend-Cooldown-Flow (Mac-Selbstbindung) | offen | Backend hält RemovalPassword, App-Release nach 24h, verschlüsselte Delivery |
| Android: Ina Internal-Test | offen | Opt-in-Link (Play Console → Interne Tests → „Teilnahme am Test") an Ina; Account `inabrini15@gmail.com` |
| Android Open Testing | besprochen | „Offener Test" (public Link) braucht Google-Review; Glücksspiel-Framing beachten |
---
## 📦 Working-Tree (uncommitted — NICHT deployed)
**Backend (deploy nach GO → triggert Staging via push main):**
- `dns/profile.get.ts``ProhibitDisablement: true` ergänzt (gegen Toggle-Bypass)
- `magic/register.post.ts``platform`-Feld (windows) + Cross-Counting Desktop-Slots
- `magic/status.get.ts`**neu** (Token-Status-Poll für Win-Tamper-Service)
- `magic/info.get.ts` — Windows-Installer-URL
- `plan-features.ts` — Pro `maxProtectedDevices 0→1` (Geräte-Matrix Pro 1+1 / Legend 3+2)
- `db/devices.ts` — MAGIC_DEVICE_LIMIT entfernt (jetzt plan-gated)
- `devices/enroll.post.ts` — Cross-Counting Magic+Protected
**Native (Frontend, kein Backend-Deploy):**
- `devices.tsx` (Mac/Win-Menü raus → MagicSheet direkt), `MagicSheet.tsx` (Mac/Win-Toggle + i18n), `DeviceProgressBar.tsx` (label), `AddMacSheet.tsx`, locales ar/de/en/fr
**Marketing:** `layouts/default.vue` (Nav-Umbau), locales de/en (Pricing 2/5 Geräte)
---
## 🚀 Committed + deployed heute
- `80be124` **blocklist `||domain^`** (Subdomain-Fix) → deployed Staging, AdGuard re-pulled (395k Regeln), `www.*` verifiziert NXDOMAIN
- `4b4b9fc` / `771af0f` / `869d8af` **rebreak-magic-win App + CI** → Build grün, Installer-Artefakt da
- **AdGuard-Server (rebreak-mdm):** `blocking_mode: default → nxdomain` (Config-Edit, Backup `AdGuardHome.yaml.bak-1780828505`). Beides LIVE für alle DNS-Plattformen.
---
## 🧪 Artefakte (lokal)
- `/tmp/rebreak-win-installer/ReBreak Magic_0.1.0_x64-setup.exe` — Windows-Installer (3,8 MB)
- `~/Downloads/rebreak-schutz-signed.mobileconfig` — signiertes Mac-DNS-Profil (für Clean-Boot-Test)
- `~/Downloads/removal-lock-test.mobileconfig` — Dock+RemovalPassword (beweist Removal-Lock, installiert)
- `~/Downloads/Windows11-ARM64-de.iso` — VM-ISO (4,9 GB)
---
## 🧠 Key-Findings (auch in Memory)
- **[[chromium-dns-bypass]]** — Chrome/Edge/Brave umgehen System-DNS (Mac+Win), Fix = 2 Browser-Policies
- **[[mac-magic-profile-lock]]** — RemovalPassword > Admin bewiesen; macOS-26 evtl. DNS-nur-via-MDM (Clean-Boot-Test pending); ProhibitDisablement gegen Toggle-Bypass
- **[[rebreak-magic-win]]** — Windows-App-Architektur + E2E-Validierung
- **[[Legend-USP = Multi-Device-Protection]]** — Geräte-Matrix Pro 1+1 / Legend 3+2 final
## ⚠️ Mac-Speicher: kritisch
460-GB-Disk war voll (865 MB frei). Heute ~29 GB Xcode-DevData gelöscht → ~35 GB frei. Windows-VM braucht ~40 GB. Langfristig: externe NVMe-SSD für VMs/Archive.

View File

@ -0,0 +1,69 @@
# Session-Handoff 2026-06-07/08 — Magic-Windows E2E + Geräte-UI + Marketing-Prod
> Marathon-Session. Stand: spät nachts 08.06. User macht Feierabend, schreibt
> die **FAGS-Mail** (Fachverband Glücksspielsucht meldet Interesse auf
> **Vorstandsebene** — strategisch riesig). Nächste Session vorbereitet (unten).
---
## 🏆 Das große Ding: ReBreak-Magic für Windows läuft E2E
Der komplette Flow ist **live + verifiziert**: Download (rebreak.org) → SmartScreen-Warnung → Pair (6-stelliger Code aus iPhone-App) → **register** → AdGuard-DNS-Client provisioniert → systemweiter Glücksspiel-Block am PC → **Push „Neues Gerät verbunden" aufs iPhone**. Gerät zeigt „mit ReBreak-Konto verbunden".
### Zwei harte Backend-Bugs heute gefunden + gefixt (waren der Blocker)
1. **DNS-Token 64 Zeichen** → AdGuard lehnt Client-ID ab (`hostname label too long: got 64, max 63`). Fix: `randomBytes(24).hex` = 48 Zeichen (`register.post.ts`), `magic/status` Regex `{64}``{1,63}`. Commit `db6db54` (gepusht + deployed).
2. **nginx-IP-Allow-Liste** auf `dns.rebreak.org` (`/control/`-API) hatte die **neue Staging-Box nicht** drin → 403 Forbidden (sah aus wie falsches PW, war IP). Fix: `allow 91.99.225.223;` in `/etc/nginx/sites-available/dns.rebreak.org` auf **rebreak-mdm** + nginx-reload. Backup liegt da (`.bak-*`).
### ⚠️ Wichtige Lektion (kostete ~Stunde): falsche Box
- `ssh rebreak-server` (49.13.55.22) = **ALTE migrierte Box**, läuft nur noch stale weiter. NICHT für Staging debuggen.
- **Echte Staging = `staging.rebreak.org` = 91.99.225.223** (`backend/`-Layout, pm2 `rebreak-staging`). Win-App + Deploy zeigen dahin. Memory `reference_rebreak_server` korrigiert.
- AdGuard-Reproduktion + DB-Checks via Infisical-Pattern: `source /etc/environment; infisical login --method=universal-auth --client-id=$INFISICAL_CLIENT_ID --client-secret=$INFISICAL_CLIENT_SECRET --silent --plain``infisical run --projectId=14b11b35-… --env=staging --token=…`.
---
## ✅ Phase A (Backend) — live auf Staging (Commit `a95e665`)
4 Blöcke + Hard-Lock + Reveal:
- Offline-Enroll → 410 (kein Klartext-PW-Download mehr); stationär nur via Magic
- `ProhibitDisablement` im Mac-DNS-Template
- Push „Neues Gerät verbunden" (mobile Tokens) aus `magic/register`
- `user_devices`-Realtime-Migration (Publication)
- Hard-Lock: server-PW (`magic_removal_password`) generiert/injiziert, Reveal NUR bei Account-Löschung (`user/delete`) + Kündigung (`stripe/webhook`) via Resend-Mail. Signing config-gated INAKTIV.
---
## ✅ Marketing — live auf **PROD** (rebreak.org)
- **Windows-Download-Seite** `/download/windows` (PC-Schutz korrekt geframt) + Installer `public/downloads/RebreakMagic-Setup.exe` (3,7 MB, aus CI-Artefakt; im Repo, überlebt `rsync --delete`)
- **Trial-Klarheit:** Hero „14 Tage kostenlos testen" + „danach ab 3,99 €/Monat" (löst free-vs-Preis-Verwirrung; 14-Tage-Trial existiert Stripe-seitig)
- **OS-Detection** (`useOS`): Windows-Besucher → Windows-Download, Mac → Mac
- **Pricing:** Device-Cross-Section + Header/Hamburger-Nav (Floating-Tabbar raus) — war nur auf Staging, jetzt Prod
- Deploy: `MARKETING_REMOTE_DIR=/var/www/marketing-prod ./scripts/deploy-marketing.sh`. **Fix:** `--info=progress2``--progress` (macOS-rsync-kompatibel).
---
## ✅ Native UI-Polish (committet LOKAL, 6 Commits ahead von origin = db6db54)
`e0eb171 e2e5a10 227c30c 77ce5e5 c3478f4 ca72437` — deployen erst per **App-Rebuild**:
- Device-PNG-Icons (`assets/devices/*.png` statt Ionicons)
- Geräte-Detail-Sheet (Tap → Status, verbunden-seit, HalfDonut), Row antippbar
- Trash/Menü raus → **chevron-forward** (Entfernen am Gerät selbst/Cooldown)
- Slot-Anzeige: **2 volle Progress-Circles** (Mobil grün / Computer blau) + darunter **animierter Gesamt-Verteilungs-Balken** (eigener, kein native; grün/blau-Segmente + Legende). `DeviceSlotDonut` (segment-API, `half`-Modus vorhanden aber ungenutzt), `DeviceDistributionBar`.
- **Status-Pill** in der Liste: Online (grün) / Cooldown · noch Xh (amber, aus `releaseRequestedAt`) / Ungeschützt (rot).
---
## 🚀 NÄCHSTE SESSION — der eigentliche Gamechanger (User-Priorität)
**„ReBreak für Windows echt magic wie Mac"** = iPhone-Supervision (no-erase Self-Bind) **auf Windows** portieren.
- **Warum Gamechanger:** erreicht die **breite Masse** — alle mit iPhone/iPad **ohne Mac** (nur Windows). MDM-enforced → **erzwingt Legend-Account** → Business/Umsatz.
- **Technisch plausibel:** `supervise-magic` ([[supervise-magic-phase-1]]) basiert auf **go-ios** (cross-platform Go → cross-compile nach Windows). Protokoll (lockdownd, MCInstall.SetCloudConfiguration, MobileBackup2) OS-agnostisch. Windows-usbmuxd via **Apple Mobile Device Support** (iTunes / „Apple Geräte"-App).
- **Ungeprüft (= Spike):** ob go-ios' Windows-usbmuxd den **schweren MobileBackup2-Flow** durchzieht. FMI/SDP-Precondition (ErrorCode 211) = gleiche Wall wie Mac.
- **Spike-Plan:** supervise-magic-Go-CLI für Windows cross-compilen → echtes x64-Windows + „Apple Geräte"-App + Test-iPhone (UTM-ARM64-VM taugt für iPhone-USB-Passthrough wahrscheinlich NICHT). rebreak-binder-Territorium.
### Weitere offene Threads (Prio absteigend)
- **Native-Rebuild + Push** der 6 lokalen UI-Commits (am iPhone testen: Detail-Sheet/Icons/Realtime/Rings/Status)
- **Phase B** Sleep/Budget (Design: [[magic-hardlock-offboarding]]) — 1-Tag/Gerät-Budget, eskal. Cooldown 4/8/12/24h, Auto-Reaktivieren 10h-default, Sleep = AdGuard filtering off
- **Signing:** Magic-Profile (Cert/Sign-Proxy) + **Windows-Code-Signing** gegen SmartScreen (Empfehlung: **Azure Trusted Signing ~10$/Mon**)
- **`magic/status` lastSeen-Heartbeat** → für echten Online/Offline-Status der Magic-Geräte in der Liste
- Pricing-Inkonsistenz: founding_desc „1 Monat Standard" vs Banner „3 Monate Legend" (+ „Standard" ist alter Plan-Name)
---
## 🧠 FAGS (Fachverband Glücksspielsucht) — strategisch
Vorstandsvorsitzender hat Interesse gemeldet (08.06.). Fachliche Validierung + Partnerschafts-/DiGA-Türen. User schreibt die Antwort-Mail. Bei Bedarf: rebreak-strategist / diga-regulatory für Tonalität + fachliche Positionierung.

View File

@ -0,0 +1,61 @@
# Session-Handoff 2026-06-08 — Preview-Page, Screenshots, Bug-Hunt
> **Fortsetzung in neuer Session mit `claude --dangerously-skip-permissions`.**
> Founder war zwischendurch weg (Familie). Diese Datei = vollständiger Stand zum nahtlosen Weitermachen.
## ✅ ERLEDIGT in Fortsetzung 2026-06-09
- **208k → 300k**: native Locales (de/en/fr/ar, inkl. Arabisch-Edge-Cases) **+ Marketing** (de/en). Alle JSON valide.
- **Bug #1 (Protection-Sheet/Push zu früh) GEFIXT**: wahre Ursache = `protectionShouldBeActive` ist Account-DEFAULT (nicht „je aktiv"). Fix = lokales AsyncStorage-Flag `protection:everActiveHere` (gesetzt nach erfolgreichem `activateUrlFilter()`), gated `recoveringFromBypass` in `lib/protection.ts`. **Live verifiziert** am Sim (Blocker-Tab zeigt kein Sheet mehr). Siehe Memory `project_protection_bypass_default_flag_fix`. NEXT_RELEASE.md gepflegt.
- **Screenshots 05 + 09 frisch aufgenommen**: 05-breathing = aktive 4-7-8-Animation („Halten/7", Runde 1/3) via SOS→3 Lyra-Turns→Chip (Koordinaten-Tap, Umlaut bricht Text-Match). 09-geraete = sauberer Devices-Screen (3/5). Beide in `public/preview/`. Neue Flows: `.maestro/screens/capture-05-breathing.yaml`, `capture-05-09-verify.yaml`.
- **Deploys (beide mit GO, beide grün):** (a) Backend `adguard.ts`-Idempotenz-Fix → `547f861` push → GH-Actions Deploy-Staging **success** (Health-Check 401). (b) Marketing **Prod**: `MARKETING_REMOTE_DIR=/var/www/marketing-prod ./scripts/deploy-marketing.sh``https://rebreak.org/preview/` **HTTP 200** (frisch, last-modified 00:48). Seiten-PW `rebreak-fags`. Link kann an Ilona.
- **Erkenntnis:** Live-Prod-nginx hat KEINE Basic-Auth (Repo `rebreak.org.conf` ist stale/zeigt :3015-Proxy; live = static aus `/var/www/marketing-prod`).
## Laufender Zustand der Umgebung (WICHTIG)
- **iOS-Simulator iPhone 17 Pro gebootet**, App `org.rebreak.app` installiert + **manuell via Google eingeloggt** (Account `nisyb@gmx.net`). ⚠️ **NICHT clearState / nicht neu installieren** → sonst Login (OAuth/2FA) weg.
- **Metro läuft** (Port 8081). **Marketing-Dev-Server** läuft (Port 3020) → `http://localhost:3020/preview`, Passwort `rebreak-fags`.
- **Live-Server** = `91.99.225.223` (staging.rebreak.org). SSH-Alias `rebreak-server` zeigt auf ALTEN 49.13.55.22 → für Live `ssh root@91.99.225.223`.
## ✅ DONE diese Session
- **Mac-Magic-502 gefixt**: verwaister AdGuard-Client gelöscht (live) + `backend/server/utils/adguard.ts` `createAdGuardClient` idempotent gemacht (bei 400 → clients/update). ⚠️ **Code uncommitted + undeployed** (Backend-Deploy braucht GO).
- **Johnson-Agent** angelegt: `.claude/agents/johnson.md` (Rebreak-Magic Mac+Win, FE+BE).
- **Ilona/FAGS-Mail** final: `docs/marketing/ilona-vorstand-mail.md` (human-style, kompakt, Gamban-Tabelle). Versendet (Typo lona→ilona gebounced, dann an `ilona.fuechtenschnieder@gluecksspielsucht.de` korrekt raus).
- **Marketing /preview-Seite** gebaut: `apps/marketing/app/pages/preview.vue` + `components/PreviewPhone.vue` + `PreviewDesktop.vue`. App-Icon-Logo, Mail-Provider-Logos (simple-icons), Schutz-Sektion pro Plattform, Gamban-Vergleich, Cooldown/„kein Gefängnis". Erwartet PNGs in `apps/marketing/public/preview/`.
- **Marketing de-AI'd** (Agent): ~45 Gedankenstriche raus, AI-Floskeln entschärft, de+en, Build grün. (Marketing nutzt `{var}`-Platzhalter, NICHT `%{var}`.)
- **rebreak-native UI-Batch** (Agent): Login-Logo zurück (`app/index.tsx`), Splash +1,5s (`components/BrandSplash.tsx`, Delays 3300/2700), DiGA-Wording raus (`DemographicsAccordion.tsx` + `DigaMissionBanner.tsx`), `clean``spielfrei` (de) / `gambling-free` (en), Lyra-Avatar (RiveAvatar) statt medkit-Icon im DigaMissionBanner.
- **BrandSplash-Crash gefixt**: `runOnJS(onDone)()` (Worklet-Crash). Splash-Delays vom Agent erhöht.
- **testIDs**: `header-avatar-btn` (`components/AppHeader.tsx`), `sos-send-btn` (`app/urge.tsx`).
- **Maestro-Screenshot-Pipeline**: `apps/rebreak-native/.maestro/screens/marketing-tour-loggedin.yaml` (single-session, tab-basiert, sheet-resilient) + `capture-marketing-loggedin.sh`. Maestro legt `takeScreenshot` in `$REPO_ROOT/screenshots/` ab → Script kopiert nach `public/preview/`.
- **settings.json allowlist** ergänzt: `dig *`, `eas build:list/view *`, `maestro *`, `xcrun simctl *`, capture-Script.
## 📸 Screenshot-Status
Verifiziert GUT (frisch, korrekt): **02-blocker, 03-blocked, 04-sos-lyra (= Coach/Lyra ✓), 06-mail, 07-community (Home-Feed, dialogfrei ✓), 07b-dm, 08-streak**. Liegen in `public/preview/`.
⚠️ **STALE/falsch**: `05-breathing.png` + `09-geraete.png` (alte Läufe, mtime 14:29/14:30) — entweder echt aufnehmen oder löschen (dann greift Platzhalter).
- **05-breathing**: SOS/Atemübung lebt unter `/urge` — Einstieg in der UI noch unklar (SOS-Button finden).
- **09-geraete**: via Einstellungen → `router.push('/devices')` (`app/settings.tsx:413`). Settings-Einstieg finden.
- Erkenntnis: **Bottom-Tabs = Home · Chat · Coach · Blocker · Mail** (NativeTabs, `app/(app)/_layout.tsx`). Profil via **Header-Avatar** (`header-avatar-btn`). ⚠️ **Blocker-Tab triggert das „Schutz ist aus"-Sheet, das modal alle Taps blockiert** → im Flow Blocker ZULETZT + „Später" wegtippen. Beim Launch „Nicht erlauben" (Screen-Time-Dialog) wegtippen.
- Capture-Run: `bash apps/rebreak-native/.maestro/screens/capture-marketing-loggedin.sh` (App muss eingeloggt + am Home sein).
## 🐞 OFFENE BUGS (vom Founder beim Testen gefunden)
1. **⚠️ KRITISCH (3× bestätigt): „Schutz ist aus"-Sheet + „Schutz manipuliert"-Push feuern zu früh/falsch.**
Root: `apps/rebreak-native/lib/protection.ts:584-590``phase='recoveringFromBypass'`, wenn `backend.protectionShouldBeActive===true && !mdmManaged && kein Filter aktiv`. Das ist **account-global, nicht pro-Gerät** → auf frischem Gerät/Sim, wo Schutz nie LOKAL aktiviert war, fälschlich „recoveringFromBypass" → Push (`app/(app)/_layout.tsx` `enforceProtection`/`notifyBypassDetected` ~Z.214) + ProtectionOffSheet erscheinen sofort + Sheet blockiert Navigation.
**FIX:** `recoveringFromBypass` nur, wenn ein **lokales „war hier schon mal aktiv"-Signal** existiert (persistierter Flag nach erfolgreichem `activate()`), sonst `inactive`/`setup`. Sensible Kern-Schutz-Logik → **mit Founder abstimmen** (gewünschtes Verhalten), nicht blind hacken.
2. **208k → 300k** (SAFE, einfach anwenden — wurde beim Permission-Stop unterbrochen):
Hartkodiert in `apps/rebreak-native/locales/{de,en,fr,ar}.json`. Keys: `subtitle`, `domain_limit_desc`, `protection_subtitle_legend`, `protection_subtitle_pro`, `setup_step3_subtitle_pending`, `android_step1_subtitle_pending`, `feat_blocklist`. Exakter Befehl:
```
cd apps/rebreak-native/locales
for f in de.json en.json fr.json ar.json; do
sed -i '' -e 's/208\.000/300.000/g' -e 's/208,000/300,000/g' -e 's/208 000/300 000/g' -e 's/208k/300k/g' "$f"
done
```
(Dynamische Stellen via `state.blocklistCount` sind ok.) Danach JSON validieren.
3. **Extension-Versions-Mismatch** (Build-Warnung): App-Extension `0.4.3` vs App `0.4.4` (CFBundleShortVersionString) → vor nächstem Release angleichen (zied).
## ⏭️ NÄCHSTE SCHRITTE (Bypass-Session)
1. **208k→300k** anwenden (safe).
2. **Protection-Sheet/Push-Bug** fixen (Bug 1) — mit Founder-Input zum Verhalten.
3. Screenshots **05-breathing** (`/urge`), **09-geraete** (Settings→Geräte) echt aufnehmen; Flow erweitern. Ggf. `07c-calls`, `12-domains` (custom domains), `01a/b/c-onboarding` ergänzen. Stale 05/09 vorher löschen.
4. Restliche Preview-Platzhalter (`06b-mail-connect`, `13-admin-approve` Admin-App) später.
5. **Pending Deploys (brauchen GO):** (a) Mac-Magic `adguard.ts`-Idempotenz-Fix → Backend push; (b) Marketing `/preview` deployen → Link an Ilona.
## Agenten-Lernpunkt
Background-Agents (ahmed) **können im Hintergrund keine Bash-Permission-Prompts bestätigt bekommen** → für Bash-lastige Tasks (maestro/xcrun) hängen sie. In der `--dangerously-skip-permissions`-Session entfällt das Problem.

View File

@ -0,0 +1,97 @@
# Ilona / FAGS — Mail an Ilona (Antwort auf ihre 2. PN)
**Kontext:** Ilona Füchtenschnieder-Petry (Vorsitzende fags e.V.) hat warm geantwortet,
will den Vorschlag **intern mit dem Vorstand besprechen** und bittet um einen **Text als
Mail** an `ilona.fuechtenschnieder@gluecksspielsucht.de`, den sie weiterleiten kann.
> **Regel:** Claude entwirft, Ram versendet selbst von seiner Adresse.
## Stil-Hinweis
**Kompakte Human-Style-Fassung (Stand 08.06., final).** Bewusst KURZ (lange Mails
schrecken ab), mit Tabelle (informativer als Fließtext) und „bei Interesse erzähle ich gern
mehr"-Haken. Ohne Gedankenstriche im Satz / KI-Polish. Tipp vorm Senden: einmal laut lesen,
ein paar Sätze in eigenen Worten umtippen, dann ist „KI oder nicht" erledigt.
## ⚠️ Vor dem Senden ausfüllen / bestätigen
1. **Preise** ✅ geprüft (Pro 3,99 €/Mt, Legend 7,99 €/Mt; Jahres-Optionen 29/59 €, 14-Tage-Trial).
2. **„Warst du das auch?"** ✅ bestätigt: *war nicht ich*.
3. **Sonntagscafé-Ort**: offen gehalten (Anfahrt ok) — konkreten Ort nur einsetzen, wenn sicher.
4. **PDF mit Screenshots**: angeboten; Screenshot-Brief separat, sobald gewünscht.
5. **Gamban-Tabelle**: Fakten geprüft (gamban.com/pricing, gamban.com/blocking, GambleAware,
Stand Juni 2026), bewusst FAIR (inkl. Gamban-Stärken Linux, Preis, Reifegrad). £/€ ca. 1,17.
6. **Tabellen-Rendering**: Markdown-Tabelle rendert in vielen Mail-Clients nicht sauber.
Wenn's verrutscht: Mail als HTML senden oder Tabelle als Bild/PDF beilegen.
---
## Mail-Entwurf (kompakt, mit Tabelle)
**An:** ilona.fuechtenschnieder@gluecksspielsucht.de
**Betreff:** Rebreak, kurz zum Weiterleiten an den Vorstand
> Hallo Ilona,
>
> danke dir wirklich, dass du Rebreak intern besprichst und weitergibst. Meine Geschichte
> kennst du ja schon, deshalb fasse ich mich kurz. (Und zu deiner Frage: nein, das frühere
> Angebot ohne Erklärung war nicht von mir.)
>
> Kurz gesagt: Rebreak sperrt Glücksspiel geräteweit, auch die nicht-lizenzierten
> Offshore-Seiten, die OASIS nicht erreicht. Es ist aber mehr als ein Blocker. Es begleitet
> auch in den akuten Druckmomenten und hat eine Community, die sich gegenseitig schützt. Alles
> DSGVO-konform auf Servern in Deutschland, sichtbar nur über einen Spitznamen.
>
> Hier der Vergleich zu Gamban, das in eurem Beitrag ja als möglicher Baustein genannt wurde.
> Nur die Fakten:
>
> | | Rebreak | Gamban |
> |---|---|---|
> | Plattformen | iPhone, Android, Mac, Windows | iOS, Android, Mac, Windows, Linux |
> | Offshore-Seiten blockieren | ✓ (>300.000 Domains) | ✓ (40.000+) |
> | Casino-Werbemails im Postfach | ✓ in Echtzeit | nein |
> | Begleitung im Druckmoment (KI 24/7, Atemübung) | ✓ („Lyra") | nein |
> | Community, gegenseitiger Halt | ✓ | nein |
> | Datenhaltung | Deutschland, DSGVO, nur Spitzname | UK-Anbieter |
> | Sprache, Markt | Deutsch, OASIS-Kontext | Englisch, UK |
> | Preis pro Monat | Pro 3,99 €, Legend 7,99 € | ca. 2,90 € |
> | Reifegrad | Beta, im Praxistest | etabliert (bis 99 % wirksam) |
>
> Ich will mir kein Urteil anmaßen, das überlasse ich gern dir. :)
>
> Zwei Angebote noch:
> - Rebreak ist gerade in der Beta. Schick mir ein paar Mailadressen (Android oder iPhone) der
> Kolleginnen und Kollegen, dann trage ich euch als Testende ein. Für fags gibt es kostenlose
> Legend-Lizenzen.
> - Ich komme gern persönlich zu eurem Sonntagscafé und stelle Rebreak vor.
>
> Wenn das Thema für euch interessant ist, erzähle ich sehr gern mehr (zu Mail-Schutz,
> Community, Datenschutz und dem Weg Richtung DiGA) und schicke auf Wunsch ein kompaktes PDF
> mit Screenshots.
>
> Und zum Schluss noch etwas Persönliches, das mir wirklich am Herzen liegt: Rebreak ist nicht
> aus dem Gedanken entstanden, eine App zu bauen und damit Geld zu verdienen. Es ist in einer
> dunklen Zeit entstanden, in der ich selbst nach Halt gesucht und nichts gefunden habe, das
> mir wirklich helfen konnte. Ich bin nicht nur der Entwickler, sondern auch einer von den
> Menschen, für die diese App gedacht ist. Ich kenne diesen Kreislauf von innen, und ich weiß,
> wie einsam er sich anfühlt. Wenn Rebreak auch nur einem einzigen Menschen hilft, da wieder
> herauszufinden, hat sich für mich jede Mühe gelohnt. Und wenn ich eines Tages als Vater von
> zwei Kindern von Rebreak leben kann, dann möchte ich nichts lieber tun, als genau dafür da
> zu sein und Menschen zu helfen.
>
> Dürfte ich innerhalb von Rebreak auf fags und eure Hilfe verweisen?
>
> Herzliche Grüße
> Ram
---
## Forums-Antwort (1 Zeile, Faden warm halten)
> Hallo Ilona, das freut mich sehr, danke dir! Ich schicke dir den Text gleich als Mail an deine
> Adresse, dann hast du ihn direkt zum Weiterleiten. Herzliche Grüße, Ram
---
## Lange Fassung (Backup)
Die ausführliche Variante (alle Features einzeln, große Tabelle, langer persönlicher Schluss)
gab es als Vorstufe — bei Bedarf aus der Git-Historie holen. Bewusst durch die kompakte Fassung
ersetzt, weil lange Mails abschrecken und die Tabelle die Detailtiefe trägt.

View File

@ -1,16 +1,16 @@
# Graph Report - rebreak-monorepo (2026-06-10) # Graph Report - rebreak-monorepo (2026-06-10)
## Corpus Check ## Corpus Check
- 1373 files · ~2,163,903 words - 1373 files · ~2,165,389 words
- Verdict: corpus is large enough that graph structure adds value. - Verdict: corpus is large enough that graph structure adds value.
## Summary ## Summary
- 14353 nodes · 19343 edges · 877 communities (828 shown, 49 thin omitted) - 14351 nodes · 19342 edges · 870 communities (824 shown, 46 thin omitted)
- Extraction: 99% EXTRACTED · 1% INFERRED · 0% AMBIGUOUS · INFERRED: 219 edges (avg confidence: 0.8) - Extraction: 99% EXTRACTED · 1% INFERRED · 0% AMBIGUOUS · INFERRED: 217 edges (avg confidence: 0.8)
- Token cost: 0 input · 0 output - Token cost: 0 input · 0 output
## Graph Freshness ## Graph Freshness
- Built from commit: `6937ff15` - Built from commit: `df3c4faf`
- Run `git rev-parse HEAD` and compare to check if the graph is stale. - Run `git rev-parse HEAD` and compare to check if the graph is stale.
- Run `graphify update .` after code changes (no API cost). - Run `graphify update .` after code changes (no API cost).
@ -752,15 +752,8 @@
- [[_COMMUNITY_Community 865|Community 865]] - [[_COMMUNITY_Community 865|Community 865]]
- [[_COMMUNITY_Community 866|Community 866]] - [[_COMMUNITY_Community 866|Community 866]]
- [[_COMMUNITY_Community 867|Community 867]] - [[_COMMUNITY_Community 867|Community 867]]
- [[_COMMUNITY_Community 868|Community 868]]
- [[_COMMUNITY_Community 869|Community 869]]
- [[_COMMUNITY_Community 870|Community 870]]
- [[_COMMUNITY_Community 871|Community 871]]
- [[_COMMUNITY_Community 872|Community 872]] - [[_COMMUNITY_Community 872|Community 872]]
- [[_COMMUNITY_Community 873|Community 873]]
- [[_COMMUNITY_Community 874|Community 874]] - [[_COMMUNITY_Community 874|Community 874]]
- [[_COMMUNITY_Community 875|Community 875]]
- [[_COMMUNITY_Community 876|Community 876]]
## God Nodes (most connected - your core abstractions) ## God Nodes (most connected - your core abstractions)
1. `usePrisma()` - 299 edges 1. `usePrisma()` - 299 edges
@ -803,7 +796,7 @@
- **IEC-62304-Traceability (Anforderung→Risikomaßnahme→Test→Lyra-Eval)** — diga_03_req_lyra, diga_04_risk_lyra_01_verpasste_krise, diga_05b_test_verifikation, diga_05c_crisis_detection_recall [EXTRACTED 0.95] - **IEC-62304-Traceability (Anforderung→Risikomaßnahme→Test→Lyra-Eval)** — diga_03_req_lyra, diga_04_risk_lyra_01_verpasste_krise, diga_05b_test_verifikation, diga_05c_crisis_detection_recall [EXTRACTED 0.95]
- **FAGS/NLS Förder- & Partnerschafts-Strategie (Träger, Forschung, Geldgeber)** — entity_fags, entity_nls, entity_nbank, entity_uni_bremen, entity_step_lukaswerk, entity_bfarm [EXTRACTED 0.85] - **FAGS/NLS Förder- & Partnerschafts-Strategie (Träger, Forschung, Geldgeber)** — entity_fags, entity_nls, entity_nbank, entity_uni_bremen, entity_step_lukaswerk, entity_bfarm [EXTRACTED 0.85]
## Communities (877 total, 49 thin omitted) ## Communities (870 total, 46 thin omitted)
### Community 0 - "i18n: Blocker/Activation Strings" ### Community 0 - "i18n: Blocker/Activation Strings"
Cohesion: 0.01 Cohesion: 0.01
@ -823,35 +816,35 @@ Nodes (204): blocker, activate_app_lock_failed_msg, activate_app_lock_failed_tit
### Community 4 - "Debug & Dev Tools" ### Community 4 - "Debug & Dev Tools"
Cohesion: 0.04 Cohesion: 0.04
Nodes (52): createChatMessage(), countPostLikes(), createComment(), createCommentLike(), deleteCommentLike(), deletePostLike(), deleteUserPosts(), getCommentLike() (+44 more) Nodes (53): VALID_COUNTRIES, SupportedCountry, deleteUserPosts(), writeConsentRevoke(), decideCuratedDomain(), getCuratedDomains(), suggestCuratedDomain(), redeemDigaCode() (+45 more)
### Community 5 - "Backend API Routes" ### Community 5 - "Backend API Routes"
Cohesion: 0.06 Cohesion: 0.08
Nodes (29): EMPTY_STATS, GamesScreen(), GameStat, GameStats, LastScore, ScoreProgressBar(), ScoreProgressBarProps, useSnakeSounds() (+21 more) Nodes (21): ScoreProgressBar(), ScoreProgressBarProps, useSnakeSounds(), checkWinner(), Dir, MEMORY_EMOJIS, OPPOSITES, Pos (+13 more)
### Community 6 - "Backend Tests & Auth Routes" ### Community 6 - "Backend Tests & Auth Routes"
Cohesion: 0.05 Cohesion: 0.04
Nodes (64): getConsentLogsByUser(), setMailConnectionConsent(), writeConsentGrant(), getBlocklistedDomainsSet(), consumeOauthPendingState(), createOauthPendingState(), deleteAllMailConnections(), deleteMailConnection() (+56 more) Nodes (68): getConsentLogsByUser(), setMailConnectionConsent(), writeConsentGrant(), getBlocklistedDomainsSet(), getCustomMailDisplayNames(), getMailDisplayNamePatterns(), consumeOauthPendingState(), countMailConnections() (+60 more)
### Community 7 - "Consent & Magic API Routes" ### Community 7 - "Consent & Magic API Routes"
Cohesion: 0.04 Cohesion: 0.04
Nodes (48): DevicesScreen(), formatCountdown(), formatLastSeen(), formatSince(), MobileDeviceRow(), mobileIcon(), SectionCard(), SectionLabel() (+40 more) Nodes (53): DevicesScreen(), formatCountdown(), formatLastSeen(), formatSince(), MobileDeviceRow(), mobileIcon(), SectionCard(), SectionLabel() (+45 more)
### Community 8 - "i18n: Pricing Strings" ### Community 8 - "i18n: Pricing Strings"
Cohesion: 0.03 Cohesion: 0.03
Nodes (95): CoachTabRedirect(), CooldownTestModeToggle(), DebugScreen(), DebugStub(), LogLine(), LyraEmotionPreviewCard(), ONBOARDING_STEPS, OnboardingStepValue (+87 more) Nodes (76): CoachTabRedirect(), CooldownTestModeToggle(), DebugScreen(), DebugStub(), LogLine(), LyraEmotionPreviewCard(), ONBOARDING_STEPS, OnboardingStepValue (+68 more)
### Community 9 - "Android DNS Filter (Kotlin)" ### Community 9 - "Android DNS Filter (Kotlin)"
Cohesion: 0.05 Cohesion: 0.04
Nodes (39): prismaMock, requireUserMock, isAdminUser(), cancelCooldown(), createCooldown(), getActiveCooldown(), resolveCooldown(), touchDevice() (+31 more) Nodes (48): prismaMock, requireUserMock, isAdminUser(), cleanupStaleDevices(), findUserDevice(), listUserDevices(), registerDevice(), touchDevice() (+40 more)
### Community 10 - "i18n: Pricing Strings" ### Community 10 - "i18n: Pricing Strings"
Cohesion: 0.07 Cohesion: 0.05
Nodes (21): Any, Boolean, ByteArray, Int, String, Boolean, HashList, Int (+13 more) Nodes (37): Any, Boolean, ByteArray, Int, String, Boolean, HashList, Int (+29 more)
### Community 11 - "i18n: Landing Page Strings" ### Community 11 - "i18n: Landing Page Strings"
Cohesion: 0.09 Cohesion: 0.07
Nodes (66): runtimeFiles, buildFiles, buildTargetsCommandComponents, cFileExtensions, cleanCommandsComponents, abi, artifactName, output (+58 more) Nodes (89): runtimeFiles, buildFiles, buildTargetsCommandComponents, cFileExtensions, cleanCommandsComponents, abi, artifactName, output (+81 more)
### Community 12 - "Tauri ACL Manifests (Magic Win)" ### Community 12 - "Tauri ACL Manifests (Magic Win)"
Cohesion: 0.02 Cohesion: 0.02
@ -875,27 +868,27 @@ Nodes (87): landing, blocker_badge, blocker_desc, blocker_feat_cooldown, blocker
### Community 17 - "App Root Layout & Shell" ### Community 17 - "App Root Layout & Shell"
Cohesion: 0.04 Cohesion: 0.04
Nodes (53): BlockerScreen(), makeModalStyles(), makeStyles(), RoomDetail, RoomScreen(), RoomSettingsModal(), AuthCallback(), CooldownBanner() (+45 more) Nodes (64): CallScreen(), fmtDuration(), DmData, DmHistoryResponse, DmScreen(), makeStyles(), MediaLibraryModule, makeModalStyles() (+56 more)
### Community 18 - "Community 18" ### Community 18 - "Community 18"
Cohesion: 0.06 Cohesion: 0.05
Nodes (43): ping(), useLastSeenHeartbeat(), invalidateMe(), listeners, Me, OnboardingStep, apiFetch(), apiUrl (+35 more) Nodes (49): CreateRoomSheet(), Props, FormSheet(), FormSheetProps, ProtectionOffSheet(), StepState, Props, SheetField (+41 more)
### Community 19 - "Community 19" ### Community 19 - "Community 19"
Cohesion: 0.02 Cohesion: 0.02
Nodes (82): auth, acceptTerms, acceptTermsSuffix, alreadyRegistered, appleSignin, appleSignup, backToLogin, backToLoginPlain (+74 more) Nodes (82): auth, acceptTerms, acceptTermsSuffix, alreadyRegistered, appleSignin, appleSignup, backToLogin, backToLoginPlain (+74 more)
### Community 20 - "Community 20" ### Community 20 - "Community 20"
Cohesion: 0.04 Cohesion: 0.05
Nodes (62): DmData, DmHistoryResponse, DmScreen(), makeStyles(), MediaLibraryModule, queryClient, RootLayoutInner(), PickerOption (+54 more) Nodes (48): EASE_OUT, LandingScreen(), { width: SW, height: SH }, queryClient, RootLayoutInner(), ConfirmOtpScreen(), OTP_INPUT_STYLE, OTP_INPUT_STYLE (+40 more)
### Community 21 - "Community 21" ### Community 21 - "Community 21"
Cohesion: 0.04 Cohesion: 0.04
Nodes (54): MailScreen(), PLAN_LABEL, PROVIDER_CONFIG, ConnectBody, ConnectResult, PROVIDER_DOMAIN_MAP, useMailConnect(), UseMailConnectReturn (+46 more) Nodes (50): MailScreen(), PLAN_LABEL, ConnectBody, ConnectResult, PROVIDER_DOMAIN_MAP, useMailConnect(), UseMailConnectReturn, useMailDisconnect() (+42 more)
### Community 22 - "Community 22" ### Community 22 - "Community 22"
Cohesion: 0.06 Cohesion: 0.03
Nodes (37): AddDomainSheet(), Props, DomainGrid(), DomainTile(), Props, STATUS_PRIORITY, timeAgo(), Props (+29 more) Nodes (60): BlockerScreen(), AuthCallback(), AddDomainSheet(), Props, CooldownBanner(), Props, DomainGrid(), DomainTile() (+52 more)
### Community 23 - "Community 23" ### Community 23 - "Community 23"
Cohesion: 0.03 Cohesion: 0.03
@ -914,16 +907,16 @@ Cohesion: 0.03
Nodes (68): dependencies, @config-plugins/react-native-webrtc, expo, expo-apple-authentication, expo-application, expo-av, expo-blur, expo-build-properties (+60 more) Nodes (68): dependencies, @config-plugins/react-native-webrtc, expo, expo-apple-authentication, expo-application, expo-av, expo-blur, expo-build-properties (+60 more)
### Community 27 - "Community 27" ### Community 27 - "Community 27"
Cohesion: 0.05 Cohesion: 0.06
Nodes (57): arcPath(), HalfDonut(), HalfDonutSegment, polar(), Props, DiGaMilestoneModal(), MILESTONES, DeviceDetail (+49 more) Nodes (47): ApprovedDomainsData, BackendCooldownEntry, CooldownHistoryData, DemographicsResponse, formatDuration(), formatStartedAt(), mapCooldownEntry(), ProtectionCoverageData (+39 more)
### Community 28 - "Community 28" ### Community 28 - "Community 28"
Cohesion: 0.04 Cohesion: 0.04
Nodes (65): appHeader, editProfile, settings, sosLabel, sosSubtitle, sosTagline, applock, prompt (+57 more) Nodes (65): appHeader, editProfile, settings, sosLabel, sosSubtitle, sosTagline, applock, prompt (+57 more)
### Community 29 - "Community 29" ### Community 29 - "Community 29"
Cohesion: 0.09 Cohesion: 0.05
Nodes (26): ChatHeaderStatus(), Props, UserDevice, DeviceLimitRow(), formatLastSeen(), platformIcon(), LastSeenMap, useLastSeenBatch() (+18 more) Nodes (48): UserDevice, DeviceLimitRow(), formatLastSeen(), platformIcon(), SuggestResult, SuggestState, useCuratedSuggest(), LastSeenMap (+40 more)
### Community 30 - "Community 30" ### Community 30 - "Community 30"
Cohesion: 0.06 Cohesion: 0.06
@ -934,16 +927,16 @@ Cohesion: 0.05
Nodes (54): Config: AdGuard DoH Vars (ADGUARD_BASE_URL/USER/PASSWORD), Config: ENCRYPTION_KEY (AES-256 for DB fields incl mdmDnsToken), Config: GROQ_API_KEY LLM Provider Var, Config: Infisical Secret Injection (no .env files), Backend Environment Variables Doc, RebreakMagic Device-Binding API Documentation, Component: BreathingDrawer (Atemuebung), CHIP_SETS.start Atemuebung Chip (sosConstants.ts, hardcoded) (+46 more) Nodes (54): Config: AdGuard DoH Vars (ADGUARD_BASE_URL/USER/PASSWORD), Config: ENCRYPTION_KEY (AES-256 for DB fields incl mdmDnsToken), Config: GROQ_API_KEY LLM Provider Var, Config: Infisical Secret Injection (no .env files), Backend Environment Variables Doc, RebreakMagic Device-Binding API Documentation, Component: BreathingDrawer (Atemuebung), CHIP_SETS.start Atemuebung Chip (sosConstants.ts, hardcoded) (+46 more)
### Community 32 - "Community 32" ### Community 32 - "Community 32"
Cohesion: 0.07 Cohesion: 0.06
Nodes (33): autoReleaseInactiveDevices(), bindDeviceToUser(), cancelDeviceRelease(), cleanupStaleDevices(), countActiveMagicBindings(), deleteUserDevice(), DEVICE_SELECT, DeviceRecord (+25 more) Nodes (37): autoReleaseInactiveDevices(), bindDeviceToUser(), cancelDeviceRelease(), countActiveMagicBindings(), deleteUserDevice(), DEVICE_SELECT, DeviceRecord, ensureMagicRemovalPassword() (+29 more)
### Community 33 - "Community 33" ### Community 33 - "Community 33"
Cohesion: 0.08 Cohesion: 0.08
Nodes (22): Data, Error, HashList, Int, NEPacketTunnelNetworkSettings, NEProviderStopReason, NSObject, ExtLog (+14 more) Nodes (22): Data, Error, HashList, Int, NEPacketTunnelNetworkSettings, NEProviderStopReason, NSObject, ExtLog (+14 more)
### Community 34 - "Community 34" ### Community 34 - "Community 34"
Cohesion: 0.13 Cohesion: 0.14
Nodes (16): addStreakEvent(), deleteUserStreaks(), getActiveStreak(), getStreakEvents(), resetStreak(), updateStreakSavings(), upsertStreak(), createUrgeLog() (+8 more) Nodes (18): cancelCooldown(), createCooldown(), getActiveCooldown(), resolveCooldown(), appendProtectionEvent(), computeProtectionCoverage(), getLastProtectionEvent(), ProtectionCoverage (+10 more)
### Community 35 - "Community 35" ### Community 35 - "Community 35"
Cohesion: 0.05 Cohesion: 0.05
@ -962,8 +955,8 @@ Cohesion: 0.06
Nodes (46): jsonFile, kind, cache-v2, cmakeFiles-v1, codemodel-v2, cmake, generator, paths (+38 more) Nodes (46): jsonFile, kind, cache-v2, cmakeFiles-v1, codemodel-v2, cmake, generator, paths (+38 more)
### Community 39 - "Community 39" ### Community 39 - "Community 39"
Cohesion: 0.50 Cohesion: 0.05
Nodes (3): callHandler(), g, mocks Nodes (42): Lang, LANG_NAME, LANGS, LYRA_TOPICS, LyraTopic, TOPIC_HINTS, postFromCatalog(), postFromLLM() (+34 more)
### Community 40 - "Community 40" ### Community 40 - "Community 40"
Cohesion: 0.07 Cohesion: 0.07
@ -994,12 +987,12 @@ Cohesion: 0.07
Nodes (32): Any, Bool, ExpoReactNativeFactoryDelegate, NSUserActivity, RCTBridge, RCTReactNativeFactory, String, UIApplication (+24 more) Nodes (32): Any, Bool, ExpoReactNativeFactoryDelegate, NSUserActivity, RCTBridge, RCTReactNativeFactory, String, UIApplication (+24 more)
### Community 47 - "Community 47" ### Community 47 - "Community 47"
Cohesion: 0.06 Cohesion: 0.04
Nodes (44): AppLayout(), DmConvUnreadSlice, NotificationsScreen(), AppHeader(), Props, HeroShieldCheck(), Props, createNativeTabNavigator (+36 more) Nodes (65): AppLayout(), DmConvUnreadSlice, NotificationsScreen(), PickerOption, PLAN_ACCENT, Section, SectionRow, SettingsScreen() (+57 more)
### Community 48 - "Community 48" ### Community 48 - "Community 48"
Cohesion: 0.09 Cohesion: 0.12
Nodes (21): Candidate, detectAndSaveFeedback(), speakCartesia(), speakElevenLabs(), speakGoogle(), deleteMemoryById(), enforceMaxMemories(), getMemoriesForUser() (+13 more) Nodes (12): Candidate, detectAndSaveFeedback(), PROVIDER_CONFIG, deleteMemoryById(), enforceMaxMemories(), getMemoriesForUser(), LyraMemoryRow, markReferenced() (+4 more)
### Community 49 - "Community 49" ### Community 49 - "Community 49"
Cohesion: 0.08 Cohesion: 0.08
@ -1007,7 +1000,7 @@ Nodes (40): ProtectionLogCard(), nickname, error_invalid_input, lyra, placeholde
### Community 50 - "Community 50" ### Community 50 - "Community 50"
Cohesion: 0.08 Cohesion: 0.08
Nodes (18): ContentView, Bool, String, String, Bool, Int, String, Binding (+10 more) Nodes (17): ContentView, Bool, String, String, Bool, Int, String, WizardStep (+9 more)
### Community 51 - "Community 51" ### Community 51 - "Community 51"
Cohesion: 0.05 Cohesion: 0.05
@ -1018,12 +1011,12 @@ Cohesion: 0.07
Nodes (38): pnpm-workspace.yaml (apps/*, backend), DiGA-Listung (BfArM) Pfad, Landesfachstelle Glücksspielsucht Niedersachsen (LSG-Nds), MHH AG Verhaltenssüchte (Prof. Astrid Müller), NBank LOI förderstrategie, FAGS-Outreach-Paket Rebreak Beta + NBank-LOIs, Fachverband Glücksspielsucht e.V. (FAGS), Bielefeld, MDM-Lock Add-On (3-5€ on top auf Pro/Legend) (+30 more) Nodes (38): pnpm-workspace.yaml (apps/*, backend), DiGA-Listung (BfArM) Pfad, Landesfachstelle Glücksspielsucht Niedersachsen (LSG-Nds), MHH AG Verhaltenssüchte (Prof. Astrid Müller), NBank LOI förderstrategie, FAGS-Outreach-Paket Rebreak Beta + NBank-LOIs, Fachverband Glücksspielsucht e.V. (FAGS), Bielefeld, MDM-Lock Add-On (3-5€ on top auf Pro/Legend) (+30 more)
### Community 53 - "Community 53" ### Community 53 - "Community 53"
Cohesion: 0.07 Cohesion: 0.09
Nodes (39): CallScreen(), fmtDuration(), ChatScreen(), DmItem(), formatTime(), makeStyles(), FilterChip, HomeScreen() (+31 more) Nodes (33): ChatScreen(), DmItem(), formatTime(), makeStyles(), FilterChip, HomeScreen(), ComposeCard(), Props (+25 more)
### Community 54 - "Community 54" ### Community 54 - "Community 54"
Cohesion: 0.11 Cohesion: 0.14
Nodes (26): Bool, Double, Int, String, UInt32, Double, String, URL (+18 more) Nodes (21): Bool, Double, Int, String, UInt32, Double, Bool, Double (+13 more)
### Community 55 - "Community 55" ### Community 55 - "Community 55"
Cohesion: 0.06 Cohesion: 0.06
@ -1046,8 +1039,8 @@ Cohesion: 0.19
Nodes (32): runtimeFiles, buildFiles, buildTargetsCommandComponents, cFileExtensions, cleanCommandsComponents, abi, artifactName, output (+24 more) Nodes (32): runtimeFiles, buildFiles, buildTargetsCommandComponents, cFileExtensions, cleanCommandsComponents, abi, artifactName, output (+24 more)
### Community 60 - "Community 60" ### Community 60 - "Community 60"
Cohesion: 0.12 Cohesion: 0.09
Nodes (27): DiGA/MDR Dossier-Plan & Arbeitsteilung, Kostenlose BfArM-Hersteller-Beratung (Klasse I/IIa-Hebel), Zweckbestimmung / Intended Use v0 (Dok 01), Indikation Glücksspielstörung (ICD-10 F63.0 / ICD-11 6C50), Intended-Use-Claim (Begleitung, kein Therapie-/Diagnose-Ersatz), MDR Rule 11 Klassifizierung (I/IIa, größter Kostenhebel), REQ-MAGIC Selbstbindung (RebreakMagic Lock-Modus, 24h-Cooldown), REQ-MAIL Mail-Schutz (deterministische Trigger-Mail-Entfernung) (+19 more) Nodes (32): DiGA/MDR Dossier-Plan & Arbeitsteilung, Kostenlose BfArM-Hersteller-Beratung (Klasse I/IIa-Hebel), Zweckbestimmung / Intended Use v0 (Dok 01), Indikation Glücksspielstörung (ICD-10 F63.0 / ICD-11 6C50), Intended-Use-Claim (Begleitung, kein Therapie-/Diagnose-Ersatz), MDR Rule 11 Klassifizierung (I/IIa, größter Kostenhebel), REQ-MAGIC Selbstbindung (RebreakMagic Lock-Modus, 24h-Cooldown), REQ-MAIL Mail-Schutz (deterministische Trigger-Mail-Entfernung) (+24 more)
### Community 61 - "Community 61" ### Community 61 - "Community 61"
Cohesion: 0.19 Cohesion: 0.19
@ -1070,8 +1063,8 @@ Cohesion: 0.06
Nodes (30): artifacts, backtrace, backtraceGraph, commands, files, nodes, compileGroups, id (+22 more) Nodes (30): artifacts, backtrace, backtraceGraph, commands, files, nodes, compileGroups, id (+22 more)
### Community 66 - "Community 66" ### Community 66 - "Community 66"
Cohesion: 0.08 Cohesion: 0.07
Nodes (3): RebreakProtectionModule, Module, RebreakProtectionModule Nodes (5): RebreakProtectionModule, Module, NETunnelProviderManager, DisableResult, RebreakProtectionModule
### Community 67 - "Community 67" ### Community 67 - "Community 67"
Cohesion: 0.16 Cohesion: 0.16
@ -1079,15 +1072,15 @@ Nodes (24): Bool, Date, String, URL, Codable, MagicAPIClient.swift (/api/magic/*
### Community 68 - "Community 68" ### Community 68 - "Community 68"
Cohesion: 0.15 Cohesion: 0.15
Nodes (29): refreshAndSaveTokens(), assertEnv(), clearConnectionError(), coalescePending, decrypt(), encrypt(), getCredentialsForConnection(), getKey() (+21 more) Nodes (31): refreshAndSaveTokens(), assertEnv(), clearConnectionError(), coalescePending, decrypt(), encrypt(), getCredentialsForConnection(), getKey() (+23 more)
### Community 69 - "Community 69" ### Community 69 - "Community 69"
Cohesion: 0.09 Cohesion: 0.07
Nodes (24): landing, settings, headerMenu, debug, logout, home, greeting_day, greeting_evening (+16 more) Nodes (31): landing, settings, games, back_to_picker, last_score, skeleton_footer, subtitle, headerMenu (+23 more)
### Community 70 - "Community 70" ### Community 70 - "Community 70"
Cohesion: 0.06 Cohesion: 0.06
Nodes (31): cta_button, cta_title, resources, blocklist_desc, blocklist_title, chart_label, fact1_text, fact1_title (+23 more) Nodes (34): nav, download_app, login, cta_button, cta_title, resources, blocklist_desc, blocklist_title (+26 more)
### Community 71 - "Community 71" ### Community 71 - "Community 71"
Cohesion: 0.06 Cohesion: 0.06
@ -1102,12 +1095,12 @@ Cohesion: 0.11
Nodes (20): Bool, MagicDevice, String, Timer, Void, Date, MagicDevice, String (+12 more) Nodes (20): Bool, MagicDevice, String, Timer, Void, Date, MagicDevice, String (+12 more)
### Community 74 - "Community 74" ### Community 74 - "Community 74"
Cohesion: 0.11 Cohesion: 0.10
Nodes (23): Any, Binding, Bool, Configuration, ErrorDetails, String, Field, PrefilterFetchInterval (+15 more) Nodes (24): Any, Binding, Bool, Configuration, ErrorDetails, String, CaseIterable, Field (+16 more)
### Community 75 - "Community 75" ### Community 75 - "Community 75"
Cohesion: 0.10 Cohesion: 0.09
Nodes (27): delphi GmbH / DigiSucht (Versorgungsforschung, Triage-Verweisziel), FAGS / fags e.V. (Fachverband Glücksspielsucht, Ilona Füchtenschnieder), Gamban (UK-Wettbewerber, Glücksspiel-Blocker), GGL (Gemeinsame Glücksspielbehörde der Länder, OASIS-Aufsicht), MHH AG Verhaltenssüchte (Med. Hochschule Hannover), NBank Niedersachsen (Gründungskredit-Geber), NLS Hannover (Niedersächsische Landesstelle für Suchtfragen), OASIS (staatliches Spielersperrsystem) (+19 more) Nodes (30): delphi GmbH / DigiSucht (Versorgungsforschung, Triage-Verweisziel), FAGS / fags e.V. (Fachverband Glücksspielsucht, Ilona Füchtenschnieder), Gamban (UK-Wettbewerber, Glücksspiel-Blocker), GGL (Gemeinsame Glücksspielbehörde der Länder, OASIS-Aufsicht), MHH AG Verhaltenssüchte (Med. Hochschule Hannover), NBank Niedersachsen (Gründungskredit-Geber), NLS Hannover (Niedersächsische Landesstelle für Suchtfragen), OASIS (staatliches Spielersperrsystem) (+22 more)
### Community 76 - "Community 76" ### Community 76 - "Community 76"
Cohesion: 0.07 Cohesion: 0.07
@ -1122,12 +1115,12 @@ Cohesion: 0.20
Nodes (20): AppHandle, Option, Result, String, activateProtection(), cancelRelease(), getState(), logout() (+12 more) Nodes (20): AppHandle, Option, Result, String, activateProtection(), cancelRelease(), getState(), logout() (+12 more)
### Community 79 - "Community 79" ### Community 79 - "Community 79"
Cohesion: 0.10 Cohesion: 0.11
Nodes (23): crisisPrompts, EvalPrompt, EXPECTED_CRISIS_MATCHES, harmlessPrompts, PROMPTS_DIR, REQ-LYRA Lyra KI-Coach & Krisen-Behandlung (höchste Sicherheitsrelevanz), Deterministischer Krisen-Keyword-Pre-Filter (crisis-filter.ts), R-LYRA-01 verpasste Krise/Suizidalität (S4, Top-Risiko) (+15 more) Nodes (21): crisisPrompts, EvalPrompt, EXPECTED_CRISIS_MATCHES, harmlessPrompts, PROMPTS_DIR, REQ-LYRA Lyra KI-Coach & Krisen-Behandlung (höchste Sicherheitsrelevanz), Deterministischer Krisen-Keyword-Pre-Filter (crisis-filter.ts), R-LYRA-01 verpasste Krise/Suizidalität (S4, Top-Risiko) (+13 more)
### Community 80 - "Community 80" ### Community 80 - "Community 80"
Cohesion: 0.13 Cohesion: 0.11
Nodes (18): protectedDeviceIcon(), ProtectedDeviceRow(), confirmProtectedDeviceInstalled(), countActiveProtectedDevices(), createProtectedDevice(), DEVICE_SELECT, DEVICE_SELECT_WITH_TOKEN, getDeviceBlocklistMode() (+10 more) Nodes (20): protectedDeviceIcon(), ProtectedDeviceRow(), listMagicDevices(), confirmProtectedDeviceInstalled(), countActiveProtectedDevices(), createProtectedDevice(), DEVICE_SELECT, DEVICE_SELECT_WITH_TOKEN (+12 more)
### Community 81 - "Community 81" ### Community 81 - "Community 81"
Cohesion: 0.14 Cohesion: 0.14
@ -1178,8 +1171,8 @@ Cohesion: 0.25
Nodes (26): buildFiles, buildTargetsCommandComponents, cleanCommandsComponents, abi, artifactName, output, runtimeFiles, libraries (+18 more) Nodes (26): buildFiles, buildTargetsCommandComponents, cleanCommandsComponents, abi, artifactName, output, runtimeFiles, libraries (+18 more)
### Community 93 - "Community 93" ### Community 93 - "Community 93"
Cohesion: 0.12 Cohesion: 0.14
Nodes (24): R-FALSE-01 falsche Sicherheit bei inaktivem Schutz, useProtectionState(), UseProtectionStateReturn, BackendCooldownStatus, BackendProtectionState, CooldownState, formatCooldownRemaining(), isAllLayersOn() (+16 more) Nodes (23): REQ-PROT Schutz/Blocker (geräteweite Zugangserschwerung), R-FALSE-01 falsche Sicherheit bei inaktivem Schutz, useProtectionState(), UseProtectionStateReturn, BackendCooldownStatus, BackendProtectionState, CooldownState, formatCooldownRemaining() (+15 more)
### Community 94 - "Community 94" ### Community 94 - "Community 94"
Cohesion: 0.10 Cohesion: 0.10
@ -1187,7 +1180,7 @@ Nodes (25): buildType, serviceAccountKeyPath, track, build, development, preview
### Community 95 - "Community 95" ### Community 95 - "Community 95"
Cohesion: 0.11 Cohesion: 0.11
Nodes (19): callIdToUuid(), displayIncomingCall(), endCall(), reportConnected(), reportEnded(), setupCallKeep(), startOutgoingCall(), startRingback() (+11 more) Nodes (21): useCallKeepEvents(), useIncomingCalls(), callIdToUuid(), displayIncomingCall(), endCall(), reportConnected(), reportEnded(), setupCallKeep() (+13 more)
### Community 96 - "Community 96" ### Community 96 - "Community 96"
Cohesion: 0.08 Cohesion: 0.08
@ -1242,8 +1235,8 @@ Cohesion: 0.08
Nodes (24): backtraceGraph, commands, files, nodes, installers, paths, build, source (+16 more) Nodes (24): backtraceGraph, commands, files, nodes, installers, paths, build, source (+16 more)
### Community 109 - "Community 109" ### Community 109 - "Community 109"
Cohesion: 0.12 Cohesion: 0.14
Nodes (18): Bool, String, Bool, DeviceState, String, EnrollmentStatus, Equatable, DetectorError (+10 more) Nodes (14): Bool, DeviceState, String, Equatable, DetectorError, cfgutilMissing, deviceLocked, ideviceinfoMissing (+6 more)
### Community 110 - "Community 110" ### Community 110 - "Community 110"
Cohesion: 0.19 Cohesion: 0.19
@ -1303,7 +1296,7 @@ Nodes (21): String, Int, CodingKey, CodingKeys, bitCount, bits, falsePositiveTol
### Community 124 - "Community 124" ### Community 124 - "Community 124"
Cohesion: 0.13 Cohesion: 0.13
Nodes (18): AuthSession, Bool, Date, DeviceState, WizardStep, CaseIterable, advance(), DebugSupervisionMode (+10 more) Nodes (17): AuthSession, Bool, Date, DeviceState, WizardStep, advance(), DebugSupervisionMode, forceSupervised (+9 more)
### Community 125 - "Community 125" ### Community 125 - "Community 125"
Cohesion: 0.09 Cohesion: 0.09
@ -1430,8 +1423,8 @@ Cohesion: 0.09
Nodes (22): artifacts, backtrace, backtraceGraph, commands, files, nodes, compileGroups, dependencies (+14 more) Nodes (22): artifacts, backtrace, backtraceGraph, commands, files, nodes, compileGroups, dependencies (+14 more)
### Community 156 - "Community 156" ### Community 156 - "Community 156"
Cohesion: 0.07 Cohesion: 0.09
Nodes (25): Option, Props, WheelPickerModal(), Plan, useUserPlan(), GERMAN_CITIES_BY_BUNDESLAND, getCitiesForBundesland(), BIRTH_YEAR_OPTIONS (+17 more) Nodes (19): Option, Props, WheelPickerModal(), GERMAN_CITIES_BY_BUNDESLAND, getCitiesForBundesland(), BIRTH_YEAR_OPTIONS, BUNDESLAND_OPTIONS, DemographicsAccordion() (+11 more)
### Community 157 - "Community 157" ### Community 157 - "Community 157"
Cohesion: 0.11 Cohesion: 0.11
@ -1530,8 +1523,8 @@ Cohesion: 0.18
Nodes (10): RebreakAccessibilityService, requestPowerDialog(), AccessibilityEvent, AccessibilityNodeInfo, AccessibilityService, Boolean, Int, Long (+2 more) Nodes (10): RebreakAccessibilityService, requestPowerDialog(), AccessibilityEvent, AccessibilityNodeInfo, AccessibilityService, Boolean, Int, Long (+2 more)
### Community 181 - "Community 181" ### Community 181 - "Community 181"
Cohesion: 0.16 Cohesion: 0.09
Nodes (10): DmConversation, ALLOWED_EMOJIS, countUnreadDms(), getChatMessages(), getDmConversations(), getDmHistory(), markDmsAsRead(), sendDirectMessage() (+2 more) Nodes (18): DmConversation, ALLOWED_EMOJIS, EmptyState(), Props, countUnreadDms(), createChatMessage(), getChatMessages(), getDmConversations() (+10 more)
### Community 182 - "Community 182" ### Community 182 - "Community 182"
Cohesion: 0.32 Cohesion: 0.32
@ -1838,12 +1831,12 @@ Cohesion: 0.16
Nodes (14): APPROVAL_SELECT, approveRequest(), createApprovalRequest(), DeviceApprovalRecord, generateCode(), generateEmailToken(), getApprovalByEmailToken(), getApprovalRequest() (+6 more) Nodes (14): APPROVAL_SELECT, approveRequest(), createApprovalRequest(), DeviceApprovalRecord, generateCode(), generateEmailToken(), getApprovalByEmailToken(), getApprovalRequest() (+6 more)
### Community 258 - "Community 258" ### Community 258 - "Community 258"
Cohesion: 0.10 Cohesion: 0.19
Nodes (13): CrisisLevel, SseEvents, streamSosLyra(), BenchMarker, BenchOnMetric, BenchSession, MarkerEntry, cleanForTts() (+5 more) Nodes (6): cleanForTts(), QueueItem, SosTtsFetchOpts, SosTtsMode, SosTtsQueue, SosTtsQueueOpts
### Community 259 - "Community 259" ### Community 259 - "Community 259"
Cohesion: 0.19 Cohesion: 0.18
Nodes (18): FileProvider, FormatBackupDate(), RenderInfoPlist(), RenderManifestPlist(), RenderStatusPlist(), renderTemplate(), TLManifestDB(), TLManifestPlist() (+10 more) Nodes (19): FileProvider, FormatBackupDate(), RenderInfoPlist(), RenderManifestPlist(), RenderStatusPlist(), renderTemplate(), TLManifestDB(), TLManifestPlist() (+11 more)
### Community 260 - "Community 260" ### Community 260 - "Community 260"
Cohesion: 0.13 Cohesion: 0.13
@ -1855,23 +1848,23 @@ Nodes (20): artifacts, backtrace, backtraceGraph, commands, files, nodes, compil
### Community 262 - "Community 262" ### Community 262 - "Community 262"
Cohesion: 0.18 Cohesion: 0.18
Nodes (9): Any, Bool, Int, String, URL, SharedLogStore, ModuleDefinition, NEVPNStatus (+1 more) Nodes (8): Any, Bool, Int, String, URL, SharedLogStore, ModuleDefinition, NEVPNStatus
### Community 263 - "Community 263" ### Community 263 - "Community 263"
Cohesion: 0.19 Cohesion: 0.19
Nodes (14): blocklist.bin Sync Mechanism, Brand-Token (curated brand core), Brand-Token-Matching (shared system), GET /api/url-filter/brand-tokens Endpoint, brand_tokens DB Table, BrandTokenList class (Swift, pendant to HashList), Klartext Brand-Token Privacy Decision, DnsFilter.classify Integration Point (+6 more) Nodes (14): blocklist.bin Sync Mechanism, Brand-Token (curated brand core), Brand-Token-Matching (shared system), GET /api/url-filter/brand-tokens Endpoint, brand_tokens DB Table, BrandTokenList class (Swift, pendant to HashList), Klartext Brand-Token Privacy Decision, DnsFilter.classify Integration Point (+6 more)
### Community 264 - "Community 264" ### Community 264 - "Community 264"
Cohesion: 0.16 Cohesion: 0.17
Nodes (16): _cooldown_ok(), extract_client_id(), main(), _mark_fired(), post_handshake(), # NOTE: field name is "CP" in AdGuard Home's querylog JSON serialization, Parse one NDJSON line from querylog.json. Returns the ClientID string if non, Tails a file line-by-line. Detects log rotation by monitoring inode. On rota (+8 more) Nodes (15): _cooldown_ok(), extract_client_id(), main(), _mark_fired(), post_handshake(), # NOTE: field name is "CP" in AdGuard Home's querylog JSON serialization, Parse one NDJSON line from querylog.json. Returns the ClientID string if non, Tails a file line-by-line. Detects log rotation by monitoring inode. On rota (+7 more)
### Community 265 - "Community 265" ### Community 265 - "Community 265"
Cohesion: 0.16 Cohesion: 0.16
Nodes (8): deviceConnReadWriter, envBool(), Open(), plistUnmarshal(), Conn, DeviceConnectionInterface, DeviceEntry, Client Nodes (8): deviceConnReadWriter, envBool(), Open(), plistUnmarshal(), Conn, DeviceConnectionInterface, DeviceEntry, Client
### Community 266 - "Community 266" ### Community 266 - "Community 266"
Cohesion: 0.33 Cohesion: 0.22
Nodes (10): Connect(), WaitForReconnect(), Duration, Conn, backupCurrentConfig(), makeLogger(), Supervise(), superviseEscalated() (+2 more) Nodes (17): Connect(), Conn, cliOpts, backupCurrentConfig(), makeLogger(), superviseEscalated(), superviseFresh(), Unsupervise() (+9 more)
### Community 267 - "Community 267" ### Community 267 - "Community 267"
Cohesion: 0.20 Cohesion: 0.20
@ -1882,8 +1875,8 @@ Cohesion: 0.20
Nodes (8): Bool, Int, HashList, Snapshot, String, UInt64, UnsafeRawPointer, URL Nodes (8): Bool, Int, HashList, Snapshot, String, UInt64, UnsafeRawPointer, URL
### Community 269 - "Community 269" ### Community 269 - "Community 269"
Cohesion: 0.15 Cohesion: 0.18
Nodes (14): Lang, LANG_NAME, LANGS, LYRA_TOPICS, LyraTopic, TOPIC_HINTS, postFromCatalog(), postFromLLM() (+6 more) Nodes (13): Country-Curated Layer-2 Blocklist (50-cap), Pro=10/Legend=20 Custom-Domain Slots (refillable), Layer1/Layer2 Decoupling (Custom vs Country-Curated), Layer-2 Country-Pivot Plan, Travel-Detection via Cellular-MCC, IMAP IDLE Real-Time Mail-Scan Daemon, rebreak-imap-idle Daemon README, Mail Daemon Deployment Handoff (+5 more)
### Community 270 - "Community 270" ### Community 270 - "Community 270"
Cohesion: 0.11 Cohesion: 0.11
@ -1910,8 +1903,8 @@ Cohesion: 0.11
Nodes (17): archive, artifacts, backtrace, backtraceGraph, commands, files, nodes, compileGroups (+9 more) Nodes (17): archive, artifacts, backtrace, backtraceGraph, commands, files, nodes, compileGroups (+9 more)
### Community 276 - "Community 276" ### Community 276 - "Community 276"
Cohesion: 0.15 Cohesion: 0.24
Nodes (16): MagicRemovalCredential, AppPlan, VALID_PLANS, reconcileMailAccounts(), reconcileProtectedDevices(), reconcileUpgrade(), runDowngradeReconciliation(), triggerFinalScanForConnection() (+8 more) Nodes (10): NanoMDM Server (mdm.rebreak.org), supervise-magic Go-CLI Binary, ReBreak Magic Mac App, AuthService.swift (Supabase + Keychain), ReBreak Magic Mac Phase 2 Summary, rebreak-magic-mac XcodeGen project.yml, ReBreak Magic Mac README, Rebreak Magic Mac (Self-Bind Wizard) (+2 more)
### Community 277 - "Community 277" ### Community 277 - "Community 277"
Cohesion: 0.15 Cohesion: 0.15
@ -1922,8 +1915,8 @@ Cohesion: 0.20
Nodes (9): Bool, ConfigurationModel, ErrorDetails, TimeInterval, View, ContentViewModel, ActivationButtonAction, disableFilter (+1 more) Nodes (9): Bool, ConfigurationModel, ErrorDetails, TimeInterval, View, ContentViewModel, ActivationButtonAction, disableFilter (+1 more)
### Community 279 - "Community 279" ### Community 279 - "Community 279"
Cohesion: 0.20 Cohesion: 0.13
Nodes (9): build, beforeBuildCommand, beforeDevCommand, devUrl, frontendDist, identifier, productName, $schema (+1 more) Nodes (14): build, beforeBuildCommand, beforeDevCommand, devUrl, frontendDist, bundle, active, externalBin (+6 more)
### Community 280 - "Community 280" ### Community 280 - "Community 280"
Cohesion: 0.11 Cohesion: 0.11
@ -1950,24 +1943,24 @@ Cohesion: 0.29
Nodes (9): Bool, Date, Int, String, EnrollmentStatus, MDMStatus, MDMStatusError, parseError (+1 more) Nodes (9): Bool, Date, Int, String, EnrollmentStatus, MDMStatus, MDMStatusError, parseError (+1 more)
### Community 286 - "Community 286" ### Community 286 - "Community 286"
Cohesion: 0.08 Cohesion: 0.06
Nodes (29): CoachScreen(), formatDuration(), LoadingPulse(), MessageWithMeta, styles, ThinkingDots(), formatVoiceDuration(), Props (+21 more) Nodes (36): CoachScreen(), formatDuration(), formatTimestamp(), LoadingPulse(), MessageRow(), MessageWithMeta, styles, ThinkingDots() (+28 more)
### Community 287 - "Community 287" ### Community 287 - "Community 287"
Cohesion: 0.22 Cohesion: 0.22
Nodes (8): Bool, Int, Never, String, Task, Void, EnrollView, TransferAnimationView Nodes (8): Bool, Int, Never, String, Task, Void, EnrollView, TransferAnimationView
### Community 288 - "Community 288" ### Community 288 - "Community 288"
Cohesion: 0.18 Cohesion: 0.17
Nodes (13): Date, Double, Error, Void, DispatchWorkItem, HealthProbeDelegate, Outcome, blocked (+5 more) Nodes (14): Date, Double, Error, Void, DispatchWorkItem, HealthProbeDelegate, Outcome, blocked (+6 more)
### Community 289 - "Community 289" ### Community 289 - "Community 289"
Cohesion: 0.08 Cohesion: 0.13
Nodes (32): Country-Curated Layer-2 Blocklist (50-cap), Pro=10/Legend=20 Custom-Domain Slots (refillable), Layer1/Layer2 Decoupling (Custom vs Country-Curated), Layer-2 Country-Pivot Plan, Travel-Detection via Cellular-MCC, Rive Animator Brief — Lyra Avatar, Lyra Avatar Rive Emotion-State Animation, Timeline-Name Code-Contract (no-rename) (+24 more) Nodes (17): Microsoft as Sub-Processor (Art. 28), Privacy-Policy DSB Notes, No separate Consent-UI for Lyra LLM transfer, Stufe-2 Lyra PII-Pseudonymization (Q3 2026), 12 Sub-Processor DPA/TIA Status, rebreak-native ~1.3% A11y Coverage Gap, Accessibility Audit & DiGA-Roadmap, WCAG 2.1 AA / BITV Compliance for DiGA (+9 more)
### Community 290 - "Community 290" ### Community 290 - "Community 290"
Cohesion: 0.17 Cohesion: 0.20
Nodes (6): Bool, Configuration, String, ConfigurationError, badConfiguration, ConfigurationModel Nodes (4): Bool, Configuration, String, ConfigurationModel
### Community 291 - "Community 291" ### Community 291 - "Community 291"
Cohesion: 0.12 Cohesion: 0.12
@ -2350,8 +2343,8 @@ Cohesion: 0.19
Nodes (9): prismaMock, banUserFromModerationItem(), deleteModerationItem(), dismissModerationItem(), listModerationQueue(), ListQueueOpts, ModerationItemType, ModerationQueueItem (+1 more) Nodes (9): prismaMock, banUserFromModerationItem(), deleteModerationItem(), dismissModerationItem(), listModerationQueue(), ListQueueOpts, ModerationItemType, ModerationQueueItem (+1 more)
### Community 386 - "Community 386" ### Community 386 - "Community 386"
Cohesion: 0.15 Cohesion: 0.20
Nodes (16): POST /api/magic/register Endpoint, GET /api/magic/status Endpoint, rebreak-staging Backend (Nitro) Service, ReBreak Magic Mac App, AuthService.swift (Supabase + Keychain), ReBreak Magic Mac Phase 2 Summary, MacProfileInstaller.swift (DNS-Filter Profile), ReBreak Magic Mac README (+8 more) Nodes (11): POST /api/magic/register Endpoint, GET /api/magic/status Endpoint, MacProfileInstaller.swift (DNS-Filter Profile), ReBreak Magic Windows App (Tauri 2), Eigene DoH-Infra (dns.rebreak.org, AdGuard), ReBreak Magic Windows index.html, ReBreak Magic Windows README, rebreak-protection-service (Windows SYSTEM Tamper-Service) (+3 more)
### Community 387 - "Community 387" ### Community 387 - "Community 387"
Cohesion: 0.14 Cohesion: 0.14
@ -2458,16 +2451,16 @@ Cohesion: 0.32
Nodes (13): confirm(), die(), dim(), err(), info(), log(), ok(), run() (+5 more) Nodes (13): confirm(), die(), dim(), err(), info(), log(), ok(), run() (+5 more)
### Community 413 - "Community 413" ### Community 413 - "Community 413"
Cohesion: 0.09 Cohesion: 0.08
Nodes (23): coach, crisis_bar_label, error, feedback_saved, input_placeholder, modeBadge, new_chat, online (+15 more) Nodes (25): welcomeBack, coach, crisis_bar_label, error, feedback_saved, input_placeholder, modeBadge, new_chat (+17 more)
### Community 414 - "Community 414" ### Community 414 - "Community 414"
Cohesion: 0.15 Cohesion: 0.15
Nodes (12): Configuration, String, CustomDebugStringConvertible, FilterStatus, disabled, invalid, running, starting (+4 more) Nodes (12): Configuration, String, CustomDebugStringConvertible, FilterStatus, disabled, invalid, running, starting (+4 more)
### Community 415 - "Community 415" ### Community 415 - "Community 415"
Cohesion: 0.14 Cohesion: 0.24
Nodes (17): Client, EscalateUnsupervised(), Open(), Certificate, DeviceConnectionInterface, DeviceEntry, PlistCodec, cliOpts (+9 more) Nodes (8): Client, EscalateUnsupervised(), Open(), Certificate, DeviceConnectionInterface, DeviceEntry, PlistCodec, Supervise()
### Community 416 - "Community 416" ### Community 416 - "Community 416"
Cohesion: 0.31 Cohesion: 0.31
@ -2478,8 +2471,8 @@ Cohesion: 0.13
Nodes (14): dependencies, @fontsource/nunito, react, react-dom, @tauri-apps/api, name, private, scripts (+6 more) Nodes (14): dependencies, @fontsource/nunito, react, react-dom, @tauri-apps/api, name, private, scripts (+6 more)
### Community 418 - "Community 418" ### Community 418 - "Community 418"
Cohesion: 0.16 Cohesion: 0.11
Nodes (11): Apple_SwiftHomomorphicEncryption_Pir_V1_KeywordDatabaseRow, Bool, Data, String, _2, _GeneratedWithProtocGenSwiftVersion, D, Sendable (+3 more) Nodes (18): Apple_SwiftHomomorphicEncryption_Pir_V1_KeywordDatabaseRow, String, URL, Bool, Data, String, RuntimeError, _2 (+10 more)
### Community 419 - "Community 419" ### Community 419 - "Community 419"
Cohesion: 0.11 Cohesion: 0.11
@ -2494,8 +2487,8 @@ Cohesion: 0.14
Nodes (12): String, String, Identifiable, Int, WizardStep, done, enroll, macRegistration (+4 more) Nodes (12): String, String, Identifiable, Int, WizardStep, done, enroll, macRegistration (+4 more)
### Community 422 - "Community 422" ### Community 422 - "Community 422"
Cohesion: 0.15 Cohesion: 0.28
Nodes (11): EASE_OUT, LandingScreen(), { width: SW, height: SH }, BrandSplash(), EASE_IN, EASE_OUT, { height: SH }, ParticleConfig (+3 more) Nodes (9): SAFETY-REQ-LLM-002 Sicherheits-Grenzen (Rollen-Integrität, Refusal), Rive Animator Brief — Lyra Avatar, Lyra Avatar Rive Emotion-State Animation, Timeline-Name Code-Contract (no-rename), Forbidden Vocabulary (no Sucht/addiction/Therapie), Lyra AI-Coach Persona (SOS + Coach modes), Lyra Persona — Single Source of Truth, Verbotenes Pathologisierungs-Vokabular (Sucht/süchtig/Therapie) (+1 more)
### Community 423 - "Community 423" ### Community 423 - "Community 423"
Cohesion: 0.14 Cohesion: 0.14
@ -2510,8 +2503,12 @@ Cohesion: 0.19
Nodes (14): Maestro Flow: blocker/vpn-activate-verify, Android 16 VPN FGS specialUse Type Fix, Android Surgical Tamper-Lock (a11y + device admin), Blocker / Protection Screen, iOS Guided 3-Step Protection Setup (FamilyControls + Screen Time + VPN URL-Filter), Protection Bypass Local 'was active here' Flag, RebreakVpnService (Android DNS-Filter FGS), 24h-Cooldown Server-Timed Deactivation (+6 more) Nodes (14): Maestro Flow: blocker/vpn-activate-verify, Android 16 VPN FGS specialUse Type Fix, Android Surgical Tamper-Lock (a11y + device admin), Blocker / Protection Screen, iOS Guided 3-Step Protection Setup (FamilyControls + Screen Time + VPN URL-Filter), Protection Bypass Local 'was active here' Flag, RebreakVpnService (Android DNS-Filter FGS), 24h-Cooldown Server-Timed Deactivation (+6 more)
### Community 426 - "Community 426" ### Community 426 - "Community 426"
Cohesion: 0.12 Cohesion: 0.20
Nodes (21): PermissionDeniedVariant, applock, prompt, signOut_body, signOut_title, unlock, family_controls_error, permission_denied (+13 more) Nodes (14): PermissionDeniedVariant, applock, prompt, signOut_body, signOut_title, unlock, family_controls_error, permission_denied (+6 more)
### Community 427 - "Community 427"
Cohesion: 0.20
Nodes (4): Conn, WaitForReconnect(), DeviceEntry, Duration
### Community 428 - "Community 428" ### Community 428 - "Community 428"
Cohesion: 0.14 Cohesion: 0.14
@ -2523,16 +2520,16 @@ Nodes (5): fs, MODULE_A11Y_XML, MODULE_DEVICE_ADMIN_XML, path, {
} }
### Community 429 - "Community 429" ### Community 429 - "Community 429"
Cohesion: 0.05 Cohesion: 0.09
Nodes (44): ResolveResult, resolveTypeAndValue(), TestResolveResult, listUserDevices(), addUserCustomDomain(), countActiveCustomDomains(), getUserCustomDomains(), countMailConnections() (+36 more) Nodes (30): speakCartesia(), speakElevenLabs(), speakGoogle(), resolveTypeAndValue(), TestResolveResult, consumeVoiceQuota(), estimateAudioSeconds(), getRemainingVoiceQuota() (+22 more)
### Community 430 - "Community 430" ### Community 430 - "Community 430"
Cohesion: 0.22 Cohesion: 0.33
Nodes (13): rebreak Admin App (Nuxt SSR panel), admin_audit_log DSGVO Audit-Log (Hans-Mueller), requireAdmin Middleware (admin_users JWT check), GitHub Actions Artifact-Deploy Pattern, imap-idle Daemon, rebreak-admin-staging Service, Build admin (Nuxt SSR) Job, Deploy admin to Hetzner Job (+5 more) Nodes (7): rebreak Admin App (Nuxt SSR panel), admin_audit_log DSGVO Audit-Log (Hans-Mueller), requireAdmin Middleware (admin_users JWT check), rebreak-admin-staging Service, Build admin (Nuxt SSR) Job, Deploy admin to Hetzner Job, Deploy Admin Staging Workflow
### Community 431 - "Community 431" ### Community 431 - "Community 431"
Cohesion: 0.06 Cohesion: 0.08
Nodes (25): prismaMock, EmptyState(), Props, adminApproveSubmission(), adminRejectSubmission(), castDomainVote(), CustomDomainType, deleteUserCustomDomain() (+17 more) Nodes (18): prismaMock, ResolveResult, addUserCustomDomain(), adminApproveSubmission(), adminRejectSubmission(), countActiveCustomDomains(), CustomDomainType, deleteUserCustomDomain() (+10 more)
### Community 432 - "Community 432" ### Community 432 - "Community 432"
Cohesion: 0.22 Cohesion: 0.22
@ -2555,8 +2552,8 @@ Cohesion: 0.24
Nodes (10): args, dryRun, fetchFromDb(), fetchFromHagezi(), formatTxtpb(), main(), normalizeDomain(), outputPath (+2 more) Nodes (10): args, dryRun, fetchFromDb(), fetchFromHagezi(), formatTxtpb(), main(), normalizeDomain(), outputPath (+2 more)
### Community 437 - "Community 437" ### Community 437 - "Community 437"
Cohesion: 0.11 Cohesion: 0.13
Nodes (22): makeStyles(), SOSScreen(), ChipSpec, detectEmotion(), LyraEmotion, parseLyraResponse(), SOS_BOOT, currentProvider() (+14 more) Nodes (19): makeStyles(), SOSScreen(), ChipSpec, detectEmotion(), LyraEmotion, parseLyraResponse(), BreathPhase, BreathState (+11 more)
### Community 438 - "Community 438" ### Community 438 - "Community 438"
Cohesion: 0.24 Cohesion: 0.24
@ -2571,16 +2568,16 @@ Cohesion: 0.18
Nodes (10): _comment, DE, FR, GB, _meta, maxDomainsPerCountry, status, updatedAt (+2 more) Nodes (10): _comment, DE, FR, GB, _meta, maxDomainsPerCountry, status, updatedAt (+2 more)
### Community 441 - "Community 441" ### Community 441 - "Community 441"
Cohesion: 0.32 Cohesion: 0.40
Nodes (23): abi, artifactName, output, runtimeFiles, libraries, appmodules::@6890427a1f51a3e7e1df, core::@1b9a7d546b295b7d0867, react_codegen_lottiereactnative::@0fa4dc904d7e359a99fb (+15 more) Nodes (5): GameRatingStars(), sizeMap, StarRating(), StarRatingProps, StarSize
### Community 442 - "Community 442" ### Community 442 - "Community 442"
Cohesion: 0.18 Cohesion: 0.18
Nodes (11): error, common, back, cancel, confirm, continue, loading, ok (+3 more) Nodes (11): error, common, back, cancel, confirm, continue, loading, ok (+3 more)
### Community 443 - "Community 443" ### Community 443 - "Community 443"
Cohesion: 0.20 Cohesion: 0.15
Nodes (9): App, Scene, Scene, View, SimpleURLFilterApp, RebreakMagicApp, app, security (+1 more) Nodes (12): App, Scene, Scene, View, SimpleURLFilterApp, RebreakMagicApp, app, security (+4 more)
### Community 444 - "Community 444" ### Community 444 - "Community 444"
Cohesion: 0.20 Cohesion: 0.20
@ -2603,8 +2600,8 @@ Cohesion: 0.20
Nodes (9): DE, FR, GB, _meta, maxDomainsPerCountry, status, updatedAt, version (+1 more) Nodes (9): DE, FR, GB, _meta, maxDomainsPerCountry, status, updatedAt, version (+1 more)
### Community 449 - "Community 449" ### Community 449 - "Community 449"
Cohesion: 0.18 Cohesion: 0.22
Nodes (12): ReBreak Magic RE-Hardening Assessment & Plan, Admin-User kann alles aufheben (echter Schutz = Cooldown+Psychologie), Server-seitige Token-Revocation (Kern-Sicherheit im Backend), DNS-Token DPAPI-Verschlüsselung (Windows), Windows protection.json ACL-Härtung (DNS-Token-Schutz), Windows Service-Name-Bypass (sc.exe stop RebreakProtection), Session-Handoff 2026-06-07/08 (Magic-Windows E2E), DNS-Token 64-Zeichen-Bug (AdGuard hostname label too long) (+4 more) Nodes (10): ReBreak Magic RE-Hardening Assessment & Plan, Admin-User kann alles aufheben (echter Schutz = Cooldown+Psychologie), Server-seitige Token-Revocation (Kern-Sicherheit im Backend), DNS-Token DPAPI-Verschlüsselung (Windows), Windows protection.json ACL-Härtung (DNS-Token-Schutz), Windows Service-Name-Bypass (sc.exe stop RebreakProtection), DNS-Token 64-Zeichen-Bug (AdGuard hostname label too long), ReBreak Magic Windows E2E-Flow (Download→Pair→Register→AdGuard-DNS) (+2 more)
### Community 450 - "Community 450" ### Community 450 - "Community 450"
Cohesion: 0.36 Cohesion: 0.36
@ -3071,16 +3068,16 @@ Cohesion: 0.22
Nodes (8): backtraceGraph, commands, files, nodes, installers, paths, build, source Nodes (8): backtraceGraph, commands, files, nodes, installers, paths, build, source
### Community 566 - "Community 566" ### Community 566 - "Community 566"
Cohesion: 0.12 Cohesion: 0.11
Nodes (16): CallRingPushPayload, ChatPushPayload, DeviceAddedPushPayload, devicePlatformLabel(), expo, getDisplayName(), sendCallRingPush(), sendChatPush() (+8 more) Nodes (18): CallRingPushPayload, ChatPushPayload, DeviceAddedPushPayload, devicePlatformLabel(), ensureExpo(), ExpoInstance, ExpoModule, ExpoPushMessage (+10 more)
### Community 567 - "Community 567" ### Community 567 - "Community 567"
Cohesion: 0.22 Cohesion: 0.22
Nodes (7): AppAlert(), AppAlertMode, AppAlertProps, BaseProps, ConfirmProps, ErrorProps, SuccessProps Nodes (7): AppAlert(), AppAlertMode, AppAlertProps, BaseProps, ConfirmProps, ErrorProps, SuccessProps
### Community 568 - "Community 568" ### Community 568 - "Community 568"
Cohesion: 0.14 Cohesion: 0.40
Nodes (13): formatTimestamp(), MessageRow(), GameCard(), GameCardProps, Props, st, CardType, GameHeader() (+5 more) Nodes (3): Props, st, GamePickerGrid()
### Community 569 - "Community 569" ### Community 569 - "Community 569"
Cohesion: 0.42 Cohesion: 0.42
@ -3207,8 +3204,8 @@ Cohesion: 0.29
Nodes (8): Maestro Profile Demographics Flow, Maestro Profile View-and-Edit Flow, Maestro View-Profile Flow, Maestro E2E Test Setup Guide, Maestro Selector Strategy (testID > text > coordinates), Maestro TestID TODO Backlog, Maestro SOS Urge Flow (Lyra chat + breathing), Maestro Start-Session Urge Flow Nodes (8): Maestro Profile Demographics Flow, Maestro Profile View-and-Edit Flow, Maestro View-Profile Flow, Maestro E2E Test Setup Guide, Maestro Selector Strategy (testID > text > coordinates), Maestro TestID TODO Backlog, Maestro SOS Urge Flow (Lyra chat + breathing), Maestro Start-Session Urge Flow
### Community 600 - "Community 600" ### Community 600 - "Community 600"
Cohesion: 0.21 Cohesion: 0.36
Nodes (13): Outlook OAuth DSGVO Review Memo, MS lacks 3rd-party token-revoke endpoint, OAuth preferred over App-Password (DSGVO datamin), Microsoft Basic-Auth Deprecation (Sep 2024), BFF OAuth Token-Exchange Pattern, Outlook OAuth2 Implementation Plan, Microsoft Outlook OAuth2 Mail Integration, ImapFlow XOAUTH2 Auth (+5 more) Nodes (8): Outlook OAuth DSGVO Review Memo, MS lacks 3rd-party token-revoke endpoint, OAuth preferred over App-Password (DSGVO datamin), Microsoft Basic-Auth Deprecation (Sep 2024), BFF OAuth Token-Exchange Pattern, Outlook OAuth2 Implementation Plan, Microsoft Outlook OAuth2 Mail Integration, ImapFlow XOAUTH2 Auth
### Community 601 - "Community 601" ### Community 601 - "Community 601"
Cohesion: 0.25 Cohesion: 0.25
@ -3219,16 +3216,16 @@ Cohesion: 0.43
Nodes (8): family_controls_error, permission_denied, title, fallback_body, fallback_label, retry_cta, retry_loading, settings_cta Nodes (8): family_controls_error, permission_denied, title, fallback_body, fallback_label, retry_cta, retry_loading, settings_cta
### Community 603 - "Community 603" ### Community 603 - "Community 603"
Cohesion: 0.12 Cohesion: 0.50
Nodes (16): compilerOptions, allowSyntheticDefaultImports, baseUrl, forceConsistentCasingInFileNames, lib, module, moduleResolution, noEmit (+8 more) Nodes (4): Binding, Bool, String, PreflightView
### Community 604 - "Community 604" ### Community 604 - "Community 604"
Cohesion: 0.43 Cohesion: 0.43
Nodes (8): family_controls_error, permission_denied, title, fallback_body, fallback_label, retry_cta, retry_loading, settings_cta Nodes (8): family_controls_error, permission_denied, title, fallback_body, fallback_label, retry_cta, retry_loading, settings_cta
### Community 605 - "Community 605" ### Community 605 - "Community 605"
Cohesion: 0.18 Cohesion: 0.60
Nodes (12): CallNoteRow(), ChatBubble(), fmtSec(), MessageReaction, Props, useBubbleColors(), VoiceNoteBubble(), AnchorRect (+4 more) Nodes (5): Maestro Flow: calls/incoming-call-screen, CallScreen (/call, useCallStore), PKPushRegistry Local-Var Dealloc Bug (Build 76), iOS VoIP PushKit + CallKit Incoming-Call Flow, Voice-Calls Debug State (2026-06-05)
### Community 606 - "Community 606" ### Community 606 - "Community 606"
Cohesion: 0.43 Cohesion: 0.43
@ -3515,12 +3512,12 @@ Cohesion: 0.50
Nodes (3): info, author, version Nodes (3): info, author, version
### Community 686 - "Community 686" ### Community 686 - "Community 686"
Cohesion: 0.50 Cohesion: 0.20
Nodes (3): nav, download_app, login Nodes (11): EMPTY_STATS, GamesScreen(), GameStat, GameStats, LastScore, GameCard(), GameCardProps, GAME_META (+3 more)
### Community 687 - "Community 687" ### Community 687 - "Community 687"
Cohesion: 0.50 Cohesion: 0.17
Nodes (3): nav, download_app, login Nodes (7): CrisisLevel, SseEvents, streamSosLyra(), BenchMarker, BenchOnMetric, BenchSession, MarkerEntry
### Community 688 - "Community 688" ### Community 688 - "Community 688"
Cohesion: 0.83 Cohesion: 0.83
@ -3539,8 +3536,8 @@ Cohesion: 0.50
Nodes (3): enabled, packages, prefabPath Nodes (3): enabled, packages, prefabPath
### Community 696 - "Community 696" ### Community 696 - "Community 696"
Cohesion: 1.00 Cohesion: 0.50
Nodes (3): WizardStep, Color, StepIndicator Nodes (5): InlineRatingDrawer(), s, s, SosFeedback, SosFeedbackModal()
### Community 704 - "Community 704" ### Community 704 - "Community 704"
Cohesion: 0.67 Cohesion: 0.67
@ -3555,67 +3552,47 @@ Cohesion: 1.00
Nodes (3): Deploy Pipeline (deploy.sh, atomic .output-staging swap, pm2), WEBHOOK_MIGRATION_PLAN.md (standalone→Nitro endpoint), GitHub Webhook as Nitro API Endpoint (Trucko pattern) Nodes (3): Deploy Pipeline (deploy.sh, atomic .output-staging swap, pm2), WEBHOOK_MIGRATION_PLAN.md (standalone→Nitro endpoint), GitHub Webhook as Nitro API Endpoint (Trucko pattern)
### Community 863 - "Community 863" ### Community 863 - "Community 863"
Cohesion: 0.24 Cohesion: 0.50
Nodes (7): VALID_COUNTRIES, SupportedCountry, decideCuratedDomain(), getCuratedDomains(), suggestCuratedDomain(), PUBLIC_EMAIL_DOMAINS, isPublicEmailDomain() Nodes (3): nav, download_app, login
### Community 864 - "Community 864" ### Community 864 - "Community 864"
Cohesion: 0.22 Cohesion: 0.50
Nodes (9): BreathPhase, BreathState, Chip, CHIP_SETS, ChipSet, BreathingCard(), BreathingDrawer(), Props (+1 more) Nodes (4): Bool, String, EnrollmentStatus, DevicesState
### Community 865 - "Community 865" ### Community 865 - "Community 865"
Cohesion: 0.31 Cohesion: 0.25
Nodes (8): MailBlockedItem, MailResultsResponse, useMailResults(), ActivityItem(), domainFromEmail(), formatDate(), MailActivityLog(), Props Nodes (7): currentProvider(), endpointForProvider(), listeners, TTS_PROVIDER_ENDPOINT, TTS_PROVIDER_LABEL, TtsProvider, PROVIDERS
### Community 866 - "Community 866" ### Community 866 - "Community 866"
Cohesion: 0.28 Cohesion: 0.28
Nodes (7): currentLlmProvider(), listeners, LLM_PROVIDER_LABEL, LlmProvider, useLlmProvider(), LlmProviderToggle(), PROVIDERS Nodes (7): currentLlmProvider(), listeners, LLM_PROVIDER_LABEL, LlmProvider, useLlmProvider(), LlmProviderToggle(), PROVIDERS
### Community 867 - "Community 867" ### Community 867 - "Community 867"
Cohesion: 0.25 Cohesion: 0.67
Nodes (8): windows, bundle, active, externalBin, icon, targets, installMode, nsis Nodes (4): App-Preview Screenshots (/preview), capture-marketing.sh Maestro Pipeline, preview.vue Marketing Page, Maestro Flow: screens/01-onboarding (screenshot)
### Community 868 - "Community 868"
Cohesion: 0.29
Nodes (7): Maestro E2E Test-Schicht (Mobile App, ~20 Flows), Software-Verifikation Test-Nachweis v0 (Dok 05b, IEC 62304), Fehlende Traceability-Matrix (Tests↔Anforderungen↔Risiken), Vitest Unit-/Integrationstests (Backend, 21 Dateien), Maestro-Screenshot-Pipeline (marketing-tour-loggedin), Protection-Bypass-Fix everActiveHere-Flag, Session-Handoff 2026-06-08 Preview/Screenshots/Bug-Hunt
### Community 869 - "Community 869"
Cohesion: 0.40
Nodes (5): GameRatingStars(), sizeMap, StarRating(), StarRatingProps, StarSize
### Community 870 - "Community 870"
Cohesion: 0.40
Nodes (6): NanoMDM Server (mdm.rebreak.org), supervise-magic Go-CLI Binary, rebreak-magic-mac XcodeGen project.yml, Rebreak Magic Mac (Self-Bind Wizard), supervise-magic (Supervise without Reset), Mac+iPhone Self-Binding 6-Step Wizard
### Community 871 - "Community 871"
Cohesion: 0.60
Nodes (5): Maestro Flow: calls/incoming-call-screen, CallScreen (/call, useCallStore), PKPushRegistry Local-Var Dealloc Bug (Build 76), iOS VoIP PushKit + CallKit Incoming-Call Flow, Voice-Calls Debug State (2026-06-05)
### Community 872 - "Community 872" ### Community 872 - "Community 872"
Cohesion: 1.00 Cohesion: 1.00
Nodes (3): getBestScore(), key(), saveBestScore() Nodes (3): getBestScore(), key(), saveBestScore()
### Community 873 - "Community 873"
Cohesion: 0.67
Nodes (4): App-Preview Screenshots (/preview), capture-marketing.sh Maestro Pipeline, preview.vue Marketing Page, Maestro Flow: screens/01-onboarding (screenshot)
## Knowledge Gaps ## Knowledge Gaps
- **8557 isolated node(s):** `graphify (Knowledge-Graph)`, `bundleIdentifier`, `name`, `private`, `version` (+8552 more) - **8560 isolated node(s):** `RECONNECT_DELAYS_MS`, `pool`, `MS_OAUTH_SCOPES`, `sessions`, `scanInFlight` (+8555 more)
These have ≤1 connection - possible missing edges or undocumented components. These have ≤1 connection - possible missing edges or undocumented components.
- **49 thin communities (<3 nodes) omitted from report** — run `graphify query` to explore isolated nodes. - **46 thin communities (<3 nodes) omitted from report** — run `graphify query` to explore isolated nodes.
## Suggested Questions ## Suggested Questions
_Questions this graph is uniquely positioned to answer:_ _Questions this graph is uniquely positioned to answer:_
- **Why does `useColors()` connect `i18n: Pricing Strings` to `Backend API Routes`, `Consent & Magic API Routes`, `App Root Layout & Shell`, `Community 18`, `Community 20`, `Community 21`, `Community 22`, `Community 27`, `Community 156`, `Community 29`, `Community 286`, `Community 426`, `Community 47`, `Community 431`, `Community 49`, `Community 53`, `Community 437`, `Community 568`, `Community 80`, `Community 605`, `Community 864`, `Community 865`?** - **Why does `useColors()` connect `i18n: Pricing Strings` to `Backend API Routes`, `Consent & Magic API Routes`, `App Root Layout & Shell`, `Community 18`, `Community 20`, `Community 21`, `Community 22`, `Community 27`, `Community 156`, `Community 29`, `Community 286`, `Community 426`, `Community 686`, `Community 47`, `Community 49`, `Community 53`, `Community 437`, `Community 181`, `Community 696`, `Community 80`?**
_High betweenness centrality (0.046) - this node is a cross-community bridge._ _High betweenness centrality (0.042) - this node is a cross-community bridge._
- **Why does `usePrisma()` connect `Debug & Dev Tools` to `Community 32`, `Community 384`, `Community 257`, `Community 385`, `Community 34`, `Community 68`, `Backend Tests & Auth Routes`, `Android DNS Filter (Kotlin)`, `Community 269`, `Community 429`, `Community 431`, `Community 80`, `Community 48`, `Community 276`, `Community 181`, `Community 566`, `Community 62`, `Community 863`?** - **Why does `blocker` connect `i18n: Blocker/Activation Strings` to `Community 389`, `Community 69`, `i18n: Pricing Strings`, `Community 426`, `Community 47`, `Community 49`, `Community 22`?**
_High betweenness centrality (0.029) - this node is a cross-community bridge._ _High betweenness centrality (0.028) - this node is a cross-community bridge._
- **Why does `blocker` connect `i18n: Blocker/Activation Strings` to `Community 389`, `Community 69`, `i18n: Pricing Strings`, `Community 426`, `Community 47`, `App Root Layout & Shell`, `Community 49`, `Community 22`?** - **Why does `usePrisma()` connect `Debug & Dev Tools` to `Community 32`, `Community 384`, `Community 34`, `Community 257`, `Community 385`, `Community 68`, `Backend Tests & Auth Routes`, `Community 39`, `Android DNS Filter (Kotlin)`, `Community 429`, `Community 431`, `Community 80`, `Community 48`, `Community 181`, `Community 566`, `Community 62`?**
_High betweenness centrality (0.029) - this node is a cross-community bridge._ _High betweenness centrality (0.023) - this node is a cross-community bridge._
- **Are the 2 inferred relationships involving `usePrisma()` (e.g. with `runRevoke()` and `requireUser()`) actually correct?** - **Are the 2 inferred relationships involving `usePrisma()` (e.g. with `runRevoke()` and `requireUser()`) actually correct?**
_`usePrisma()` has 2 INFERRED edges - model-reasoned connections that need verification._ _`usePrisma()` has 2 INFERRED edges - model-reasoned connections that need verification._
- **What connects `graphify (Knowledge-Graph)`, `bundleIdentifier`, `name` to the rest of the system?** - **What connects `RECONNECT_DELAYS_MS`, `pool`, `MS_OAUTH_SCOPES` to the rest of the system?**
_8601 weakly-connected nodes found - possible documentation gaps or missing edges._ _8604 weakly-connected nodes found - possible documentation gaps or missing edges._
- **Should `i18n: Blocker/Activation Strings` be split into smaller, more focused modules?** - **Should `i18n: Blocker/Activation Strings` be split into smaller, more focused modules?**
_Cohesion score 0.006825938566552901 - nodes in this community are weakly interconnected._ _Cohesion score 0.006825938566552901 - nodes in this community are weakly interconnected._
- **Should `i18n: Blocker/Activation Strings` be split into smaller, more focused modules?** - **Should `i18n: Blocker/Activation Strings` be split into smaller, more focused modules?**

File diff suppressed because it is too large Load Diff

View File

@ -25,9 +25,9 @@
"semantic_hash": "e5f6eb13c875f326cfd5915d3364614f" "semantic_hash": "e5f6eb13c875f326cfd5915d3364614f"
}, },
"/Users/chahinebrini/mono/rebreak-monorepo/backend/nitro.config.ts": { "/Users/chahinebrini/mono/rebreak-monorepo/backend/nitro.config.ts": {
"mtime": 1780651100.3393717, "mtime": 1781098496.7526796,
"ast_hash": "5337f6dc128a2e0f99758c66097a22e3", "ast_hash": "00fe0ab55b634aca00786cbfba7697ea",
"semantic_hash": "5337f6dc128a2e0f99758c66097a22e3" "semantic_hash": ""
}, },
"/Users/chahinebrini/mono/rebreak-monorepo/backend/prisma.config.ts": { "/Users/chahinebrini/mono/rebreak-monorepo/backend/prisma.config.ts": {
"mtime": 1778033276.136318, "mtime": 1778033276.136318,
@ -1120,9 +1120,9 @@
"semantic_hash": "dc170d505aedde45d648baa6aacd7e8d" "semantic_hash": "dc170d505aedde45d648baa6aacd7e8d"
}, },
"/Users/chahinebrini/mono/rebreak-monorepo/backend/server/api/mail/scan-internal.post.ts": { "/Users/chahinebrini/mono/rebreak-monorepo/backend/server/api/mail/scan-internal.post.ts": {
"mtime": 1780772000.0296645, "mtime": 1781092509.8649359,
"ast_hash": "4196a86701ac61dee101e35944af5fae", "ast_hash": "fde632a8537b8a013f39fb91f69e1ac4",
"semantic_hash": "4196a86701ac61dee101e35944af5fae" "semantic_hash": ""
}, },
"/Users/chahinebrini/mono/rebreak-monorepo/backend/server/api/mail/proxy-config.get.ts": { "/Users/chahinebrini/mono/rebreak-monorepo/backend/server/api/mail/proxy-config.get.ts": {
"mtime": 1778032995.0984364, "mtime": 1778032995.0984364,
@ -1150,9 +1150,9 @@
"semantic_hash": "5a819c76f2b7cf9747798bdcd07992d5" "semantic_hash": "5a819c76f2b7cf9747798bdcd07992d5"
}, },
"/Users/chahinebrini/mono/rebreak-monorepo/backend/server/api/mail/scan.post.ts": { "/Users/chahinebrini/mono/rebreak-monorepo/backend/server/api/mail/scan.post.ts": {
"mtime": 1779973242.1609433, "mtime": 1781092587.402889,
"ast_hash": "5cad50581294fcf372fad8752269ebe6", "ast_hash": "a5fda386477f7e66e25526698ef9d199",
"semantic_hash": "5cad50581294fcf372fad8752269ebe6" "semantic_hash": ""
}, },
"/Users/chahinebrini/mono/rebreak-monorepo/backend/server/api/mail/results.get.ts": { "/Users/chahinebrini/mono/rebreak-monorepo/backend/server/api/mail/results.get.ts": {
"mtime": 1778683260.5017214, "mtime": 1778683260.5017214,
@ -1920,9 +1920,9 @@
"semantic_hash": "55af2637a5b3c77f7fb7c8163cc201f7" "semantic_hash": "55af2637a5b3c77f7fb7c8163cc201f7"
}, },
"/Users/chahinebrini/mono/rebreak-monorepo/backend/server/services/push.ts": { "/Users/chahinebrini/mono/rebreak-monorepo/backend/server/services/push.ts": {
"mtime": 1780861075.5283923, "mtime": 1781092766.6544745,
"ast_hash": "35d56dcaab3e786304ab73b79abb3cea", "ast_hash": "e25f9ea7353380d1c73de789a0e127a2",
"semantic_hash": "35d56dcaab3e786304ab73b79abb3cea" "semantic_hash": ""
}, },
"/Users/chahinebrini/mono/rebreak-monorepo/backend/server/services/voip-push.ts": { "/Users/chahinebrini/mono/rebreak-monorepo/backend/server/services/voip-push.ts": {
"mtime": 1780595681.9513056, "mtime": 1780595681.9513056,
@ -1940,9 +1940,9 @@
"semantic_hash": "c18933134fc157906a3e491e8d342e87" "semantic_hash": "c18933134fc157906a3e491e8d342e87"
}, },
"/Users/chahinebrini/mono/rebreak-monorepo/backend/imap-idle/index.mjs": { "/Users/chahinebrini/mono/rebreak-monorepo/backend/imap-idle/index.mjs": {
"mtime": 1780772017.8478682, "mtime": 1781119795.4583845,
"ast_hash": "4f41e3575c5c8416e95bcf33cd0b1d0b", "ast_hash": "d80d2455a44a3d3ac9277ad8da3aa0ba",
"semantic_hash": "4f41e3575c5c8416e95bcf33cd0b1d0b" "semantic_hash": ""
}, },
"/Users/chahinebrini/mono/rebreak-monorepo/backend/scripts/generate-pir-input.ts": { "/Users/chahinebrini/mono/rebreak-monorepo/backend/scripts/generate-pir-input.ts": {
"mtime": 1779277186.8516371, "mtime": 1779277186.8516371,
@ -2005,9 +2005,9 @@
"semantic_hash": "8c0913472d610e7a6f2b2e7a3caec6dc" "semantic_hash": "8c0913472d610e7a6f2b2e7a3caec6dc"
}, },
"/Users/chahinebrini/mono/rebreak-monorepo/scripts/deploy-from-artifact.sh": { "/Users/chahinebrini/mono/rebreak-monorepo/scripts/deploy-from-artifact.sh": {
"mtime": 1780298966.7916753, "mtime": 1781099229.5164309,
"ast_hash": "8dd3099346bf2c491fecd62c5afa6681", "ast_hash": "96939f3d96a62246da00795981d2d03e",
"semantic_hash": "8dd3099346bf2c491fecd62c5afa6681" "semantic_hash": ""
}, },
"/Users/chahinebrini/mono/rebreak-monorepo/scripts/deploy-webhook/server.mjs": { "/Users/chahinebrini/mono/rebreak-monorepo/scripts/deploy-webhook/server.mjs": {
"mtime": 1778106454.0610669, "mtime": 1778106454.0610669,
@ -2195,8 +2195,8 @@
"semantic_hash": "da631aa4384ac11d6511e73f1ff0c5bf" "semantic_hash": "da631aa4384ac11d6511e73f1ff0c5bf"
}, },
"/Users/chahinebrini/mono/rebreak-monorepo/apps/rebreak-native/app.config.ts": { "/Users/chahinebrini/mono/rebreak-monorepo/apps/rebreak-native/app.config.ts": {
"mtime": 1781073703.6655655, "mtime": 1781099346.8820252,
"ast_hash": "5cb4572c544fd385522d9d019d32b752", "ast_hash": "4c41684adceca23a5c30e8293e07c44c",
"semantic_hash": "" "semantic_hash": ""
}, },
"/Users/chahinebrini/mono/rebreak-monorepo/apps/rebreak-native/babel.config.js": { "/Users/chahinebrini/mono/rebreak-monorepo/apps/rebreak-native/babel.config.js": {
@ -2205,8 +2205,8 @@
"semantic_hash": "754dc6a047f1004deb396cadb03c5d7c" "semantic_hash": "754dc6a047f1004deb396cadb03c5d7c"
}, },
"/Users/chahinebrini/mono/rebreak-monorepo/apps/rebreak-native/package.json": { "/Users/chahinebrini/mono/rebreak-monorepo/apps/rebreak-native/package.json": {
"mtime": 1781073703.6689231, "mtime": 1781099346.8844354,
"ast_hash": "e3570fd91354c8017ce07eaa2a26f35b", "ast_hash": "1ac439b18d99e589e0ab5036a2f42f64",
"semantic_hash": "" "semantic_hash": ""
}, },
"/Users/chahinebrini/mono/rebreak-monorepo/apps/rebreak-native/tsconfig.json": { "/Users/chahinebrini/mono/rebreak-monorepo/apps/rebreak-native/tsconfig.json": {
@ -2275,8 +2275,8 @@
"semantic_hash": "cfad92e1cfdee1be4486bc56df15e7b0" "semantic_hash": "cfad92e1cfdee1be4486bc56df15e7b0"
}, },
"/Users/chahinebrini/mono/rebreak-monorepo/apps/rebreak-native/app/dm.tsx": { "/Users/chahinebrini/mono/rebreak-monorepo/apps/rebreak-native/app/dm.tsx": {
"mtime": 1781090770.358354, "mtime": 1781091425.5896177,
"ast_hash": "afaa8864153c549fa26716b7998d4d9b", "ast_hash": "2ee40dab096fb63cd4d010a4636256cf",
"semantic_hash": "" "semantic_hash": ""
}, },
"/Users/chahinebrini/mono/rebreak-monorepo/apps/rebreak-native/app/debug.tsx": { "/Users/chahinebrini/mono/rebreak-monorepo/apps/rebreak-native/app/debug.tsx": {
@ -2590,62 +2590,62 @@
"semantic_hash": "79892b512c5b108eecf7ce2573a819a5" "semantic_hash": "79892b512c5b108eecf7ce2573a819a5"
}, },
"/Users/chahinebrini/mono/rebreak-monorepo/apps/rebreak-native/ios/Podfile.properties.json": { "/Users/chahinebrini/mono/rebreak-monorepo/apps/rebreak-native/ios/Podfile.properties.json": {
"mtime": 1780886883.2589357, "mtime": 1781099356.0946026,
"ast_hash": "0847919237e15fbd3abf30da0e4cb8c4", "ast_hash": "0847919237e15fbd3abf30da0e4cb8c4",
"semantic_hash": "0847919237e15fbd3abf30da0e4cb8c4" "semantic_hash": "0847919237e15fbd3abf30da0e4cb8c4"
}, },
"/Users/chahinebrini/mono/rebreak-monorepo/apps/rebreak-native/ios/RebreakPacketTunnelExtension/HashList.swift": { "/Users/chahinebrini/mono/rebreak-monorepo/apps/rebreak-native/ios/RebreakPacketTunnelExtension/HashList.swift": {
"mtime": 1780886883.182512, "mtime": 1781099356.0209033,
"ast_hash": "a8d42be577e98ea2bde0eb3beb5bbfe6", "ast_hash": "a8d42be577e98ea2bde0eb3beb5bbfe6",
"semantic_hash": "a8d42be577e98ea2bde0eb3beb5bbfe6" "semantic_hash": "a8d42be577e98ea2bde0eb3beb5bbfe6"
}, },
"/Users/chahinebrini/mono/rebreak-monorepo/apps/rebreak-native/ios/RebreakPacketTunnelExtension/DomainHasher.swift": { "/Users/chahinebrini/mono/rebreak-monorepo/apps/rebreak-native/ios/RebreakPacketTunnelExtension/DomainHasher.swift": {
"mtime": 1780886883.1822066, "mtime": 1781099356.0206153,
"ast_hash": "093b1e647a0d3803c75060c0355c095e", "ast_hash": "093b1e647a0d3803c75060c0355c095e",
"semantic_hash": "093b1e647a0d3803c75060c0355c095e" "semantic_hash": "093b1e647a0d3803c75060c0355c095e"
}, },
"/Users/chahinebrini/mono/rebreak-monorepo/apps/rebreak-native/ios/RebreakPacketTunnelExtension/PacketTunnelProvider.swift": { "/Users/chahinebrini/mono/rebreak-monorepo/apps/rebreak-native/ios/RebreakPacketTunnelExtension/PacketTunnelProvider.swift": {
"mtime": 1780886883.1835492, "mtime": 1781099356.0227797,
"ast_hash": "b8c819e0f611ec3c6b685429a34bb1c8", "ast_hash": "b8c819e0f611ec3c6b685429a34bb1c8",
"semantic_hash": "b8c819e0f611ec3c6b685429a34bb1c8" "semantic_hash": "b8c819e0f611ec3c6b685429a34bb1c8"
}, },
"/Users/chahinebrini/mono/rebreak-monorepo/apps/rebreak-native/ios/RebreakPacketTunnelExtension/DnsFilter.swift": { "/Users/chahinebrini/mono/rebreak-monorepo/apps/rebreak-native/ios/RebreakPacketTunnelExtension/DnsFilter.swift": {
"mtime": 1780886883.1819117, "mtime": 1781099356.020003,
"ast_hash": "be7ad5d15bf0edf4a90dc6ef8ac88ce9", "ast_hash": "be7ad5d15bf0edf4a90dc6ef8ac88ce9",
"semantic_hash": "be7ad5d15bf0edf4a90dc6ef8ac88ce9" "semantic_hash": "be7ad5d15bf0edf4a90dc6ef8ac88ce9"
}, },
"/Users/chahinebrini/mono/rebreak-monorepo/apps/rebreak-native/ios/ReBreak/ReBreak-Bridging-Header.h": { "/Users/chahinebrini/mono/rebreak-monorepo/apps/rebreak-native/ios/ReBreak/ReBreak-Bridging-Header.h": {
"mtime": 1780886883.187419, "mtime": 1781099356.0249267,
"ast_hash": "7bda22d76cc88bc22215241b452bd3d3", "ast_hash": "7bda22d76cc88bc22215241b452bd3d3",
"semantic_hash": "7bda22d76cc88bc22215241b452bd3d3" "semantic_hash": "7bda22d76cc88bc22215241b452bd3d3"
}, },
"/Users/chahinebrini/mono/rebreak-monorepo/apps/rebreak-native/ios/ReBreak/AppDelegate.swift": { "/Users/chahinebrini/mono/rebreak-monorepo/apps/rebreak-native/ios/ReBreak/AppDelegate.swift": {
"mtime": 1780886883.2745233, "mtime": 1781099356.111295,
"ast_hash": "00cf50df46613f328fe200ab677f3b1a", "ast_hash": "00cf50df46613f328fe200ab677f3b1a",
"semantic_hash": "00cf50df46613f328fe200ab677f3b1a" "semantic_hash": "00cf50df46613f328fe200ab677f3b1a"
}, },
"/Users/chahinebrini/mono/rebreak-monorepo/apps/rebreak-native/ios/ReBreak/Images.xcassets/Contents.json": { "/Users/chahinebrini/mono/rebreak-monorepo/apps/rebreak-native/ios/ReBreak/Images.xcassets/Contents.json": {
"mtime": 1780886882.844067, "mtime": 1781099355.7058473,
"ast_hash": "96cbaeb138eab0aceebfdf0022c7d835", "ast_hash": "96cbaeb138eab0aceebfdf0022c7d835",
"semantic_hash": "96cbaeb138eab0aceebfdf0022c7d835" "semantic_hash": "96cbaeb138eab0aceebfdf0022c7d835"
}, },
"/Users/chahinebrini/mono/rebreak-monorepo/apps/rebreak-native/ios/ReBreak/Images.xcassets/AppIcon.appiconset/Contents.json": { "/Users/chahinebrini/mono/rebreak-monorepo/apps/rebreak-native/ios/ReBreak/Images.xcassets/AppIcon.appiconset/Contents.json": {
"mtime": 1780886883.173795, "mtime": 1781099356.014499,
"ast_hash": "e753dae9a3ae1e8aeb8f44742ea0614b", "ast_hash": "e753dae9a3ae1e8aeb8f44742ea0614b",
"semantic_hash": "e753dae9a3ae1e8aeb8f44742ea0614b" "semantic_hash": "e753dae9a3ae1e8aeb8f44742ea0614b"
}, },
"/Users/chahinebrini/mono/rebreak-monorepo/apps/rebreak-native/ios/ReBreak/Images.xcassets/SplashScreenBackground.colorset/Contents.json": { "/Users/chahinebrini/mono/rebreak-monorepo/apps/rebreak-native/ios/ReBreak/Images.xcassets/SplashScreenBackground.colorset/Contents.json": {
"mtime": 1780886883.1759312, "mtime": 1781099356.0158637,
"ast_hash": "7a1fd2646e70f93ae4034dadbe219b43", "ast_hash": "7a1fd2646e70f93ae4034dadbe219b43",
"semantic_hash": "7a1fd2646e70f93ae4034dadbe219b43" "semantic_hash": "7a1fd2646e70f93ae4034dadbe219b43"
}, },
"/Users/chahinebrini/mono/rebreak-monorepo/apps/rebreak-native/ios/ReBreak/Images.xcassets/SplashScreenLegacy.imageset/Contents.json": { "/Users/chahinebrini/mono/rebreak-monorepo/apps/rebreak-native/ios/ReBreak/Images.xcassets/SplashScreenLegacy.imageset/Contents.json": {
"mtime": 1780886883.177204, "mtime": 1781099356.0170434,
"ast_hash": "a84d44e2229df0082c16210906e94575", "ast_hash": "a84d44e2229df0082c16210906e94575",
"semantic_hash": "a84d44e2229df0082c16210906e94575" "semantic_hash": "a84d44e2229df0082c16210906e94575"
}, },
"/Users/chahinebrini/mono/rebreak-monorepo/apps/rebreak-native/ios/RebreakContentFilter/FilterDataProvider.swift": { "/Users/chahinebrini/mono/rebreak-monorepo/apps/rebreak-native/ios/RebreakContentFilter/FilterDataProvider.swift": {
"mtime": 1780886883.1850178, "mtime": 1781099356.0233262,
"ast_hash": "75d30ad1138439cb747519fe75dd5462", "ast_hash": "75d30ad1138439cb747519fe75dd5462",
"semantic_hash": "75d30ad1138439cb747519fe75dd5462" "semantic_hash": "75d30ad1138439cb747519fe75dd5462"
}, },
@ -3365,8 +3365,8 @@
"semantic_hash": "c243c6784e80ca91e22c1e37f71bcf20" "semantic_hash": "c243c6784e80ca91e22c1e37f71bcf20"
}, },
"/Users/chahinebrini/mono/rebreak-monorepo/apps/rebreak-native/hooks/useChatRealtime.ts": { "/Users/chahinebrini/mono/rebreak-monorepo/apps/rebreak-native/hooks/useChatRealtime.ts": {
"mtime": 1781090763.2619104, "mtime": 1781091420.6683738,
"ast_hash": "243caa8c159822d41375b6ff422f5914", "ast_hash": "86afc4068b0d4e1ca045d6a6f3fed587",
"semantic_hash": "" "semantic_hash": ""
}, },
"/Users/chahinebrini/mono/rebreak-monorepo/apps/rebreak-native/hooks/useLastSeenHeartbeat.ts": { "/Users/chahinebrini/mono/rebreak-monorepo/apps/rebreak-native/hooks/useLastSeenHeartbeat.ts": {
@ -3440,8 +3440,8 @@
"semantic_hash": "d46991903a163c0331feb7c15e8211f4" "semantic_hash": "d46991903a163c0331feb7c15e8211f4"
}, },
"/Users/chahinebrini/mono/rebreak-monorepo/apps/rebreak-native/android/app/build.gradle": { "/Users/chahinebrini/mono/rebreak-monorepo/apps/rebreak-native/android/app/build.gradle": {
"mtime": 1781073703.6626234, "mtime": 1781099346.8792436,
"ast_hash": "75fdd78656d6a210c2b7874b0ee0faae", "ast_hash": "3572e426b00fc0f801751b93e38f9ee6",
"semantic_hash": "" "semantic_hash": ""
}, },
"/Users/chahinebrini/mono/rebreak-monorepo/apps/rebreak-native/android/app/google-services.json": { "/Users/chahinebrini/mono/rebreak-monorepo/apps/rebreak-native/android/app/google-services.json": {
@ -3490,12 +3490,12 @@
"semantic_hash": "53876b03fe0a931f3accc4e6ef99503b" "semantic_hash": "53876b03fe0a931f3accc4e6ef99503b"
}, },
"/Users/chahinebrini/mono/rebreak-monorepo/apps/rebreak-native/android/app/.cxx/RelWithDebInfo/5m151t3n/armeabi-v7a/compile_commands.json": { "/Users/chahinebrini/mono/rebreak-monorepo/apps/rebreak-native/android/app/.cxx/RelWithDebInfo/5m151t3n/armeabi-v7a/compile_commands.json": {
"mtime": 1781073836.0998764, "mtime": 1781100084.6737607,
"ast_hash": "a47cc6a699bb29c35b353e290d0e9d3b", "ast_hash": "a47cc6a699bb29c35b353e290d0e9d3b",
"semantic_hash": "a47cc6a699bb29c35b353e290d0e9d3b" "semantic_hash": "a47cc6a699bb29c35b353e290d0e9d3b"
}, },
"/Users/chahinebrini/mono/rebreak-monorepo/apps/rebreak-native/android/app/.cxx/RelWithDebInfo/5m151t3n/armeabi-v7a/android_gradle_build_mini.json": { "/Users/chahinebrini/mono/rebreak-monorepo/apps/rebreak-native/android/app/.cxx/RelWithDebInfo/5m151t3n/armeabi-v7a/android_gradle_build_mini.json": {
"mtime": 1781073836.174424, "mtime": 1781100084.766397,
"ast_hash": "9301f24629f8d09ea1e6ec9ac1861fd9", "ast_hash": "9301f24629f8d09ea1e6ec9ac1861fd9",
"semantic_hash": "9301f24629f8d09ea1e6ec9ac1861fd9" "semantic_hash": "9301f24629f8d09ea1e6ec9ac1861fd9"
}, },
@ -3505,17 +3505,17 @@
"semantic_hash": "d314c30797af8190a9eeaecda8c0ee99" "semantic_hash": "d314c30797af8190a9eeaecda8c0ee99"
}, },
"/Users/chahinebrini/mono/rebreak-monorepo/apps/rebreak-native/android/app/.cxx/RelWithDebInfo/5m151t3n/armeabi-v7a/android_gradle_build.json": { "/Users/chahinebrini/mono/rebreak-monorepo/apps/rebreak-native/android/app/.cxx/RelWithDebInfo/5m151t3n/armeabi-v7a/android_gradle_build.json": {
"mtime": 1781073836.1413016, "mtime": 1781100084.7344887,
"ast_hash": "3e5d069e204dc4f0515b59f44287f1a3", "ast_hash": "3e5d069e204dc4f0515b59f44287f1a3",
"semantic_hash": "3e5d069e204dc4f0515b59f44287f1a3" "semantic_hash": "3e5d069e204dc4f0515b59f44287f1a3"
}, },
"/Users/chahinebrini/mono/rebreak-monorepo/apps/rebreak-native/android/app/.cxx/RelWithDebInfo/5m151t3n/armeabi-v7a/CMakeFiles/_CMakeLTOTest-CXX/src/foo.cpp": { "/Users/chahinebrini/mono/rebreak-monorepo/apps/rebreak-native/android/app/.cxx/RelWithDebInfo/5m151t3n/armeabi-v7a/CMakeFiles/_CMakeLTOTest-CXX/src/foo.cpp": {
"mtime": 1781073835.804489, "mtime": 1781100084.267121,
"ast_hash": "bbf8a05ecab16853fd9ad99b367175f8", "ast_hash": "bbf8a05ecab16853fd9ad99b367175f8",
"semantic_hash": "bbf8a05ecab16853fd9ad99b367175f8" "semantic_hash": "bbf8a05ecab16853fd9ad99b367175f8"
}, },
"/Users/chahinebrini/mono/rebreak-monorepo/apps/rebreak-native/android/app/.cxx/RelWithDebInfo/5m151t3n/armeabi-v7a/CMakeFiles/_CMakeLTOTest-CXX/src/main.cpp": { "/Users/chahinebrini/mono/rebreak-monorepo/apps/rebreak-native/android/app/.cxx/RelWithDebInfo/5m151t3n/armeabi-v7a/CMakeFiles/_CMakeLTOTest-CXX/src/main.cpp": {
"mtime": 1781073835.8048143, "mtime": 1781100084.267873,
"ast_hash": "6155bf9e179fd0f9c2529e16119d24d8", "ast_hash": "6155bf9e179fd0f9c2529e16119d24d8",
"semantic_hash": "6155bf9e179fd0f9c2529e16119d24d8" "semantic_hash": "6155bf9e179fd0f9c2529e16119d24d8"
}, },
@ -3725,12 +3725,12 @@
"semantic_hash": "bc5564cd1a233b5995493d0a94bbdd76" "semantic_hash": "bc5564cd1a233b5995493d0a94bbdd76"
}, },
"/Users/chahinebrini/mono/rebreak-monorepo/apps/rebreak-native/android/app/.cxx/RelWithDebInfo/5m151t3n/x86/compile_commands.json": { "/Users/chahinebrini/mono/rebreak-monorepo/apps/rebreak-native/android/app/.cxx/RelWithDebInfo/5m151t3n/x86/compile_commands.json": {
"mtime": 1781073866.9398248, "mtime": 1781100117.9617994,
"ast_hash": "66984f9a8a2ef85bf6765e0fa4fe4b83", "ast_hash": "66984f9a8a2ef85bf6765e0fa4fe4b83",
"semantic_hash": "66984f9a8a2ef85bf6765e0fa4fe4b83" "semantic_hash": "66984f9a8a2ef85bf6765e0fa4fe4b83"
}, },
"/Users/chahinebrini/mono/rebreak-monorepo/apps/rebreak-native/android/app/.cxx/RelWithDebInfo/5m151t3n/x86/android_gradle_build_mini.json": { "/Users/chahinebrini/mono/rebreak-monorepo/apps/rebreak-native/android/app/.cxx/RelWithDebInfo/5m151t3n/x86/android_gradle_build_mini.json": {
"mtime": 1781073867.0011928, "mtime": 1781100118.0257287,
"ast_hash": "30d567c74332cde560affcd138d9604d", "ast_hash": "30d567c74332cde560affcd138d9604d",
"semantic_hash": "30d567c74332cde560affcd138d9604d" "semantic_hash": "30d567c74332cde560affcd138d9604d"
}, },
@ -3740,17 +3740,17 @@
"semantic_hash": "d314c30797af8190a9eeaecda8c0ee99" "semantic_hash": "d314c30797af8190a9eeaecda8c0ee99"
}, },
"/Users/chahinebrini/mono/rebreak-monorepo/apps/rebreak-native/android/app/.cxx/RelWithDebInfo/5m151t3n/x86/android_gradle_build.json": { "/Users/chahinebrini/mono/rebreak-monorepo/apps/rebreak-native/android/app/.cxx/RelWithDebInfo/5m151t3n/x86/android_gradle_build.json": {
"mtime": 1781073866.9804351, "mtime": 1781100118.0082788,
"ast_hash": "5fc1b715aeadb95d76c0ea04ef900cce", "ast_hash": "5fc1b715aeadb95d76c0ea04ef900cce",
"semantic_hash": "5fc1b715aeadb95d76c0ea04ef900cce" "semantic_hash": "5fc1b715aeadb95d76c0ea04ef900cce"
}, },
"/Users/chahinebrini/mono/rebreak-monorepo/apps/rebreak-native/android/app/.cxx/RelWithDebInfo/5m151t3n/x86/CMakeFiles/_CMakeLTOTest-CXX/src/foo.cpp": { "/Users/chahinebrini/mono/rebreak-monorepo/apps/rebreak-native/android/app/.cxx/RelWithDebInfo/5m151t3n/x86/CMakeFiles/_CMakeLTOTest-CXX/src/foo.cpp": {
"mtime": 1781073866.5783515, "mtime": 1781100117.6987588,
"ast_hash": "bbf8a05ecab16853fd9ad99b367175f8", "ast_hash": "bbf8a05ecab16853fd9ad99b367175f8",
"semantic_hash": "bbf8a05ecab16853fd9ad99b367175f8" "semantic_hash": "bbf8a05ecab16853fd9ad99b367175f8"
}, },
"/Users/chahinebrini/mono/rebreak-monorepo/apps/rebreak-native/android/app/.cxx/RelWithDebInfo/5m151t3n/x86/CMakeFiles/_CMakeLTOTest-CXX/src/main.cpp": { "/Users/chahinebrini/mono/rebreak-monorepo/apps/rebreak-native/android/app/.cxx/RelWithDebInfo/5m151t3n/x86/CMakeFiles/_CMakeLTOTest-CXX/src/main.cpp": {
"mtime": 1781073866.578687, "mtime": 1781100117.699018,
"ast_hash": "6155bf9e179fd0f9c2529e16119d24d8", "ast_hash": "6155bf9e179fd0f9c2529e16119d24d8",
"semantic_hash": "6155bf9e179fd0f9c2529e16119d24d8" "semantic_hash": "6155bf9e179fd0f9c2529e16119d24d8"
}, },
@ -3960,12 +3960,12 @@
"semantic_hash": "78a35038e0622e60963edc9e2bb98350" "semantic_hash": "78a35038e0622e60963edc9e2bb98350"
}, },
"/Users/chahinebrini/mono/rebreak-monorepo/apps/rebreak-native/android/app/.cxx/RelWithDebInfo/5m151t3n/arm64-v8a/compile_commands.json": { "/Users/chahinebrini/mono/rebreak-monorepo/apps/rebreak-native/android/app/.cxx/RelWithDebInfo/5m151t3n/arm64-v8a/compile_commands.json": {
"mtime": 1781073800.525331, "mtime": 1781100051.5533953,
"ast_hash": "0f33a6cf3d9c81a85595d18f755caaa6", "ast_hash": "0f33a6cf3d9c81a85595d18f755caaa6",
"semantic_hash": "0f33a6cf3d9c81a85595d18f755caaa6" "semantic_hash": "0f33a6cf3d9c81a85595d18f755caaa6"
}, },
"/Users/chahinebrini/mono/rebreak-monorepo/apps/rebreak-native/android/app/.cxx/RelWithDebInfo/5m151t3n/arm64-v8a/android_gradle_build_mini.json": { "/Users/chahinebrini/mono/rebreak-monorepo/apps/rebreak-native/android/app/.cxx/RelWithDebInfo/5m151t3n/arm64-v8a/android_gradle_build_mini.json": {
"mtime": 1781073801.183012, "mtime": 1781100051.7113771,
"ast_hash": "c5285287748988fb63ef367ac81c7048", "ast_hash": "c5285287748988fb63ef367ac81c7048",
"semantic_hash": "c5285287748988fb63ef367ac81c7048" "semantic_hash": "c5285287748988fb63ef367ac81c7048"
}, },
@ -3975,17 +3975,17 @@
"semantic_hash": "d314c30797af8190a9eeaecda8c0ee99" "semantic_hash": "d314c30797af8190a9eeaecda8c0ee99"
}, },
"/Users/chahinebrini/mono/rebreak-monorepo/apps/rebreak-native/android/app/.cxx/RelWithDebInfo/5m151t3n/arm64-v8a/android_gradle_build.json": { "/Users/chahinebrini/mono/rebreak-monorepo/apps/rebreak-native/android/app/.cxx/RelWithDebInfo/5m151t3n/arm64-v8a/android_gradle_build.json": {
"mtime": 1781073801.064058, "mtime": 1781100051.6193027,
"ast_hash": "9450cfc36a13b7f24fa57dcd84d6965b", "ast_hash": "9450cfc36a13b7f24fa57dcd84d6965b",
"semantic_hash": "9450cfc36a13b7f24fa57dcd84d6965b" "semantic_hash": "9450cfc36a13b7f24fa57dcd84d6965b"
}, },
"/Users/chahinebrini/mono/rebreak-monorepo/apps/rebreak-native/android/app/.cxx/RelWithDebInfo/5m151t3n/arm64-v8a/CMakeFiles/_CMakeLTOTest-CXX/src/foo.cpp": { "/Users/chahinebrini/mono/rebreak-monorepo/apps/rebreak-native/android/app/.cxx/RelWithDebInfo/5m151t3n/arm64-v8a/CMakeFiles/_CMakeLTOTest-CXX/src/foo.cpp": {
"mtime": 1781073799.8494682, "mtime": 1781100050.7198606,
"ast_hash": "bbf8a05ecab16853fd9ad99b367175f8", "ast_hash": "bbf8a05ecab16853fd9ad99b367175f8",
"semantic_hash": "bbf8a05ecab16853fd9ad99b367175f8" "semantic_hash": "bbf8a05ecab16853fd9ad99b367175f8"
}, },
"/Users/chahinebrini/mono/rebreak-monorepo/apps/rebreak-native/android/app/.cxx/RelWithDebInfo/5m151t3n/arm64-v8a/CMakeFiles/_CMakeLTOTest-CXX/src/main.cpp": { "/Users/chahinebrini/mono/rebreak-monorepo/apps/rebreak-native/android/app/.cxx/RelWithDebInfo/5m151t3n/arm64-v8a/CMakeFiles/_CMakeLTOTest-CXX/src/main.cpp": {
"mtime": 1781073799.849759, "mtime": 1781100050.7201498,
"ast_hash": "6155bf9e179fd0f9c2529e16119d24d8", "ast_hash": "6155bf9e179fd0f9c2529e16119d24d8",
"semantic_hash": "6155bf9e179fd0f9c2529e16119d24d8" "semantic_hash": "6155bf9e179fd0f9c2529e16119d24d8"
}, },
@ -4195,12 +4195,12 @@
"semantic_hash": "76d80416ea4b83e2170137be0f6c62d7" "semantic_hash": "76d80416ea4b83e2170137be0f6c62d7"
}, },
"/Users/chahinebrini/mono/rebreak-monorepo/apps/rebreak-native/android/app/.cxx/RelWithDebInfo/5m151t3n/x86_64/compile_commands.json": { "/Users/chahinebrini/mono/rebreak-monorepo/apps/rebreak-native/android/app/.cxx/RelWithDebInfo/5m151t3n/x86_64/compile_commands.json": {
"mtime": 1781073898.8306706, "mtime": 1781100148.1231582,
"ast_hash": "cce4bf831bc4b7f5a9b284f56696cb22", "ast_hash": "cce4bf831bc4b7f5a9b284f56696cb22",
"semantic_hash": "cce4bf831bc4b7f5a9b284f56696cb22" "semantic_hash": "cce4bf831bc4b7f5a9b284f56696cb22"
}, },
"/Users/chahinebrini/mono/rebreak-monorepo/apps/rebreak-native/android/app/.cxx/RelWithDebInfo/5m151t3n/x86_64/android_gradle_build_mini.json": { "/Users/chahinebrini/mono/rebreak-monorepo/apps/rebreak-native/android/app/.cxx/RelWithDebInfo/5m151t3n/x86_64/android_gradle_build_mini.json": {
"mtime": 1781073898.89234, "mtime": 1781100148.1796436,
"ast_hash": "09a097b4d4dade95e0cf0055c5c76c71", "ast_hash": "09a097b4d4dade95e0cf0055c5c76c71",
"semantic_hash": "09a097b4d4dade95e0cf0055c5c76c71" "semantic_hash": "09a097b4d4dade95e0cf0055c5c76c71"
}, },
@ -4210,17 +4210,17 @@
"semantic_hash": "d314c30797af8190a9eeaecda8c0ee99" "semantic_hash": "d314c30797af8190a9eeaecda8c0ee99"
}, },
"/Users/chahinebrini/mono/rebreak-monorepo/apps/rebreak-native/android/app/.cxx/RelWithDebInfo/5m151t3n/x86_64/android_gradle_build.json": { "/Users/chahinebrini/mono/rebreak-monorepo/apps/rebreak-native/android/app/.cxx/RelWithDebInfo/5m151t3n/x86_64/android_gradle_build.json": {
"mtime": 1781073898.8724933, "mtime": 1781100148.1618986,
"ast_hash": "d34917cd0acf613830fa24ce4d823dc5", "ast_hash": "d34917cd0acf613830fa24ce4d823dc5",
"semantic_hash": "d34917cd0acf613830fa24ce4d823dc5" "semantic_hash": "d34917cd0acf613830fa24ce4d823dc5"
}, },
"/Users/chahinebrini/mono/rebreak-monorepo/apps/rebreak-native/android/app/.cxx/RelWithDebInfo/5m151t3n/x86_64/CMakeFiles/_CMakeLTOTest-CXX/src/foo.cpp": { "/Users/chahinebrini/mono/rebreak-monorepo/apps/rebreak-native/android/app/.cxx/RelWithDebInfo/5m151t3n/x86_64/CMakeFiles/_CMakeLTOTest-CXX/src/foo.cpp": {
"mtime": 1781073898.5592494, "mtime": 1781100147.8950455,
"ast_hash": "bbf8a05ecab16853fd9ad99b367175f8", "ast_hash": "bbf8a05ecab16853fd9ad99b367175f8",
"semantic_hash": "bbf8a05ecab16853fd9ad99b367175f8" "semantic_hash": "bbf8a05ecab16853fd9ad99b367175f8"
}, },
"/Users/chahinebrini/mono/rebreak-monorepo/apps/rebreak-native/android/app/.cxx/RelWithDebInfo/5m151t3n/x86_64/CMakeFiles/_CMakeLTOTest-CXX/src/main.cpp": { "/Users/chahinebrini/mono/rebreak-monorepo/apps/rebreak-native/android/app/.cxx/RelWithDebInfo/5m151t3n/x86_64/CMakeFiles/_CMakeLTOTest-CXX/src/main.cpp": {
"mtime": 1781073898.559822, "mtime": 1781100147.895296,
"ast_hash": "6155bf9e179fd0f9c2529e16119d24d8", "ast_hash": "6155bf9e179fd0f9c2529e16119d24d8",
"semantic_hash": "6155bf9e179fd0f9c2529e16119d24d8" "semantic_hash": "6155bf9e179fd0f9c2529e16119d24d8"
}, },
@ -6505,9 +6505,9 @@
"semantic_hash": "2fd4e4c916da6ace47a63119f42c44ec" "semantic_hash": "2fd4e4c916da6ace47a63119f42c44ec"
}, },
"/Users/chahinebrini/mono/rebreak-monorepo/.github/workflows/deploy-staging.yml": { "/Users/chahinebrini/mono/rebreak-monorepo/.github/workflows/deploy-staging.yml": {
"mtime": 1780298849.8483675, "mtime": 1781096788.1786268,
"ast_hash": "405612dc7a24d9cda0388ef873ef8db1", "ast_hash": "f6b41140b98cd8446489173b3b8d8175",
"semantic_hash": "405612dc7a24d9cda0388ef873ef8db1" "semantic_hash": ""
}, },
"/Users/chahinebrini/mono/rebreak-monorepo/.github/workflows/build-magic-win.yml": { "/Users/chahinebrini/mono/rebreak-monorepo/.github/workflows/build-magic-win.yml": {
"mtime": 1780832567.5303535, "mtime": 1780832567.5303535,
@ -6935,7 +6935,7 @@
"semantic_hash": "f4afab5570d6101011b7b84eb7d11d17" "semantic_hash": "f4afab5570d6101011b7b84eb7d11d17"
}, },
"/Users/chahinebrini/mono/rebreak-monorepo/apps/rebreak-native/android/app/.cxx/RelWithDebInfo/5m151t3n/armeabi-v7a/additional_project_files.txt": { "/Users/chahinebrini/mono/rebreak-monorepo/apps/rebreak-native/android/app/.cxx/RelWithDebInfo/5m151t3n/armeabi-v7a/additional_project_files.txt": {
"mtime": 1781073836.139619, "mtime": 1781100084.730819,
"ast_hash": "ce4de092e3d56226e9c4f869a10540b0", "ast_hash": "ce4de092e3d56226e9c4f869a10540b0",
"semantic_hash": "ce4de092e3d56226e9c4f869a10540b0" "semantic_hash": "ce4de092e3d56226e9c4f869a10540b0"
}, },
@ -6950,22 +6950,22 @@
"semantic_hash": "bc83b589a97bb0c895990549d7ba73fc" "semantic_hash": "bc83b589a97bb0c895990549d7ba73fc"
}, },
"/Users/chahinebrini/mono/rebreak-monorepo/apps/rebreak-native/android/app/.cxx/RelWithDebInfo/5m151t3n/armeabi-v7a/CMakeFiles/TargetDirectories.txt": { "/Users/chahinebrini/mono/rebreak-monorepo/apps/rebreak-native/android/app/.cxx/RelWithDebInfo/5m151t3n/armeabi-v7a/CMakeFiles/TargetDirectories.txt": {
"mtime": 1781073836.0989523, "mtime": 1781100084.672128,
"ast_hash": "19be9476b5a519b647b0d9fb774ed89b", "ast_hash": "19be9476b5a519b647b0d9fb774ed89b",
"semantic_hash": "19be9476b5a519b647b0d9fb774ed89b" "semantic_hash": "19be9476b5a519b647b0d9fb774ed89b"
}, },
"/Users/chahinebrini/mono/rebreak-monorepo/apps/rebreak-native/android/app/.cxx/RelWithDebInfo/5m151t3n/armeabi-v7a/CMakeFiles/_CMakeLTOTest-CXX/bin/CMakeCache.txt": { "/Users/chahinebrini/mono/rebreak-monorepo/apps/rebreak-native/android/app/.cxx/RelWithDebInfo/5m151t3n/armeabi-v7a/CMakeFiles/_CMakeLTOTest-CXX/bin/CMakeCache.txt": {
"mtime": 1781073835.814379, "mtime": 1781100084.2792053,
"ast_hash": "0b8d2cea0f921952e562191dd9a6f46f", "ast_hash": "0b8d2cea0f921952e562191dd9a6f46f",
"semantic_hash": "0b8d2cea0f921952e562191dd9a6f46f" "semantic_hash": "0b8d2cea0f921952e562191dd9a6f46f"
}, },
"/Users/chahinebrini/mono/rebreak-monorepo/apps/rebreak-native/android/app/.cxx/RelWithDebInfo/5m151t3n/armeabi-v7a/CMakeFiles/_CMakeLTOTest-CXX/bin/CMakeFiles/TargetDirectories.txt": { "/Users/chahinebrini/mono/rebreak-monorepo/apps/rebreak-native/android/app/.cxx/RelWithDebInfo/5m151t3n/armeabi-v7a/CMakeFiles/_CMakeLTOTest-CXX/bin/CMakeFiles/TargetDirectories.txt": {
"mtime": 1781073835.8188832, "mtime": 1781100084.2853115,
"ast_hash": "5bcb11cdfd08eb18fd4b4ec3262d4468", "ast_hash": "5bcb11cdfd08eb18fd4b4ec3262d4468",
"semantic_hash": "5bcb11cdfd08eb18fd4b4ec3262d4468" "semantic_hash": "5bcb11cdfd08eb18fd4b4ec3262d4468"
}, },
"/Users/chahinebrini/mono/rebreak-monorepo/apps/rebreak-native/android/app/.cxx/RelWithDebInfo/5m151t3n/armeabi-v7a/CMakeFiles/_CMakeLTOTest-CXX/src/CMakeLists.txt": { "/Users/chahinebrini/mono/rebreak-monorepo/apps/rebreak-native/android/app/.cxx/RelWithDebInfo/5m151t3n/armeabi-v7a/CMakeFiles/_CMakeLTOTest-CXX/src/CMakeLists.txt": {
"mtime": 1781073835.804099, "mtime": 1781100084.2665324,
"ast_hash": "3b72b5cc3e6a0f6bfa3e24ca244eea7d", "ast_hash": "3b72b5cc3e6a0f6bfa3e24ca244eea7d",
"semantic_hash": "3b72b5cc3e6a0f6bfa3e24ca244eea7d" "semantic_hash": "3b72b5cc3e6a0f6bfa3e24ca244eea7d"
}, },
@ -6980,7 +6980,7 @@
"semantic_hash": "f4afab5570d6101011b7b84eb7d11d17" "semantic_hash": "f4afab5570d6101011b7b84eb7d11d17"
}, },
"/Users/chahinebrini/mono/rebreak-monorepo/apps/rebreak-native/android/app/.cxx/RelWithDebInfo/5m151t3n/x86/additional_project_files.txt": { "/Users/chahinebrini/mono/rebreak-monorepo/apps/rebreak-native/android/app/.cxx/RelWithDebInfo/5m151t3n/x86/additional_project_files.txt": {
"mtime": 1781073866.979074, "mtime": 1781100118.0064566,
"ast_hash": "6bcdbb448e790539b93a892d19846be7", "ast_hash": "6bcdbb448e790539b93a892d19846be7",
"semantic_hash": "6bcdbb448e790539b93a892d19846be7" "semantic_hash": "6bcdbb448e790539b93a892d19846be7"
}, },
@ -6995,22 +6995,22 @@
"semantic_hash": "e93e240404f6cdee362151f18088f806" "semantic_hash": "e93e240404f6cdee362151f18088f806"
}, },
"/Users/chahinebrini/mono/rebreak-monorepo/apps/rebreak-native/android/app/.cxx/RelWithDebInfo/5m151t3n/x86/CMakeFiles/TargetDirectories.txt": { "/Users/chahinebrini/mono/rebreak-monorepo/apps/rebreak-native/android/app/.cxx/RelWithDebInfo/5m151t3n/x86/CMakeFiles/TargetDirectories.txt": {
"mtime": 1781073866.9387717, "mtime": 1781100117.9604154,
"ast_hash": "f7975dc628a4a1c4cfcf11083b5079c2", "ast_hash": "f7975dc628a4a1c4cfcf11083b5079c2",
"semantic_hash": "f7975dc628a4a1c4cfcf11083b5079c2" "semantic_hash": "f7975dc628a4a1c4cfcf11083b5079c2"
}, },
"/Users/chahinebrini/mono/rebreak-monorepo/apps/rebreak-native/android/app/.cxx/RelWithDebInfo/5m151t3n/x86/CMakeFiles/_CMakeLTOTest-CXX/bin/CMakeCache.txt": { "/Users/chahinebrini/mono/rebreak-monorepo/apps/rebreak-native/android/app/.cxx/RelWithDebInfo/5m151t3n/x86/CMakeFiles/_CMakeLTOTest-CXX/bin/CMakeCache.txt": {
"mtime": 1781073866.608122, "mtime": 1781100117.707986,
"ast_hash": "6424c691ed74073b9d9ed2728e164272", "ast_hash": "6424c691ed74073b9d9ed2728e164272",
"semantic_hash": "6424c691ed74073b9d9ed2728e164272" "semantic_hash": "6424c691ed74073b9d9ed2728e164272"
}, },
"/Users/chahinebrini/mono/rebreak-monorepo/apps/rebreak-native/android/app/.cxx/RelWithDebInfo/5m151t3n/x86/CMakeFiles/_CMakeLTOTest-CXX/bin/CMakeFiles/TargetDirectories.txt": { "/Users/chahinebrini/mono/rebreak-monorepo/apps/rebreak-native/android/app/.cxx/RelWithDebInfo/5m151t3n/x86/CMakeFiles/_CMakeLTOTest-CXX/bin/CMakeFiles/TargetDirectories.txt": {
"mtime": 1781073866.6233263, "mtime": 1781100117.7121255,
"ast_hash": "e1ba60372eb0f0eed1533515a3d62d2e", "ast_hash": "e1ba60372eb0f0eed1533515a3d62d2e",
"semantic_hash": "e1ba60372eb0f0eed1533515a3d62d2e" "semantic_hash": "e1ba60372eb0f0eed1533515a3d62d2e"
}, },
"/Users/chahinebrini/mono/rebreak-monorepo/apps/rebreak-native/android/app/.cxx/RelWithDebInfo/5m151t3n/x86/CMakeFiles/_CMakeLTOTest-CXX/src/CMakeLists.txt": { "/Users/chahinebrini/mono/rebreak-monorepo/apps/rebreak-native/android/app/.cxx/RelWithDebInfo/5m151t3n/x86/CMakeFiles/_CMakeLTOTest-CXX/src/CMakeLists.txt": {
"mtime": 1781073866.577554, "mtime": 1781100117.6984527,
"ast_hash": "3b72b5cc3e6a0f6bfa3e24ca244eea7d", "ast_hash": "3b72b5cc3e6a0f6bfa3e24ca244eea7d",
"semantic_hash": "3b72b5cc3e6a0f6bfa3e24ca244eea7d" "semantic_hash": "3b72b5cc3e6a0f6bfa3e24ca244eea7d"
}, },
@ -7025,7 +7025,7 @@
"semantic_hash": "f4afab5570d6101011b7b84eb7d11d17" "semantic_hash": "f4afab5570d6101011b7b84eb7d11d17"
}, },
"/Users/chahinebrini/mono/rebreak-monorepo/apps/rebreak-native/android/app/.cxx/RelWithDebInfo/5m151t3n/arm64-v8a/additional_project_files.txt": { "/Users/chahinebrini/mono/rebreak-monorepo/apps/rebreak-native/android/app/.cxx/RelWithDebInfo/5m151t3n/arm64-v8a/additional_project_files.txt": {
"mtime": 1781073801.061488, "mtime": 1781100051.6168122,
"ast_hash": "1b4fb0cc14a842924757df80ecca9ed3", "ast_hash": "1b4fb0cc14a842924757df80ecca9ed3",
"semantic_hash": "1b4fb0cc14a842924757df80ecca9ed3" "semantic_hash": "1b4fb0cc14a842924757df80ecca9ed3"
}, },
@ -7040,22 +7040,22 @@
"semantic_hash": "6712c8fd2785a4067cd65bf280bad965" "semantic_hash": "6712c8fd2785a4067cd65bf280bad965"
}, },
"/Users/chahinebrini/mono/rebreak-monorepo/apps/rebreak-native/android/app/.cxx/RelWithDebInfo/5m151t3n/arm64-v8a/CMakeFiles/TargetDirectories.txt": { "/Users/chahinebrini/mono/rebreak-monorepo/apps/rebreak-native/android/app/.cxx/RelWithDebInfo/5m151t3n/arm64-v8a/CMakeFiles/TargetDirectories.txt": {
"mtime": 1781073800.5246754, "mtime": 1781100051.5523384,
"ast_hash": "835bfd3ee9e8fe2c784e3f6d5f68e41d", "ast_hash": "835bfd3ee9e8fe2c784e3f6d5f68e41d",
"semantic_hash": "835bfd3ee9e8fe2c784e3f6d5f68e41d" "semantic_hash": "835bfd3ee9e8fe2c784e3f6d5f68e41d"
}, },
"/Users/chahinebrini/mono/rebreak-monorepo/apps/rebreak-native/android/app/.cxx/RelWithDebInfo/5m151t3n/arm64-v8a/CMakeFiles/_CMakeLTOTest-CXX/bin/CMakeCache.txt": { "/Users/chahinebrini/mono/rebreak-monorepo/apps/rebreak-native/android/app/.cxx/RelWithDebInfo/5m151t3n/arm64-v8a/CMakeFiles/_CMakeLTOTest-CXX/bin/CMakeCache.txt": {
"mtime": 1781073799.867049, "mtime": 1781100050.7305112,
"ast_hash": "e9ff3221d0ee689fccb1bfce436f17b3", "ast_hash": "e9ff3221d0ee689fccb1bfce436f17b3",
"semantic_hash": "e9ff3221d0ee689fccb1bfce436f17b3" "semantic_hash": "e9ff3221d0ee689fccb1bfce436f17b3"
}, },
"/Users/chahinebrini/mono/rebreak-monorepo/apps/rebreak-native/android/app/.cxx/RelWithDebInfo/5m151t3n/arm64-v8a/CMakeFiles/_CMakeLTOTest-CXX/bin/CMakeFiles/TargetDirectories.txt": { "/Users/chahinebrini/mono/rebreak-monorepo/apps/rebreak-native/android/app/.cxx/RelWithDebInfo/5m151t3n/arm64-v8a/CMakeFiles/_CMakeLTOTest-CXX/bin/CMakeFiles/TargetDirectories.txt": {
"mtime": 1781073799.8718777, "mtime": 1781100050.7356062,
"ast_hash": "83ad581a716027adc4b57bd8b271bf19", "ast_hash": "83ad581a716027adc4b57bd8b271bf19",
"semantic_hash": "83ad581a716027adc4b57bd8b271bf19" "semantic_hash": "83ad581a716027adc4b57bd8b271bf19"
}, },
"/Users/chahinebrini/mono/rebreak-monorepo/apps/rebreak-native/android/app/.cxx/RelWithDebInfo/5m151t3n/arm64-v8a/CMakeFiles/_CMakeLTOTest-CXX/src/CMakeLists.txt": { "/Users/chahinebrini/mono/rebreak-monorepo/apps/rebreak-native/android/app/.cxx/RelWithDebInfo/5m151t3n/arm64-v8a/CMakeFiles/_CMakeLTOTest-CXX/src/CMakeLists.txt": {
"mtime": 1781073799.8490732, "mtime": 1781100050.7192829,
"ast_hash": "3b72b5cc3e6a0f6bfa3e24ca244eea7d", "ast_hash": "3b72b5cc3e6a0f6bfa3e24ca244eea7d",
"semantic_hash": "3b72b5cc3e6a0f6bfa3e24ca244eea7d" "semantic_hash": "3b72b5cc3e6a0f6bfa3e24ca244eea7d"
}, },
@ -7070,7 +7070,7 @@
"semantic_hash": "f4afab5570d6101011b7b84eb7d11d17" "semantic_hash": "f4afab5570d6101011b7b84eb7d11d17"
}, },
"/Users/chahinebrini/mono/rebreak-monorepo/apps/rebreak-native/android/app/.cxx/RelWithDebInfo/5m151t3n/x86_64/additional_project_files.txt": { "/Users/chahinebrini/mono/rebreak-monorepo/apps/rebreak-native/android/app/.cxx/RelWithDebInfo/5m151t3n/x86_64/additional_project_files.txt": {
"mtime": 1781073898.8708692, "mtime": 1781100148.1593788,
"ast_hash": "396fe611b9074b42bde5f6e761139e59", "ast_hash": "396fe611b9074b42bde5f6e761139e59",
"semantic_hash": "396fe611b9074b42bde5f6e761139e59" "semantic_hash": "396fe611b9074b42bde5f6e761139e59"
}, },
@ -7085,22 +7085,22 @@
"semantic_hash": "24ac769e34192d05c47ba73d2c60e85c" "semantic_hash": "24ac769e34192d05c47ba73d2c60e85c"
}, },
"/Users/chahinebrini/mono/rebreak-monorepo/apps/rebreak-native/android/app/.cxx/RelWithDebInfo/5m151t3n/x86_64/CMakeFiles/TargetDirectories.txt": { "/Users/chahinebrini/mono/rebreak-monorepo/apps/rebreak-native/android/app/.cxx/RelWithDebInfo/5m151t3n/x86_64/CMakeFiles/TargetDirectories.txt": {
"mtime": 1781073898.8301, "mtime": 1781100148.1224687,
"ast_hash": "0541e12c132a86d4d73f571a94893e7f", "ast_hash": "0541e12c132a86d4d73f571a94893e7f",
"semantic_hash": "0541e12c132a86d4d73f571a94893e7f" "semantic_hash": "0541e12c132a86d4d73f571a94893e7f"
}, },
"/Users/chahinebrini/mono/rebreak-monorepo/apps/rebreak-native/android/app/.cxx/RelWithDebInfo/5m151t3n/x86_64/CMakeFiles/_CMakeLTOTest-CXX/bin/CMakeCache.txt": { "/Users/chahinebrini/mono/rebreak-monorepo/apps/rebreak-native/android/app/.cxx/RelWithDebInfo/5m151t3n/x86_64/CMakeFiles/_CMakeLTOTest-CXX/bin/CMakeCache.txt": {
"mtime": 1781073898.5701597, "mtime": 1781100147.9036858,
"ast_hash": "baf1d4c4ff8b8bcbac1962f4f77bc760", "ast_hash": "baf1d4c4ff8b8bcbac1962f4f77bc760",
"semantic_hash": "baf1d4c4ff8b8bcbac1962f4f77bc760" "semantic_hash": "baf1d4c4ff8b8bcbac1962f4f77bc760"
}, },
"/Users/chahinebrini/mono/rebreak-monorepo/apps/rebreak-native/android/app/.cxx/RelWithDebInfo/5m151t3n/x86_64/CMakeFiles/_CMakeLTOTest-CXX/bin/CMakeFiles/TargetDirectories.txt": { "/Users/chahinebrini/mono/rebreak-monorepo/apps/rebreak-native/android/app/.cxx/RelWithDebInfo/5m151t3n/x86_64/CMakeFiles/_CMakeLTOTest-CXX/bin/CMakeFiles/TargetDirectories.txt": {
"mtime": 1781073898.5747914, "mtime": 1781100147.907912,
"ast_hash": "d104b31c9f5a42c9df45b3ecc805cce3", "ast_hash": "d104b31c9f5a42c9df45b3ecc805cce3",
"semantic_hash": "d104b31c9f5a42c9df45b3ecc805cce3" "semantic_hash": "d104b31c9f5a42c9df45b3ecc805cce3"
}, },
"/Users/chahinebrini/mono/rebreak-monorepo/apps/rebreak-native/android/app/.cxx/RelWithDebInfo/5m151t3n/x86_64/CMakeFiles/_CMakeLTOTest-CXX/src/CMakeLists.txt": { "/Users/chahinebrini/mono/rebreak-monorepo/apps/rebreak-native/android/app/.cxx/RelWithDebInfo/5m151t3n/x86_64/CMakeFiles/_CMakeLTOTest-CXX/src/CMakeLists.txt": {
"mtime": 1781073898.5588317, "mtime": 1781100147.8946912,
"ast_hash": "3b72b5cc3e6a0f6bfa3e24ca244eea7d", "ast_hash": "3b72b5cc3e6a0f6bfa3e24ca244eea7d",
"semantic_hash": "3b72b5cc3e6a0f6bfa3e24ca244eea7d" "semantic_hash": "3b72b5cc3e6a0f6bfa3e24ca244eea7d"
}, },
@ -7384,26 +7384,6 @@
"ast_hash": "6ccbb870b630233e41a6a4273b4672b0", "ast_hash": "6ccbb870b630233e41a6a4273b4672b0",
"semantic_hash": "6ccbb870b630233e41a6a4273b4672b0" "semantic_hash": "6ccbb870b630233e41a6a4273b4672b0"
}, },
"/Users/chahinebrini/mono/rebreak-monorepo/apps/rebreak-native/android/app/.cxx/RelWithDebInfo/5m151t3n/armeabi-v7a/.cmake/api/v1/reply/index-2026-06-10T06-43-56-0126.json": {
"mtime": 1781073836.1269782,
"ast_hash": "92e5e4513854a8ee33b8649d8a5a733a",
"semantic_hash": ""
},
"/Users/chahinebrini/mono/rebreak-monorepo/apps/rebreak-native/android/app/.cxx/RelWithDebInfo/5m151t3n/x86/.cmake/api/v1/reply/index-2026-06-10T06-44-26-0968.json": {
"mtime": 1781073866.9680228,
"ast_hash": "9e3cc2c421898601a78766628e7f425d",
"semantic_hash": ""
},
"/Users/chahinebrini/mono/rebreak-monorepo/apps/rebreak-native/android/app/.cxx/RelWithDebInfo/5m151t3n/arm64-v8a/.cmake/api/v1/reply/index-2026-06-10T06-43-20-0573.json": {
"mtime": 1781073800.5735383,
"ast_hash": "aadb4abeff66a8c956aa2a198fa11ae0",
"semantic_hash": ""
},
"/Users/chahinebrini/mono/rebreak-monorepo/apps/rebreak-native/android/app/.cxx/RelWithDebInfo/5m151t3n/x86_64/.cmake/api/v1/reply/index-2026-06-10T06-44-58-0859.json": {
"mtime": 1781073898.8597715,
"ast_hash": "a6ab1ed40e911ae57fed01928b2a8be7",
"semantic_hash": ""
},
"/Users/chahinebrini/mono/rebreak-monorepo/apps/rebreak-native/android/app/.cxx/Debug/51sk2e6z/armeabi-v7a/.cmake/api/v1/reply/cmakeFiles-v1-3bc501df0f7b3202d590.json": { "/Users/chahinebrini/mono/rebreak-monorepo/apps/rebreak-native/android/app/.cxx/Debug/51sk2e6z/armeabi-v7a/.cmake/api/v1/reply/cmakeFiles-v1-3bc501df0f7b3202d590.json": {
"mtime": 1781071634.0955763, "mtime": 1781071634.0955763,
"ast_hash": "508cc45db0022c80dbe37ad64f58d7e4", "ast_hash": "508cc45db0022c80dbe37ad64f58d7e4",
@ -7685,22 +7665,22 @@
"semantic_hash": "" "semantic_hash": ""
}, },
"/Users/chahinebrini/mono/rebreak-monorepo/apps/rebreak-native/ios/ReBreak/Images.xcassets/AppIcon.appiconset/App-Icon-1024x1024@1x.png": { "/Users/chahinebrini/mono/rebreak-monorepo/apps/rebreak-native/ios/ReBreak/Images.xcassets/AppIcon.appiconset/App-Icon-1024x1024@1x.png": {
"mtime": 1780886883.165441, "mtime": 1781099356.0136845,
"ast_hash": "f13c8cff3aa27d1021eac9e6124a73b4", "ast_hash": "f13c8cff3aa27d1021eac9e6124a73b4",
"semantic_hash": "" "semantic_hash": ""
}, },
"/Users/chahinebrini/mono/rebreak-monorepo/apps/rebreak-native/ios/ReBreak/Images.xcassets/SplashScreenLegacy.imageset/image@3x.png": { "/Users/chahinebrini/mono/rebreak-monorepo/apps/rebreak-native/ios/ReBreak/Images.xcassets/SplashScreenLegacy.imageset/image@3x.png": {
"mtime": 1780886883.1792867, "mtime": 1781099356.0190911,
"ast_hash": "06a3074fc25dc205cc2265a269958dd0", "ast_hash": "06a3074fc25dc205cc2265a269958dd0",
"semantic_hash": "" "semantic_hash": ""
}, },
"/Users/chahinebrini/mono/rebreak-monorepo/apps/rebreak-native/ios/ReBreak/Images.xcassets/SplashScreenLegacy.imageset/image@2x.png": { "/Users/chahinebrini/mono/rebreak-monorepo/apps/rebreak-native/ios/ReBreak/Images.xcassets/SplashScreenLegacy.imageset/image@2x.png": {
"mtime": 1780886883.1792939, "mtime": 1781099356.0190566,
"ast_hash": "06a3074fc25dc205cc2265a269958dd0", "ast_hash": "06a3074fc25dc205cc2265a269958dd0",
"semantic_hash": "" "semantic_hash": ""
}, },
"/Users/chahinebrini/mono/rebreak-monorepo/apps/rebreak-native/ios/ReBreak/Images.xcassets/SplashScreenLegacy.imageset/image.png": { "/Users/chahinebrini/mono/rebreak-monorepo/apps/rebreak-native/ios/ReBreak/Images.xcassets/SplashScreenLegacy.imageset/image.png": {
"mtime": 1780886883.1792905, "mtime": 1781099356.0190496,
"ast_hash": "06a3074fc25dc205cc2265a269958dd0", "ast_hash": "06a3074fc25dc205cc2265a269958dd0",
"semantic_hash": "" "semantic_hash": ""
}, },
@ -8413,5 +8393,25 @@
"mtime": 1780556379.4233892, "mtime": 1780556379.4233892,
"ast_hash": "64cbd4c2d284cbd433fd32523c90debc", "ast_hash": "64cbd4c2d284cbd433fd32523c90debc",
"semantic_hash": "" "semantic_hash": ""
},
"/Users/chahinebrini/mono/rebreak-monorepo/apps/rebreak-native/android/app/.cxx/RelWithDebInfo/5m151t3n/armeabi-v7a/.cmake/api/v1/reply/index-2026-06-10T14-01-24-0714.json": {
"mtime": 1781100084.7143128,
"ast_hash": "92e5e4513854a8ee33b8649d8a5a733a",
"semantic_hash": ""
},
"/Users/chahinebrini/mono/rebreak-monorepo/apps/rebreak-native/android/app/.cxx/RelWithDebInfo/5m151t3n/x86/.cmake/api/v1/reply/index-2026-06-10T14-01-57-0989.json": {
"mtime": 1781100117.9897835,
"ast_hash": "9e3cc2c421898601a78766628e7f425d",
"semantic_hash": ""
},
"/Users/chahinebrini/mono/rebreak-monorepo/apps/rebreak-native/android/app/.cxx/RelWithDebInfo/5m151t3n/arm64-v8a/.cmake/api/v1/reply/index-2026-06-10T14-00-51-0586.json": {
"mtime": 1781100051.586443,
"ast_hash": "aadb4abeff66a8c956aa2a198fa11ae0",
"semantic_hash": ""
},
"/Users/chahinebrini/mono/rebreak-monorepo/apps/rebreak-native/android/app/.cxx/RelWithDebInfo/5m151t3n/x86_64/.cmake/api/v1/reply/index-2026-06-10T14-02-28-0148.json": {
"mtime": 1781100148.1485403,
"ast_hash": "a6ab1ed40e911ae57fed01928b2a8be7",
"semantic_hash": ""
} }
} }

View File

@ -300,7 +300,7 @@ Rebreak ist bewusst **plattformübergreifend nativ**, nicht hybrid. Grund: der S
| Anbieter | Herkunft | Plattformen | DNS-/URL-Block | Mail-Schutz | KI-Coach | DE-Fokus | Lock-Modus | Preis (Monat) | | Anbieter | Herkunft | Plattformen | DNS-/URL-Block | Mail-Schutz | KI-Coach | DE-Fokus | Lock-Modus | Preis (Monat) |
|---|---|---|---|---|---|---|---|---| |---|---|---|---|---|---|---|---|---|
| **Rebreak** | DE | iOS, Android, **macOS** | ✅ (~330k Domains) | ✅ **IMAP-IDLE Echtzeit** | ✅ Lyra (DE) | ✅ | ✅ Rebreak Magic | 3,99 / 7,99 € | | **Rebreak** | DE | iOS, Android, **macOS** | ✅ (~330k Domains) | ✅ **IMAP-IDLE Echtzeit** | ✅ Lyra (DE) | ✅ | ✅ Rebreak Magic | 3,99 / 7,99 € |
| Gamban | UK | iOS, Android, Win, Mac | ✅ | ❌ | ❌ | teilweise EN | nur passwortgeschützt | ~3,75 € (£ 2,99 ähnl.) | | Gamban | UK | iOS, Android, Win, Mac | ✅ | ❌ | ❌ | teilweise EN | nur passwortgeschützt | 5,99 € (35,99 €/Jahr, Stand 2026) |
| BetBlocker | UK (Charity) | iOS, Android, Win, Mac | ✅ | ❌ | ❌ | nein | timer-basiert | kostenlos | | BetBlocker | UK (Charity) | iOS, Android, Win, Mac | ✅ | ❌ | ❌ | nein | timer-basiert | kostenlos |
| GamBlock | AU | Win, Mac, Android | ✅ | ❌ | ❌ | nein | stark | ~59 € | | GamBlock | AU | Win, Mac, Android | ✅ | ❌ | ❌ | nein | stark | ~59 € |
| Truple / Net Nanny | US | iOS, Android, Win | nur teils, Fokus Porn | ❌ | ❌ | nein | ja | ~10 € | | Truple / Net Nanny | US | iOS, Android, Win | nur teils, Fokus Porn | ❌ | ❌ | nein | ja | ~10 € |

16
pnpm-lock.yaml generated
View File

@ -45,7 +45,7 @@ importers:
version: 14.3.0(vue@3.5.34(typescript@5.9.3)) version: 14.3.0(vue@3.5.34(typescript@5.9.3))
'@vueuse/nuxt': '@vueuse/nuxt':
specifier: ^14.2.1 specifier: ^14.2.1
version: 14.3.0(magicast@0.5.3)(nuxt@4.1.3(@electric-sql/pglite@0.4.1)(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@parcel/watcher@2.5.6)(@types/node@22.19.17)(@vue/compiler-sfc@3.5.35)(cac@6.7.14)(db0@0.3.4(@electric-sql/pglite@0.4.1)(mysql2@3.15.3))(eslint@10.3.0(jiti@2.7.0))(ioredis@5.10.1)(lightningcss@1.32.0)(magicast@0.5.3)(mysql2@3.15.3)(optionator@0.9.4)(rollup@4.60.3)(terser@5.46.2)(typescript@5.9.3)(vite@7.3.3(@types/node@22.19.17)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.2)(yaml@2.8.4))(yaml@2.8.4))(vue@3.5.34(typescript@5.9.3)) version: 14.3.0(magicast@0.5.3)(nuxt@4.1.3(@electric-sql/pglite@0.4.1)(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@parcel/watcher@2.5.6)(@types/node@22.19.17)(@vue/compiler-sfc@3.5.35)(cac@6.7.14)(db0@0.3.4(@electric-sql/pglite@0.4.1)(mysql2@3.15.3))(eslint@10.3.0(jiti@2.7.0))(ioredis@5.10.1)(lightningcss@1.32.0)(magicast@0.5.3)(mysql2@3.15.3)(optionator@0.9.4)(rollup@4.60.3)(terser@5.46.2)(typescript@5.9.3)(yaml@2.8.4))(vue@3.5.34(typescript@5.9.3))
nuxt: nuxt:
specifier: 4.1.3 specifier: 4.1.3
version: 4.1.3(@electric-sql/pglite@0.4.1)(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@parcel/watcher@2.5.6)(@types/node@22.19.17)(@vue/compiler-sfc@3.5.35)(cac@6.7.14)(db0@0.3.4(@electric-sql/pglite@0.4.1)(mysql2@3.15.3))(eslint@10.3.0(jiti@2.7.0))(ioredis@5.10.1)(lightningcss@1.32.0)(magicast@0.5.3)(mysql2@3.15.3)(optionator@0.9.4)(rollup@4.60.3)(terser@5.46.2)(typescript@5.9.3)(vite@7.3.3(@types/node@22.19.17)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.2)(yaml@2.8.4))(yaml@2.8.4) version: 4.1.3(@electric-sql/pglite@0.4.1)(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@parcel/watcher@2.5.6)(@types/node@22.19.17)(@vue/compiler-sfc@3.5.35)(cac@6.7.14)(db0@0.3.4(@electric-sql/pglite@0.4.1)(mysql2@3.15.3))(eslint@10.3.0(jiti@2.7.0))(ioredis@5.10.1)(lightningcss@1.32.0)(magicast@0.5.3)(mysql2@3.15.3)(optionator@0.9.4)(rollup@4.60.3)(terser@5.46.2)(typescript@5.9.3)(vite@7.3.3(@types/node@22.19.17)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.2)(yaml@2.8.4))(yaml@2.8.4)
@ -94,7 +94,7 @@ importers:
version: 3.0.3(magicast@0.5.3)(vue@3.5.34(typescript@5.9.3)) version: 3.0.3(magicast@0.5.3)(vue@3.5.34(typescript@5.9.3))
'@vueuse/nuxt': '@vueuse/nuxt':
specifier: ^14.2.1 specifier: ^14.2.1
version: 14.3.0(magicast@0.5.3)(nuxt@4.1.3(@electric-sql/pglite@0.4.1)(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@parcel/watcher@2.5.6)(@types/node@22.19.17)(@vue/compiler-sfc@3.5.35)(cac@6.7.14)(db0@0.3.4(@electric-sql/pglite@0.4.1)(mysql2@3.15.3))(eslint@10.3.0(jiti@2.7.0))(ioredis@5.10.1)(lightningcss@1.32.0)(magicast@0.5.3)(mysql2@3.15.3)(optionator@0.9.4)(rollup@4.60.3)(terser@5.46.2)(typescript@5.9.3)(vite@7.3.3(@types/node@22.19.17)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.2)(yaml@2.8.4))(yaml@2.8.4))(vue@3.5.34(typescript@5.9.3)) version: 14.3.0(magicast@0.5.3)(nuxt@4.1.3(@electric-sql/pglite@0.4.1)(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@parcel/watcher@2.5.6)(@types/node@22.19.17)(@vue/compiler-sfc@3.5.35)(cac@6.7.14)(db0@0.3.4(@electric-sql/pglite@0.4.1)(mysql2@3.15.3))(eslint@10.3.0(jiti@2.7.0))(ioredis@5.10.1)(lightningcss@1.32.0)(magicast@0.5.3)(mysql2@3.15.3)(optionator@0.9.4)(rollup@4.60.3)(terser@5.46.2)(typescript@5.9.3)(yaml@2.8.4))(vue@3.5.34(typescript@5.9.3))
chart.js: chart.js:
specifier: ^4.5.1 specifier: ^4.5.1
version: 4.5.1 version: 4.5.1
@ -114,6 +114,9 @@ importers:
specifier: ^4.5.1 specifier: ^4.5.1
version: 4.6.4(vue@3.5.34(typescript@5.9.3)) version: 4.6.4(vue@3.5.34(typescript@5.9.3))
devDependencies: devDependencies:
'@iconify-json/simple-icons':
specifier: ^1.2.86
version: 1.2.86
'@nuxt/devtools': '@nuxt/devtools':
specifier: latest specifier: latest
version: 4.0.0-alpha.7(db0@0.3.4(@electric-sql/pglite@0.4.1)(mysql2@3.15.3))(ioredis@5.10.1)(typescript@5.9.3)(vite@7.3.3(@types/node@22.19.17)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.2)(yaml@2.8.4))(vue@3.5.34(typescript@5.9.3)) version: 4.0.0-alpha.7(db0@0.3.4(@electric-sql/pglite@0.4.1)(mysql2@3.15.3))(ioredis@5.10.1)(typescript@5.9.3)(vite@7.3.3(@types/node@22.19.17)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.2)(yaml@2.8.4))(vue@3.5.34(typescript@5.9.3))
@ -1879,6 +1882,9 @@ packages:
'@iconify-json/heroicons@1.2.3': '@iconify-json/heroicons@1.2.3':
resolution: {integrity: sha512-n+vmCEgTesRsOpp5AB5ILB6srsgsYK+bieoQBNlafvoEhjVXLq8nIGN4B0v/s4DUfa0dOrjwE/cKJgIKdJXOEg==} resolution: {integrity: sha512-n+vmCEgTesRsOpp5AB5ILB6srsgsYK+bieoQBNlafvoEhjVXLq8nIGN4B0v/s4DUfa0dOrjwE/cKJgIKdJXOEg==}
'@iconify-json/simple-icons@1.2.86':
resolution: {integrity: sha512-t3jck5qPQuK1qy+bRn9eCoDQhIB7XSazKz1Fjp8hcan3XOAsTI5Mq/s3F0ekOKSvMQqkVORYK6ns6o6T9f5EMA==}
'@iconify/collections@1.0.680': '@iconify/collections@1.0.680':
resolution: {integrity: sha512-zALf9JfXAKxExpb8bYA5Ds0cHgZceRMR1pkUiwLPa0Ctl8sfXw6Honrc/APpcLbTSyBaH1ZMnS4Y/2SdSPa4Pg==} resolution: {integrity: sha512-zALf9JfXAKxExpb8bYA5Ds0cHgZceRMR1pkUiwLPa0Ctl8sfXw6Honrc/APpcLbTSyBaH1ZMnS4Y/2SdSPa4Pg==}
@ -11434,6 +11440,10 @@ snapshots:
dependencies: dependencies:
'@iconify/types': 2.0.0 '@iconify/types': 2.0.0
'@iconify-json/simple-icons@1.2.86':
dependencies:
'@iconify/types': 2.0.0
'@iconify/collections@1.0.680': '@iconify/collections@1.0.680':
dependencies: dependencies:
'@iconify/types': 2.0.0 '@iconify/types': 2.0.0
@ -14610,7 +14620,7 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- magicast - magicast
'@vueuse/nuxt@14.3.0(magicast@0.5.3)(nuxt@4.1.3(@electric-sql/pglite@0.4.1)(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@parcel/watcher@2.5.6)(@types/node@22.19.17)(@vue/compiler-sfc@3.5.35)(cac@6.7.14)(db0@0.3.4(@electric-sql/pglite@0.4.1)(mysql2@3.15.3))(eslint@10.3.0(jiti@2.7.0))(ioredis@5.10.1)(lightningcss@1.32.0)(magicast@0.5.3)(mysql2@3.15.3)(optionator@0.9.4)(rollup@4.60.3)(terser@5.46.2)(typescript@5.9.3)(vite@7.3.3(@types/node@22.19.17)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.2)(yaml@2.8.4))(yaml@2.8.4))(vue@3.5.34(typescript@5.9.3))': '@vueuse/nuxt@14.3.0(magicast@0.5.3)(nuxt@4.1.3(@electric-sql/pglite@0.4.1)(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@parcel/watcher@2.5.6)(@types/node@22.19.17)(@vue/compiler-sfc@3.5.35)(cac@6.7.14)(db0@0.3.4(@electric-sql/pglite@0.4.1)(mysql2@3.15.3))(eslint@10.3.0(jiti@2.7.0))(ioredis@5.10.1)(lightningcss@1.32.0)(magicast@0.5.3)(mysql2@3.15.3)(optionator@0.9.4)(rollup@4.60.3)(terser@5.46.2)(typescript@5.9.3)(yaml@2.8.4))(vue@3.5.34(typescript@5.9.3))':
dependencies: dependencies:
'@nuxt/kit': 4.4.4(magicast@0.5.3) '@nuxt/kit': 4.4.4(magicast@0.5.3)
'@vueuse/core': 14.3.0(vue@3.5.34(typescript@5.9.3)) '@vueuse/core': 14.3.0(vue@3.5.34(typescript@5.9.3))