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:
parent
df3c4fafa3
commit
63fae25531
3
.gitignore
vendored
3
.gitignore
vendored
@ -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/
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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({
|
||||||
|
|||||||
@ -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({
|
||||||
|
|||||||
@ -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 & Legend
|
Beta, für Pro & 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({
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
@ -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"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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)
|
||||||
@ -84,7 +95,7 @@ struct MacRegistrationView: View {
|
|||||||
Button("✓ Fertig — zurück zur Übersicht") { model.returnToHub() }
|
Button("✓ Fertig — zurück zur Übersicht") { model.returnToHub() }
|
||||||
.buttonStyle(.borderedProminent)
|
.buttonStyle(.borderedProminent)
|
||||||
}
|
}
|
||||||
|
|
||||||
if isRegistering || isInstallingProfile {
|
if isRegistering || isInstallingProfile {
|
||||||
ProgressView()
|
ProgressView()
|
||||||
.controlSize(.small)
|
.controlSize(.small)
|
||||||
@ -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) {
|
||||||
@ -211,31 +236,94 @@ struct MacRegistrationView: View {
|
|||||||
errorMessage = "Keine Registrierung vorhanden. Bitte zuerst registrieren."
|
errorMessage = "Keine Registrierung vorhanden. Bitte zuerst registrieren."
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
Task {
|
Task {
|
||||||
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 {
|
||||||
|
|||||||
4
apps/rebreak-native/.gitignore
vendored
4
apps/rebreak-native/.gitignore
vendored
@ -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*
|
||||||
|
|
||||||
|
|||||||
@ -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:
|
||||||
|
|
||||||
|
|||||||
101
apps/rebreak-native/.maestro/blocker/activation-smoke.yaml
Normal file
101
apps/rebreak-native/.maestro/blocker/activation-smoke.yaml
Normal 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"
|
||||||
@ -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 hab’s 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"
|
||||||
117
apps/rebreak-native/.maestro/calls/incoming-call-screen.yaml
Normal file
117
apps/rebreak-native/.maestro/calls/incoming-call-screen.yaml
Normal 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"
|
||||||
84
apps/rebreak-native/.maestro/dm/realtime-receive.yaml
Normal file
84
apps/rebreak-native/.maestro/dm/realtime-receive.yaml
Normal 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]"
|
||||||
102
apps/rebreak-native/.maestro/dm/send-message.yaml
Normal file
102
apps/rebreak-native/.maestro/dm/send-message.yaml
Normal 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…"
|
||||||
@ -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.
|
||||||
36
apps/rebreak-native/.maestro/screens/01-onboarding.yaml
Normal file
36
apps/rebreak-native/.maestro/screens/01-onboarding.yaml
Normal 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
|
||||||
56
apps/rebreak-native/.maestro/screens/02-blocker.yaml
Normal file
56
apps/rebreak-native/.maestro/screens/02-blocker.yaml
Normal 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
|
||||||
65
apps/rebreak-native/.maestro/screens/03-blocked.yaml
Normal file
65
apps/rebreak-native/.maestro/screens/03-blocked.yaml
Normal 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
|
||||||
81
apps/rebreak-native/.maestro/screens/04-sos-lyra.yaml
Normal file
81
apps/rebreak-native/.maestro/screens/04-sos-lyra.yaml
Normal 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 = 6–12s. 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
|
||||||
75
apps/rebreak-native/.maestro/screens/05-breathing.yaml
Normal file
75
apps/rebreak-native/.maestro/screens/05-breathing.yaml
Normal 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
|
||||||
54
apps/rebreak-native/.maestro/screens/06-mail-schutz.yaml
Normal file
54
apps/rebreak-native/.maestro/screens/06-mail-schutz.yaml
Normal 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
|
||||||
60
apps/rebreak-native/.maestro/screens/07-community.yaml
Normal file
60
apps/rebreak-native/.maestro/screens/07-community.yaml
Normal 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
|
||||||
71
apps/rebreak-native/.maestro/screens/08-streak.yaml
Normal file
71
apps/rebreak-native/.maestro/screens/08-streak.yaml
Normal 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
|
||||||
81
apps/rebreak-native/.maestro/screens/09-geraete.yaml
Normal file
81
apps/rebreak-native/.maestro/screens/09-geraete.yaml
Normal 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
|
||||||
@ -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
|
||||||
@ -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
|
||||||
77
apps/rebreak-native/.maestro/screens/capture-marketing-loggedin.sh
Executable file
77
apps/rebreak-native/.maestro/screens/capture-marketing-loggedin.sh
Executable 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 02–09 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"
|
||||||
183
apps/rebreak-native/.maestro/screens/capture-marketing.sh
Executable file
183
apps/rebreak-native/.maestro/screens/capture-marketing.sh
Executable 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
|
||||||
82
apps/rebreak-native/.maestro/screens/capture-onboarding.yaml
Normal file
82
apps/rebreak-native/.maestro/screens/capture-onboarding.yaml
Normal 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
|
||||||
@ -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
|
||||||
340
apps/rebreak-native/.maestro/screens/marketing-tour.yaml
Normal file
340
apps/rebreak-native/.maestro/screens/marketing-tour.yaml
Normal 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
|
||||||
119
apps/rebreak-native/.maestro/sos/crisis-flow.yaml
Normal file
119
apps/rebreak-native/.maestro/sos/crisis-flow.yaml
Normal 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 6–12s 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) = 6–12s. 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"
|
||||||
160
apps/rebreak-native/.maestro/stress/dm-scroll-performance.yaml
Normal file
160
apps/rebreak-native/.maestro/stress/dm-scroll-performance.yaml
Normal 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: 5–10s 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"
|
||||||
105
apps/rebreak-native/.maestro/stress/rapid-post-submit.yaml
Normal file
105
apps/rebreak-native/.maestro/stress/rapid-post-submit.yaml
Normal 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 ~1–3s 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."
|
||||||
65
apps/rebreak-native/CALLS_DEBUG_STATE.md
Normal file
65
apps/rebreak-native/CALLS_DEBUG_STATE.md
Normal 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.
|
||||||
@ -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
|
||||||
|
|||||||
@ -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 14–16).** 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.
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -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} → "{wanted}"
|
||||||
|
{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));
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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):
|
||||||
|
|||||||
@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
14
apps/rebreak-native/assets/DEPRECATED.md
Normal file
14
apps/rebreak-native/assets/DEPRECATED.md
Normal 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
|
||||||
BIN
apps/rebreak-native/assets/devices/android.png
Normal file
BIN
apps/rebreak-native/assets/devices/android.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
BIN
apps/rebreak-native/assets/devices/computer.png
Normal file
BIN
apps/rebreak-native/assets/devices/computer.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.4 KiB |
BIN
apps/rebreak-native/assets/devices/iphone.png
Normal file
BIN
apps/rebreak-native/assets/devices/iphone.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.3 KiB |
BIN
apps/rebreak-native/assets/devices/laptop.png
Normal file
BIN
apps/rebreak-native/assets/devices/laptop.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.7 KiB |
BIN
apps/rebreak-native/assets/devices/macbook.png
Normal file
BIN
apps/rebreak-native/assets/devices/macbook.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.0 KiB |
BIN
apps/rebreak-native/assets/devices/tablet.png
Normal file
BIN
apps/rebreak-native/assets/devices/tablet.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.6 KiB |
112
apps/rebreak-native/clean.sh
Executable file
112
apps/rebreak-native/clean.sh
Executable 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
|
||||||
@ -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}
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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}
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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 }}>
|
||||||
|
|||||||
@ -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)
|
||||||
# ═══════════════════════════════════════════════════════════════════════════
|
# ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
|||||||
@ -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 "$@"
|
||||||
|
|||||||
@ -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(() => {
|
||||||
|
|||||||
@ -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(() => {
|
||||||
|
|||||||
@ -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 () => {
|
||||||
|
|||||||
@ -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';
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
|
|
||||||
|
|||||||
19
apps/rebreak-native/lib/realtimeStatus.ts
Normal file
19
apps/rebreak-native/lib/realtimeStatus.ts
Normal 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";
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
|||||||
@ -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": {
|
||||||
|
|||||||
@ -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": {
|
||||||
|
|||||||
@ -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": "Samsung’s accessibility menu is fiddly. So I can guide you step by step, give ReBreak “Usage access” once — I’ll 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 — I’ll 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 — I’ll 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 — I’ll 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 isn’t active yet. You can still continue and set it up anytime under “Protection”.",
|
||||||
|
"confirm": "Set up later"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"protection_onboarding": {
|
"protection_onboarding": {
|
||||||
|
|||||||
@ -580,9 +580,15 @@
|
|||||||
"a11y_indicator": "ReBreak te guide pas à pas",
|
"a11y_indicator": "ReBreak te guide pas à pas",
|
||||||
"a11y_step3": "Active l’interrupteur, confirme la boîte de dialogue — puis reviens dans l’app.",
|
"a11y_step3": "Active l’interrupteur, confirme la boîte de dialogue — puis reviens dans l’app.",
|
||||||
"usage_title": "Une fois : activer le guidage pas à pas",
|
"usage_title": "Une fois : activer le guidage pas à pas",
|
||||||
"usage_body": "Le menu d’accessibilité de Samsung est pénible. Pour te guider étape par étape, donne à ReBreak l’« accès aux données d’usage » une fois — j’ouvre la page. Active ReBreak là, reviens et touche à nouveau la protection.",
|
"usage_body": "Le menu d’accessibilité est parfois pénible. Pour te guider étape par étape, donne à ReBreak l’« accès aux données d’usage » une fois — j’ouvre la page. Active ReBreak là, reviens et touche à nouveau la protection.",
|
||||||
"overlay_title": "Une fois : autoriser l’overlay d’aide",
|
"overlay_title": "Une fois : autoriser l’overlay d’aide",
|
||||||
"overlay_body": "Pour que mon indication s’affiche devant les Réglages (au lieu d’être cachée dans le tiroir), donne à ReBreak « Superposition à d’autres applis » une fois — j’ouvre la page. Active ReBreak là, reviens et touche à nouveau la protection."
|
"overlay_body": "Pour que mon indication s’affiche devant les Réglages (au lieu d’être cachée dans le tiroir), donne à ReBreak « Superposition à d’autres applis » une fois — j’ouvre 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 n’est pas encore active. Tu peux continuer et la configurer à tout moment sous « Protection ».",
|
||||||
|
"confirm": "Plus tard"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"mail": {
|
"mail": {
|
||||||
|
|||||||
@ -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"),
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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 }>;
|
||||||
|
|
||||||
|
|||||||
@ -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": {
|
||||||
|
|||||||
@ -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' } }],
|
||||||
|
|||||||
@ -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(() => {
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
90
docs/internal/SESSION_2026-06-07_handoff.md
Normal file
90
docs/internal/SESSION_2026-06-07_handoff.md
Normal 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.
|
||||||
69
docs/internal/SESSION_2026-06-08_handoff.md
Normal file
69
docs/internal/SESSION_2026-06-08_handoff.md
Normal 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.
|
||||||
@ -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.
|
||||||
97
docs/marketing/ilona-vorstand-mail.md
Normal file
97
docs/marketing/ilona-vorstand-mail.md
Normal 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.
|
||||||
@ -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
@ -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": ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -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 | ~5–9 € |
|
| GamBlock | AU | Win, Mac, Android | ✅ | ❌ | ❌ | nein | stark | ~5–9 € |
|
||||||
| 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
16
pnpm-lock.yaml
generated
@ -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))
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user