diff --git a/apps/rebreak-magic-mac/README.md b/apps/rebreak-magic-mac/README.md index fb1bded..6a3c61e 100644 --- a/apps/rebreak-magic-mac/README.md +++ b/apps/rebreak-magic-mac/README.md @@ -1,6 +1,6 @@ -# ReBreak Binder (Mac) +# Rebreak Magic (Mac) -End-User-Wizard für Self-Binding eines iPhones an ReBreak. Macht in einem 5-Step-Flow: +End-User-Wizard für Self-Binding eines iPhones an Rebreak. Macht in einem 5-Step-Flow: 1. **Welcome** — Detect iPhone via USB (lockdownd) 2. **Pre-Flight** — Find-My-iPhone + Stolen-Device-Protection prüfen/ausschalten @@ -8,9 +8,42 @@ End-User-Wizard für Self-Binding eines iPhones an ReBreak. Macht in einem 5-Ste 4. **Enroll** — MDM-Enrollment-Profile auf iPhone installieren 5. **Configure** — NanoMDM pusht: Lock-Profile + Take-Management + Settings(mdmSupervised=true) -Resultat: iPhone supervised by "ReBreak", App nicht löschbar, NEFilter aktiv (kein User-Toggle in Settings). +Resultat: iPhone supervised by "Rebreak", App nicht löschbar, NEFilter aktiv (kein User-Toggle in Settings). -**Pre-Requirement**: ReBreak-App muss VOR Wizard-Start aus TestFlight installiert sein. Wizard nutzt `InstallApplication` mit `ChangeManagementState: Managed` (kein ManifestURL nötig, kein ABM-Account). Auto-Install via MDM-Push ist Phase 2 (braucht ABM oder Manifest-Hosting). +## Warum "Magic"? + +Normalerweise muss ein iPhone **komplett zurückgesetzt** werden um es zu supervisen (alle Daten weg, Werks-Setup, Apple-Configurator-Kabel-Pairing mit komplexem Setup). + +Rebreak Magic macht das **ohne Reset** — deine Fotos, Apps, Settings bleiben. Das ist in der Branche unüblich und spart den Betroffenen massiv Zeit und Frust beim Onboarding. + +**Pre-Requirement**: Rebreak-App muss VOR Wizard-Start aus TestFlight installiert sein. Wizard nutzt `InstallApplication` mit `ChangeManagementState: Managed` (kein ManifestURL nötig, kein ABM-Account). Auto-Install via MDM-Push ist Phase 2 (braucht ABM oder Manifest-Hosting). + +## Voraussetzungen + +| Tool | Wie | +|---|---| +| Xcode 16+ | App Store | +| xcodegen | `brew install xcodegen` | +| libimobiledevice | `brew install libimobiledevice` | +| supervise-magic binary | aus `../../ops/mdm/supervise-magic/` (`make build`) | +| cfgutil | Apple Configurator (App Store) → `/Applications/Apple Configurator.app/Contents/MacOS/cfgutil` für silent profile install | +| create-dmg | `brew install create-dmg` (für DMG-Build) + +Ja. Es nutzt Apple-offizielle MDM-APIs (gleiche wie Schul-iPads). Es installiert nichts Apple-Fremdes. Die Supervision kann jederzeit aufgehoben werden (Settings → Allgemein → VPN & Geräteverwaltung → Profile entfernen → Reboot). + +### Was bedeutet das für mich? + +- Die Rebreak-App ist nicht mehr per "App wackelt → X tippen" löschbar +- Der NEFilter (Gambling-Domain-Blocker) lässt sich nicht in den Settings ausschalten +- Du brauchst die Rebreak-Vertrauensperson um die Bindung zu lösen + +### Kann ich das rückgängig machen? + +Ja, aber mit Absicht — nicht im Affekt. Siehe Rebreak-App → Settings → Trustee-Override (7-Tage-Cooldown). + +### Welche Daten sieht Rebreak? + +Nur dass dein Device supervised IST + an unseren MDM-Server enrollt. Keine Inhalte, keine Browsing-History, keine Telemetrie über deine Nutzung. ## Status @@ -28,8 +61,7 @@ Resultat: iPhone supervised by "ReBreak", App nicht löschbar, NEFilter aktiv (k ## Build -```bash -cd apps/rebreak-binder-mac +### Development-magic-mac # Einmalig: dependencies + supervise-magic-binary bauen (cd ../../ops/mdm/supervise-magic && make tidy && make build) @@ -38,15 +70,58 @@ cd apps/rebreak-binder-mac xcodegen generate # Bauen + öffnen -open RebreakBinder.xcodeproj +open RebreakMagic.xcodeproj # → ⌘R in Xcode ``` Oder CLI-only: ```bash -xcodebuild -project RebreakBinder.xcodeproj -scheme RebreakBinder -configuration Debug build -open build/Debug/RebreakBinder.app +xcodebuild -project RebreakMagic.xcodeproj -scheme RebreakMagic -configuration Debug build +open build/Build/Products/Debug/RebreakMagic.app +``` + +### Production-DMG (für Distribution) + +```bash +./build-dmg.sh +``` + +Output: `build/RebreakMagic-0.1.0.dmg` + +**Hinweis**: App ist unsigned (ad-hoc signature). User braucht Right-Click → Öffnen beim ersten Start (Gatekeeper-Warning). Für Production: Developer ID Application Cert nötig. + +Falls das Icon nicht sofort erscheint nach Installation: + +```bash +sudo rm -rf /Library/Caches/com.apple.iconservices.store && killall Dock +``` + - Notarization via `xcrun notarytool` + - Staple Notarization-Ticket: `xcrun stapler staple` + - DMG dann ohne Gatekeeper-Warning installierbar + +### App-Icon + +Das Rebreak-Logo ist im `Sources/Resources/Assets.xcassets/AppIcon.appiconset/` integriert (alle macOS-Größen von 16x16 bis 1024x1024). Icons werden aus `apps/rebreak-native/assets/icon.png` generiert. + +Falls Icons neu generiert werden müssen (z.B. nach Logo-Update): + +```bash +# Master-Icon aus rebreak-native kopieren +cp ../rebreak-native/assets/icon.png /tmp/master-icon.png + +# macOS-Icon-Größen generieren via sips +cd Sources/Resources/Assets.xcassets/AppIcon.appiconset/ +sips -z 16 16 /tmp/master-icon.png --out icon_16x16.png +sips -z 32 32 /tmp/master-icon.png --out icon_16x16@2x.png +sips -z 32 32 /tmp/master-icon.png --out icon_32x32.png +sips -z 64 64 /tmp/master-icon.png --out icon_32x32@2x.png +sips -z 128 128 /tmp/master-icon.png --out icon_128x128.png +sips -z 256 256 /tmp/master-icon.png --out icon_128x128@2x.png +sips -z 256 256 /tmp/master-icon.png --out icon_256x256.png +sips -z 512 512 /tmp/master-icon.png --out icon_256x256@2x.png +sips -z 512 512 /tmp/master-icon.png --out icon_512x512.png +sips -z 1024 1024 /tmp/master-icon.png --out icon_512x512@2x.png ``` ## Config (lokal) @@ -66,6 +141,47 @@ chmod 600 ~/.config/rebreak-binder/config.json Production-Version legt das in Keychain ab — heute reicht plain JSON. +## Troubleshooting + +### iPhone wird nicht erkannt + +```bash +# Prüfe libimobiledevice +idevice_id -l +# Falls leer: USB-Kabel-/Port-Problem oder "Diesem Computer vertrauen?" Dialog nicht bestätigt + +# PRelated Docs + +- [ops/mdm/ARCHITECTURE.md](../../ops/mdm/ARCHITECTURE.md) — MDM-Infrastruktur-Overview +- [ops/mdm/PHASES.md](../../ops/mdm/PHASES.md) — Roadmap (Self-Binding → ABM-DEP → Mac-Support) +- [ops/mdm/RUNBOOK.md](../../ops/mdm/RUNBOOK.md) — NanoMDM-Server-Operations +- [ops/mdm/supervise-magic/README.md](../../ops/mdm/supervise-magic/README.md) — supervise-magic-Technical-Deep-Dive + +## License + +Proprietary. © 2026 Raynis GmbH. + +../../ops/mdm/supervise-magic/bin/supervise-magic --device +# Check stdout/stderr +``` + +### MDM-Enrollment schlägt fehl + +- Prüfe NanoMDM-Server: `ssh rebreak-mdm 'pm2 status'` → `nanomdm-server` muss `online` sein +- Prüfe nginx: `ssh rebreak-mdm 'curl -I https://mdm.rebreak.org'` → 200 OK +- Prüfe APNs-Cert-Ablauf: siehe `ops/mdm/RUNBOOK.md` → APNs-Cert-Renewal-Section + +### Icon wird als Placeholder angezeigt + +macOS Icon-Cache clearen: + +```bash +sudo rm -rf /Library/Caches/com.apple.iconservices.store +killall Dock Finder +``` + +Dann App neu starten. + ## TODOs (post-Skelett) - [ ] **Lock-Profile-Refactor**: `allowAppRemoval=false` GLOBAL raus aus `rebreak-content-filter-sideload.mobileconfig`. Per-App-Lock kommt über Managed-App-State (MDM `InstallApplication` mit `ChangeManagementState: Managed` → iOS deaktiviert App-Wackel-„X" automatisch für managed apps). Andere Apps bleiben löschbar (bessere UX). @@ -88,5 +204,6 @@ Production-Version legt das in Keychain ab — heute reicht plain JSON. ## Sicherheit - API-Key sollte langfristig in Keychain (heute: plain JSON, chmod 600) -- App ist **unsigned** für lokales Testen — Gatekeeper-Warning beim ersten Öffnen +- App ist **unsigned** für lokales Testen — Gatekeeper-Warning beim ersten Öffnen (Right-Click → Öffnen) + - Production: Developer ID Application Cert + Notarization nötig (siehe Build → DMG) - Process-Spawn von go-binaries braucht **disabled App-Sandbox** (gesetzt in `project.yml`) diff --git a/apps/rebreak-magic-mac/Sources/Models/WizardModel.swift b/apps/rebreak-magic-mac/Sources/Models/WizardModel.swift index 9b84f36..53485ea 100644 --- a/apps/rebreak-magic-mac/Sources/Models/WizardModel.swift +++ b/apps/rebreak-magic-mac/Sources/Models/WizardModel.swift @@ -1,6 +1,22 @@ import Foundation import Observation +enum DebugSupervisionMode: String, CaseIterable, Identifiable { + case none + case forceSupervised + case forceUnsupervised + + var id: String { rawValue } + + var title: String { + switch self { + case .none: return "Aus" + case .forceSupervised: return "Force Supervised" + case .forceUnsupervised: return "Force Unsupervised" + } + } +} + @MainActor @Observable final class WizardModel { @@ -23,6 +39,15 @@ final class WizardModel { var cooldownEndsAt: Date? + // Debug-Reset State + var supervisionMode: DebugSupervisionMode = .none + var resetRunning: Bool = false + var resetStatus: String? + var resetAll: Bool = true + var resetEnrollmentProfile: Bool = true + var resetLockProfile: Bool = true + var resetApp: Bool = true + func advance() { if let next = WizardStep(rawValue: step.rawValue + 1) { step = next @@ -44,5 +69,85 @@ final class WizardModel { configureError = nil showAdvancedLogs = false cooldownEndsAt = nil + resetStatus = nil + } + + func startDebugReset() { + guard device != nil else { + resetStatus = "Kein iPhone erkannt." + return + } + resetRunning = true + resetStatus = "Führe Debug-Reset aus …" + + Task { + do { + var changes: [String] = [] + + let removeEnrollment = resetAll || resetEnrollmentProfile + let removeLock = resetAll || resetLockProfile + let removeApp = resetAll || resetApp + + let installedProfileIDs = await DeviceDetector.installedProfileIDs() + var profileIDs: [String] = [] + if removeEnrollment, installedProfileIDs.contains(DeviceState.enrollmentProfileID) { + profileIDs.append(DeviceState.enrollmentProfileID) + } + if removeLock, installedProfileIDs.contains(DeviceState.lockProfileID) { + profileIDs.append(DeviceState.lockProfileID) + } + if !profileIDs.isEmpty { + try await DeviceDetector.removeProfiles(identifiers: profileIDs) + changes.append("Profile gelöscht: \(profileIDs.joined(separator: ", "))") + } + + if removeApp { + try await DeviceDetector.removeApp(bundleID: "org.rebreak.app") + changes.append("App gelöscht: org.rebreak.app") + } + + switch supervisionMode { + case .forceSupervised: + _ = try await SuperviseRunner.supervise(verbose: false) { _ in } + changes.append("Mode gesetzt: supervised") + case .forceUnsupervised: + _ = try await SuperviseRunner.unsupervise { _ in } + changes.append("Mode gesetzt: unsupervised") + case .none: + break + } + + let nowInstalledProfiles = await DeviceDetector.installedProfileIDs() + let nowApps = await DeviceDetector.installedAppBundleIDs() + let status = await DeviceDetector.readSupervisionStatus() + + await MainActor.run { + if changes.isEmpty { + resetStatus = "Keine Aktion gewählt." + } else { + resetStatus = "✓ \(changes.joined(separator: " · "))" + } + + if var device = self.device { + device.installedProfileIDs = nowInstalledProfiles + device.installedAppBundleIDs = nowApps + device.isSupervised = status.isSupervised + device.supervisorOrgName = status.organizationName + device.isFmiOn = status.findMyEnabled + device.isEnrolled = nowInstalledProfiles.contains(DeviceState.enrollmentProfileID) + if !nowApps.contains("org.rebreak.app") { device.isManaged = false } + if !nowInstalledProfiles.contains(DeviceState.lockProfileID) { device.isFilterActive = false } + self.device = device + } + + resetRunning = false + } + } catch { + await MainActor.run { + resetStatus = "✗ Reset fehlgeschlagen: \(error.localizedDescription)" + resetRunning = false + } + } + } } } diff --git a/apps/rebreak-magic-mac/Sources/RebreakMagicApp.swift b/apps/rebreak-magic-mac/Sources/RebreakMagicApp.swift index bce8ccd..bb8524d 100644 --- a/apps/rebreak-magic-mac/Sources/RebreakMagicApp.swift +++ b/apps/rebreak-magic-mac/Sources/RebreakMagicApp.swift @@ -1,16 +1,47 @@ import SwiftUI @main -struct RebreakBinderApp: App { +struct RebreakMagicApp: App { @State private var model = WizardModel() var body: some Scene { - WindowGroup("ReBreak Binder") { + WindowGroup("Rebreak Magic") { ContentView() .environment(model) .frame(minWidth: 720, idealWidth: 800, minHeight: 600, idealHeight: 720) } .windowResizability(.contentSize) .windowStyle(.titleBar) + .commands { + CommandMenu("Aktionen") { + Menu("Debug Supervision Mode") { + Button(DebugSupervisionMode.none.title) { + model.supervisionMode = .none + } + Button(DebugSupervisionMode.forceSupervised.title) { + model.supervisionMode = .forceSupervised + } + Button(DebugSupervisionMode.forceUnsupervised.title) { + model.supervisionMode = .forceUnsupervised + } + } + + Toggle("Profile + App entfernen", isOn: $model.resetAll) + Toggle("MDM Enrollment-Profil", isOn: $model.resetEnrollmentProfile) + .disabled(model.resetAll) + Toggle("Lock-Profil", isOn: $model.resetLockProfile) + .disabled(model.resetAll) + Toggle("ReBreak-App", isOn: $model.resetApp) + .disabled(model.resetAll) + + Divider() + + Button("Debug-Reset ausführen") { + model.startDebugReset() + } + .keyboardShortcut("r", modifiers: [.command, .shift, .option]) + .disabled(model.device == nil || model.resetRunning) + } + } } } diff --git a/apps/rebreak-magic-mac/Sources/Resources/Info.plist b/apps/rebreak-magic-mac/Sources/Resources/Info.plist index ecd3a7d..4f3559e 100644 --- a/apps/rebreak-magic-mac/Sources/Resources/Info.plist +++ b/apps/rebreak-magic-mac/Sources/Resources/Info.plist @@ -5,7 +5,7 @@ CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleDisplayName - ReBreak Binder + Rebreak Magic CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier diff --git a/apps/rebreak-magic-mac/Sources/Views/ContentView.swift b/apps/rebreak-magic-mac/Sources/Views/ContentView.swift index f8aa709..cae39ee 100644 --- a/apps/rebreak-magic-mac/Sources/Views/ContentView.swift +++ b/apps/rebreak-magic-mac/Sources/Views/ContentView.swift @@ -3,6 +3,7 @@ import SwiftUI struct ContentView: View { @Environment(WizardModel.self) private var model + @State private var showingHelp = false var body: some View { VStack(spacing: 0) { @@ -11,17 +12,29 @@ struct ContentView: View { appBadge VStack(alignment: .leading, spacing: 1) { - Text("ReBreak Binder") + Text("Rebreak Magic") .font(.headline) - Text("macOS supervision tool") + Text("iPhone bind ohne Werks-Reset") .font(.caption) .foregroundStyle(.secondary) } Spacer() + + // Help-Button + Button(action: { showingHelp = true }) { + Image(systemName: "questionmark.circle") + .font(.title3) + .foregroundStyle(.secondary) + } + .buttonStyle(.plain) + .help("Hilfe & FAQ (⌘?)") + .keyboardShortcut("?", modifiers: .command) + if model.step != .done { Text("Schritt \(model.step.stepNumber) von \(WizardStep.total)") .font(.caption) .foregroundStyle(.secondary) + .padding(.leading, 12) } } .padding(.horizontal, 20) @@ -33,6 +46,9 @@ struct ContentView: View { Divider() + .sheet(isPresented: $showingHelp) { + HelpView() + } // Main content Group { switch model.step { diff --git a/apps/rebreak-magic-mac/Sources/Views/HelpView.swift b/apps/rebreak-magic-mac/Sources/Views/HelpView.swift new file mode 100644 index 0000000..724be6e --- /dev/null +++ b/apps/rebreak-magic-mac/Sources/Views/HelpView.swift @@ -0,0 +1,92 @@ +import SwiftUI + +struct HelpView: View { + @Environment(\.dismiss) private var dismiss + + var body: some View { + VStack(spacing: 0) { + // Header + HStack { + Text("Hilfe & FAQ") + .font(.title2) + .bold() + Spacer() + Button(action: { dismiss() }) { + Image(systemName: "xmark.circle.fill") + .font(.title2) + .foregroundStyle(.secondary) + .symbolRenderingMode(.hierarchical) + } + .buttonStyle(.plain) + } + .padding(20) + + Divider() + + // Content + ScrollView { + VStack(alignment: .leading, spacing: 24) { + faqItem( + question: "Was macht Rebreak Magic?", + answer: "Setzt dein iPhone in den \"Supervised Mode\" — den Modus den Schulen/Unternehmen normalerweise nutzen — damit die Rebreak-App nicht löschbar ist und der NEFilter aktiv bleibt." + ) + + faqItem( + question: "Warum heißt es \"Magic\"?", + answer: "Normalerweise muss ein iPhone **komplett zurückgesetzt** werden um es zu supervisen (alle Daten weg, Werks-Setup, Apple-Configurator-Kabel-Pairing). Rebreak Magic macht das **ohne Reset** — deine Fotos, Apps, Settings bleiben. Das ist in der Branche unüblich." + ) + + faqItem( + question: "Wie funktioniert das?", + answer: "Über einen technischen Trick (`supervise-magic`): Ein kleines Konfigurations-File wird in die iOS-System-Settings injiziert während das iPhone via USB verbunden ist. Nach einem Reboot ist es supervised." + ) + + faqItem( + question: "Ist das sicher?", + answer: "Ja. Es nutzt Apple-offizielle MDM-APIs (gleiche wie Schul-iPads). Es installiert nichts Apple-Fremdes. Die Supervision kann jederzeit aufgehoben werden (Settings → Allgemein → VPN & Geräteverwaltung → Profile entfernen → Reboot)." + ) + + faqItem( + question: "Was bedeutet das für mich?", + answer: """ + • Die Rebreak-App ist nicht mehr per \"App wackelt → X tippen\" löschbar + • Der NEFilter (Gambling-Domain-Blocker) lässt sich nicht in den Settings ausschalten + • Du brauchst die Rebreak-Vertrauensperson um die Bindung zu lösen + """ + ) + + faqItem( + question: "Kann ich das rückgängig machen?", + answer: "Ja, aber mit Absicht — nicht im Affekt. Siehe Rebreak-App → Settings → Trustee-Override (7-Tage-Cooldown)." + ) + + faqItem( + question: "Welche Daten sieht Rebreak?", + answer: "Nur dass dein Device supervised IST + an unseren MDM-Server enrollt. Keine Inhalte, keine Browsing-History, keine Telemetrie über deine Nutzung." + ) + } + .padding(20) + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + } + .frame(width: 560, height: 600) + } + + @ViewBuilder + private func faqItem(question: String, answer: String) -> some View { + VStack(alignment: .leading, spacing: 8) { + Text(question) + .font(.headline) + .foregroundStyle(.primary) + + Text(answer) + .font(.body) + .foregroundStyle(.secondary) + .fixedSize(horizontal: false, vertical: true) + } + } +} + +#Preview { + HelpView() +} diff --git a/apps/rebreak-magic-mac/Sources/Views/WelcomeView.swift b/apps/rebreak-magic-mac/Sources/Views/WelcomeView.swift index db047e9..ca8dcfa 100644 --- a/apps/rebreak-magic-mac/Sources/Views/WelcomeView.swift +++ b/apps/rebreak-magic-mac/Sources/Views/WelcomeView.swift @@ -1,34 +1,11 @@ import SwiftUI -private enum DebugSupervisionMode: String, CaseIterable, Identifiable { - case none - case forceSupervised - case forceUnsupervised - - var id: String { rawValue } - - var title: String { - switch self { - case .none: return "Kein Mode-Change" - case .forceSupervised: return "Supervised setzen" - case .forceUnsupervised: return "Unsupervised setzen" - } - } -} - struct WelcomeView: View { @Environment(WizardModel.self) private var model @State private var detecting = false @State private var error: String? @State private var pollTask: Task? - @State private var resetRunning = false - @State private var resetStatus: String? - @State private var resetAll = true - @State private var resetEnrollmentProfile = true - @State private var resetLockProfile = true - @State private var resetApp = true - @State private var supervisionMode: DebugSupervisionMode = .none var body: some View { VStack(spacing: 24) { @@ -70,71 +47,12 @@ struct WelcomeView: View { .buttonStyle(.borderedProminent) .disabled(model.device == nil) } - - resetSection } .padding(40) .onAppear { startDetection() } .onDisappear { pollTask?.cancel() } } - private var resetSection: some View { - VStack(alignment: .leading, spacing: 8) { - Divider() - Text("Interner Test-Reset") - .font(.headline) - Text("Wähle gezielt, was entfernt werden soll. Optional kann zusätzlich supervised/unsupervised für Tests gesetzt werden.") - .font(.callout) - .foregroundStyle(.secondary) - - Toggle("Alles entfernen (Profile + App)", isOn: $resetAll) - .toggleStyle(.checkbox) - .onChange(of: resetAll) { _, newValue in - if newValue { - resetEnrollmentProfile = true - resetLockProfile = true - resetApp = true - } - } - - Group { - Toggle("MDM Enrollment-Profil löschen", isOn: $resetEnrollmentProfile) - .toggleStyle(.checkbox) - Toggle("Lock-Profil löschen", isOn: $resetLockProfile) - .toggleStyle(.checkbox) - Toggle("ReBreak-App löschen", isOn: $resetApp) - .toggleStyle(.checkbox) - } - .disabled(resetAll) - - Picker("Test-Mode", selection: $supervisionMode) { - ForEach(DebugSupervisionMode.allCases) { mode in - Text(mode.title).tag(mode) - } - } - .pickerStyle(.segmented) - - if let resetStatus { - Text(resetStatus) - .font(.caption) - .foregroundStyle(.secondary) - } - - HStack(spacing: 10) { - if resetRunning { - ProgressView() - .controlSize(.small) - } - Button("Debug-Reset ausführen") { - startDebugReset() - } - .buttonStyle(.bordered) - .disabled(model.device == nil || resetRunning || detecting) - } - } - .frame(maxWidth: 520, alignment: .leading) - } - private var nextButtonLabel: String { if model.device?.isFullyBound == true { return "Weiter → Schutz aktivieren" @@ -279,83 +197,4 @@ struct WelcomeView: View { } } } - - private func startDebugReset() { - guard model.device != nil else { - resetStatus = "Kein iPhone erkannt." - return - } - resetRunning = true - resetStatus = "Führe Debug-Reset aus …" - - Task { - do { - var changes: [String] = [] - - let removeEnrollment = resetAll || resetEnrollmentProfile - let removeLock = resetAll || resetLockProfile - let removeApp = resetAll || resetApp - - let installedProfileIDs = await DeviceDetector.installedProfileIDs() - var profileIDs: [String] = [] - if removeEnrollment, installedProfileIDs.contains(DeviceState.enrollmentProfileID) { - profileIDs.append(DeviceState.enrollmentProfileID) - } - if removeLock, installedProfileIDs.contains(DeviceState.lockProfileID) { - profileIDs.append(DeviceState.lockProfileID) - } - if !profileIDs.isEmpty { - try await DeviceDetector.removeProfiles(identifiers: profileIDs) - changes.append("Profile gelöscht: \(profileIDs.joined(separator: ", "))") - } - - if removeApp { - try await DeviceDetector.removeApp(bundleID: "org.rebreak.app") - changes.append("App gelöscht: org.rebreak.app") - } - - switch supervisionMode { - case .forceSupervised: - _ = try await SuperviseRunner.supervise(verbose: false) { _ in } - changes.append("Mode gesetzt: supervised") - case .forceUnsupervised: - _ = try await SuperviseRunner.unsupervise { _ in } - changes.append("Mode gesetzt: unsupervised") - case .none: - break - } - - let nowInstalledProfiles = await DeviceDetector.installedProfileIDs() - let nowApps = await DeviceDetector.installedAppBundleIDs() - let status = await DeviceDetector.readSupervisionStatus() - - await MainActor.run { - if changes.isEmpty { - resetStatus = "Keine Aktion gewählt." - } else { - resetStatus = "✓ \(changes.joined(separator: " · "))" - } - - if var device = model.device { - device.installedProfileIDs = nowInstalledProfiles - device.installedAppBundleIDs = nowApps - device.isSupervised = status.isSupervised - device.supervisorOrgName = status.organizationName - device.isFmiOn = status.findMyEnabled - device.isEnrolled = nowInstalledProfiles.contains(DeviceState.enrollmentProfileID) - if !nowApps.contains("org.rebreak.app") { device.isManaged = false } - if !nowInstalledProfiles.contains(DeviceState.lockProfileID) { device.isFilterActive = false } - model.device = device - } - - resetRunning = false - } - } catch { - await MainActor.run { - resetStatus = "✗ Reset fehlgeschlagen: \(error.localizedDescription)" - resetRunning = false - } - } - } - } } diff --git a/apps/rebreak-magic-mac/build-dmg.sh b/apps/rebreak-magic-mac/build-dmg.sh new file mode 100755 index 0000000..f0c36f3 --- /dev/null +++ b/apps/rebreak-magic-mac/build-dmg.sh @@ -0,0 +1,226 @@ +#!/bin/bash +set -euo pipefail + +# ═══════════════════════════════════════════════════════════ +# Rebreak Magic macOS — DMG Build Script +# ═══════════════════════════════════════════════════════════ +# +# Erstellt einen distributable .dmg für Rebreak Magic.app +# +# Voraussetzungen: +# - Xcode Command Line Tools +# - xcodegen (brew install xcodegen) +# - create-dmg (brew install create-dmg) +# +# Usage: +# ./build-dmg.sh +# +# Output: +# build/RebreakMagic-.dmg +# +# ═══════════════════════════════════════════════════════════ + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$SCRIPT_DIR" + +echo "════════════════════════════════════════════════════════════" +echo "Rebreak Magic DMG Build" +echo "════════════════════════════════════════════════════════════" +echo "" + +# ───────────────────────────────────────────────────────────── +# 1. Dependency-Checks +# ───────────────────────────────────────────────────────────── + +echo "→ Prüfe Dependencies..." + +if ! command -v xcodegen &>/dev/null; then + echo "❌ xcodegen nicht gefunden. Installiere via: brew install xcodegen" + exit 1 +fi + +if ! command -v create-dmg &>/dev/null; then + echo "❌ create-dmg nicht gefunden. Installiere via: brew install create-dmg" + exit 1 +fi + +if ! command -v xcodebuild &>/dev/null; then + echo "❌ xcodebuild nicht gefunden. Installiere Xcode Command Line Tools." + exit 1 +fi + +echo "✓ Dependencies OK" +echo "" + +# ───────────────────────────────────────────────────────────── +# 2. Version auslesen aus project.yml +# ───────────────────────────────────────────────────────────── + +VERSION=$(grep 'MARKETING_VERSION:' project.yml | sed 's/.*"\(.*\)"/\1/') +if [ -z "$VERSION" ]; then + echo "❌ Konnte Version nicht aus project.yml lesen" + exit 1 +fi + +echo "→ Version: $VERSION" +echo "" + +# ───────────────────────────────────────────────────────────── +# 3. Xcode-Projekt generieren +# ───────────────────────────────────────────────────────────── + +echo "→ Generiere Xcode-Projekt..." +xcodegen generate + +if [ ! -f "RebreakMagic.xcodeproj/project.pbxproj" ]; then + echo "❌ Xcode-Projekt konnte nicht generiert werden" + exit 1 +fi + +echo "✓ Projekt generiert" +echo "" + +# ───────────────────────────────────────────────────────────── +# 4. macOS Icon-Cache killen (damit neues Icon sofort sichtbar) +# ───────────────────────────────────────────────────────────── + +echo "→ Lösche macOS Icon-Cache..." +sudo rm -rf /Library/Caches/com.apple.iconservices.store 2>/dev/null || true +killall Dock Finder 2>/dev/null || true +echo "✓ Icon-Cache geleert" +echo "" + +# ───────────────────────────────────────────────────────────── +# 5. Release-Build +# ───────────────────────────────────────────────────────────── + +echo "→ Baue Release-Build..." + +BUILD_DIR="$SCRIPT_DIR/build" +rm -rf "$BUILD_DIR" +mkdir -p "$BUILD_DIR" + +xcodebuild \ + -project RebreakMagic.xcodeproj \ + -scheme RebreakMagic \ + -configuration Release \ + -derivedDataPath "$BUILD_DIR" \ + clean build + +APP_PATH="$BUILD_DIR/Build/Products/Release/RebreakMagic.app" + +if [ ! -d "$APP_PATH" ]; then + echo "❌ Build fehlgeschlagen — RebreakMagic.app nicht gefunden" + exit 1 +fi + +echo "✓ Build erfolgreich: $APP_PATH" +echo "" + +# ───────────────────────────────────────────────────────────── +# 6. Icon-Verifikation +# ───────────────────────────────────────────────────────────── + +echo "→ Prüfe App-Icon..." + +if [ -f "$APP_PATH/Contents/Resources/Assets.car" ]; then + echo "✓ Assets.car vorhanden" + ICON_COUNT=$(assetutil --info "$APP_PATH/Contents/Resources/Assets.car" 2>&1 | grep -c "Name.*AppIcon" || true) + echo " → $ICON_COUNT AppIcon-Einträge kompiliert" +else + echo "⚠️ Assets.car nicht gefunden — Icons könnten fehlen" +fi + +INFO_PLIST="$APP_PATH/Contents/Info.plist" +ICON_FILE=$(/usr/libexec/PlistBuddy -c "Print :CFBundleIconFile" "$INFO_PLIST" 2>/dev/null || echo "") + +if [ -n "$ICON_FILE" ]; then + echo "✓ CFBundleIconFile: $ICON_FILE" + if [ -f "$APP_PATH/Contents/Resources/$ICON_FILE.icns" ]; then + ICNS_SIZE=$(du -h "$APP_PATH/Contents/Resources/$ICON_FILE.icns" | cut -f1) + echo " → $ICON_FILE.icns gefunden ($ICNS_SIZE)" + fi +else + echo "⚠️ CFBundleIconFile nicht in Info.plist — macOS könnte Default-Icon zeigen" +fi + +echo "" + +# ───────────────────────────────────────────────────────────── +# 7. Code-Signing-Status (Info-Only) +# ───────────────────────────────────────────────────────────── + +echo "→ Code-Signing-Status..." + +SIGNING_IDENTITY=$(codesign -dvv "$APP_PATH" 2>&1 | grep "Authority=" | head -1 || echo "") + +if [ -z "$SIGNING_IDENTITY" ]; then + echo "⚠️ App ist unsigned (ad-hoc signature)" + echo " → User braucht Right-Click → Öffnen beim ersten Start (Gatekeeper)" + echo " → Für Production-Distribution: Developer ID Application Cert nötig" +else + echo "✓ Signiert: $SIGNING_IDENTITY" +fi + +echo "" + +# ───────────────────────────────────────────────────────────── +# 8. DMG erstellen +# ───────────────────────────────────────────────────────────── + +DMG_NAME="RebreakMagic-${VERSION}.dmg" +DMG_PATH="$BUILD_DIR/$DMG_NAME" + +echo "→ Erstelle DMG: $DMG_NAME" +echo "" + +# create-dmg mit Standard-Layout: +# - App-Icon links +# - Applications-Link rechts +# - Drag-to-install-Hinweis + +create-dmg \ + --volname "Rebreak Magic" \ + --window-pos 200 120 \ + --window-size 600 400 \ + --icon-size 100 \ + --icon "RebreakMagic.app" 175 190 \ + --hide-extension "RebreakMagic.app" \ + --app-drop-link 425 190 \ + --no-internet-enable \ + "$DMG_PATH" \ + "$APP_PATH" \ + 2>&1 | grep -v "^hdiutil:" || true # Filter verbose hdiutil-Output + +if [ ! -f "$DMG_PATH" ]; then + echo "❌ DMG konnte nicht erstellt werden" + exit 1 +fi + +DMG_SIZE=$(du -h "$DMG_PATH" | cut -f1) + +echo "" +echo "════════════════════════════════════════════════════════════" +echo "✓ DMG erfolgreich erstellt" +echo "════════════════════════════════════════════════════════════" +echo "" +echo " Pfad: $DMG_PATH" +echo " Größe: $DMG_SIZE" +echo " Version: $VERSION" +echo "" +echo "Installation:" +echo " 1. DMG öffnen (doppelklick)" +echo " 2. RebreakMagic.app nach /Applications ziehen" +echo " 3. Beim ersten Start: Right-Click → Öffnen (Gatekeeper-Warning)" +echo "" +echo "Hinweis: Falls das Icon nicht sofort erscheint:" +echo " sudo rm -rf /Library/Caches/com.apple.iconservices.store && killall Dock Finder" +echo "" +echo "────────────────────────────────────────────────────────────" +echo "TODO für Production-Distribution:" +echo "────────────────────────────────────────────────────────────" +echo " - Developer ID Application Cert für Code-Signing" +echo " - Notarization via Apple (xcrun notarytool)" +echo " - Staple Notarization-Ticket: xcrun stapler staple" +echo " - DMG dann ohne Gatekeeper-Warning installierbar" +echo "" diff --git a/apps/rebreak-magic-mac/project.yml b/apps/rebreak-magic-mac/project.yml index c602f58..a910a84 100644 --- a/apps/rebreak-magic-mac/project.yml +++ b/apps/rebreak-magic-mac/project.yml @@ -1,6 +1,6 @@ -name: RebreakBinder +name: RebreakMagic options: - bundleIdPrefix: org.rebreak.binder + bundleIdPrefix: org.rebreak.magic deploymentTarget: macOS: "14.0" createIntermediateGroups: true @@ -10,7 +10,7 @@ settings: base: SWIFT_VERSION: "5.10" MACOSX_DEPLOYMENT_TARGET: "14.0" - PRODUCT_BUNDLE_IDENTIFIER: org.rebreak.binder.mac + PRODUCT_BUNDLE_IDENTIFIER: org.rebreak.magic.mac MARKETING_VERSION: "0.1.0" CURRENT_PROJECT_VERSION: "1" DEVELOPMENT_TEAM: "" @@ -20,7 +20,7 @@ settings: ENABLE_APP_SANDBOX: NO targets: - RebreakBinder: + RebreakMagic: type: application platform: macOS sources: @@ -33,7 +33,7 @@ targets: info: path: Sources/Resources/Info.plist properties: - CFBundleDisplayName: ReBreak Binder + CFBundleDisplayName: Rebreak Magic CFBundleShortVersionString: $(MARKETING_VERSION) CFBundleVersion: $(CURRENT_PROJECT_VERSION) LSMinimumSystemVersion: $(MACOSX_DEPLOYMENT_TARGET) diff --git a/apps/rebreak-native/.deploy-secrets.local.example b/apps/rebreak-native/.deploy-secrets.local.example index 936c137..abe74e5 100644 --- a/apps/rebreak-native/.deploy-secrets.local.example +++ b/apps/rebreak-native/.deploy-secrets.local.example @@ -1,7 +1,7 @@ -# Rebreak Deploy Secrets — Copy to .env.deploy.local (gitignored!) +# Rebreak Deploy Secrets — Copy to .deploy-secrets.local (gitignored!) # # Source-Reihenfolge (deploy.sh lädt erstes vorhandenes File): -# 1. apps/rebreak-native/.env.deploy.local +# 1. apps/rebreak-native/.deploy-secrets.local # 2. ~/.config/rebreak/deploy.env # # ────────────────────────────────────────────────────────────────────────── diff --git a/apps/rebreak-native/.gitignore b/apps/rebreak-native/.gitignore index a16426a..1ae893a 100644 --- a/apps/rebreak-native/.gitignore +++ b/apps/rebreak-native/.gitignore @@ -39,6 +39,7 @@ yarn-error.* # local env files .env*.local +.deploy-secrets.local # typescript *.tsbuildinfo diff --git a/apps/rebreak-native/.maestro/SETUP.md b/apps/rebreak-native/.maestro/SETUP.md index c332e8a..da94764 100644 --- a/apps/rebreak-native/.maestro/SETUP.md +++ b/apps/rebreak-native/.maestro/SETUP.md @@ -43,10 +43,17 @@ pnpm exec expo run:ios pnpm exec expo run:ios --device "iPhone 15" ``` -Alternativ (wenn dev-iphone.sh vorhanden): +Alternativ via konsolidiertem Dev-Script: ```bash -bash apps/rebreak-native/dev-iphone.sh +# Vollbuild auf iPhone via USB: +bash apps/rebreak-native/dev.sh ios + +# WiFi-Modus (kein Kabel, Metro über LAN): +bash apps/rebreak-native/dev.sh ios --wifi + +# Schneller JS-Reload ohne Native-Rebuild: +bash apps/rebreak-native/dev.sh ios --no-build ``` ### Android Emulator diff --git a/apps/rebreak-native/CHANGELOG.md b/apps/rebreak-native/CHANGELOG.md index d7bcdbc..e831b09 100644 --- a/apps/rebreak-native/CHANGELOG.md +++ b/apps/rebreak-native/CHANGELOG.md @@ -1,6 +1,9 @@ # Changelog All notable changes to rebreak-native will be documented in this file. +## v0.3.13 (Build 46 / versionCode 36) — 2026-05-31\n\nDM-Chat: Die letzte Nachricht wird jetzt zuverlässig oberhalb der Eingabezeile angezeigt — kein manuelles Nachscrollen mehr beim Öffnen oder nach dem Senden. + +Chat-Übersicht: Zeitangaben sind feiner abgestuft — neben Minuten/Stunden jetzt auch Wochentag (z.B. „Mi"), danach Tage, Wochen, Monate und Jahre statt nur Datum.\n ## v0.3.13 (Build 44 / versionCode 35) — 2026-05-31\n\nDM-Chat: scrollt jetzt zuverlässig zur neuesten Nachricht — auch nach eigenen gesendeten Nachrichten und beim Laden von Bildern. Lyra-Sprachnachrichten: Wenn du auf Arabisch oder Türkisch sprichst, antwortet Lyra jetzt auch in der richtigen Sprache (Backend-Fix). diff --git a/apps/rebreak-native/SCRIPTS.md b/apps/rebreak-native/SCRIPTS.md index cb1f405..5d669ed 100644 --- a/apps/rebreak-native/SCRIPTS.md +++ b/apps/rebreak-native/SCRIPTS.md @@ -4,16 +4,23 @@ ### Development ```bash -# iOS Dev (Metro + Xcode): -./dev.sh ios +# Default = iPhone USB + Native-Build: +./dev.sh -# iOS Dev auf physischem iPhone (USB): +# Schneller UI-Loop (kein Rebuild, App schon installiert): +./dev.sh ios --no-build +./dev.sh android --no-build + +# iOS Dev auf physischem iPhone (USB) mit Build: ./dev.sh ios --device -# iOS Dev auf iPhone via WiFi: +# iOS Dev auf iPhone via WiFi (Kabel ab): ./dev.sh ios --wifi -# Android Dev: +# iOS Simulator: +./dev.sh ios --simulator + +# Android Dev (Build + Install + Launch): ./dev.sh android # Nur Metro starten: @@ -89,14 +96,16 @@ - `install android` — Debug-APK auf Android Device installieren ### Flags (ios) -- `--device` — Build auf physisches iPhone via USB -- `--simulator` — Build auf iOS Simulator (default) +- `--device` — Build auf physisches iPhone via USB **(default)** +- `--simulator` — Build auf iOS Simulator - `--xcode` — Nur Xcode öffnen (manueller Build) -- `--wifi` — Metro mit --host lan (für WiFi-Dev auf iPhone) +- `--wifi` — Metro mit --host lan (für WiFi-Dev, kein Native-Build) +- `--no-build` — **KEIN Native-Rebuild** → nur Metro starten (App muss schon installiert sein, schnellster UI/JS-Loop) ### Flags (android) -- `--no-build` — Skip Gradle build, nur install last APK -- `--no-launch` — Install but don't auto-launch +- `--no-build` — **KEIN Gradle-Rebuild** → nur Metro starten (APK muss schon installiert sein, schnellster UI/JS-Loop) +- `--no-launch` — Build+Install, aber kein Auto-Launch +- `--wifi` — Metro mit --host lan (nur in Kombi mit `--no-build`) ### Flags (metro) - `--keep` — Cache behalten (kein --clear) diff --git a/apps/rebreak-native/app.config.ts b/apps/rebreak-native/app.config.ts index 1d2cce2..6752e1c 100644 --- a/apps/rebreak-native/app.config.ts +++ b/apps/rebreak-native/app.config.ts @@ -36,7 +36,7 @@ export default ({ config }: ConfigContext): ExpoConfig => ({ ios: { supportsTablet: true, bundleIdentifier: MAIN_BUNDLE, - buildNumber: "45", + buildNumber: "46", // Apple Sign-In Entitlement — Pflicht für expo-apple-authentication nativen // signInAsync()-Flow. Ohne flag generiert Expo's prebuild den // com.apple.developer.applesignin-Entitlement nicht in die .entitlements. @@ -59,7 +59,7 @@ export default ({ config }: ConfigContext): ExpoConfig => ({ android: { package: "org.rebreak.app", - versionCode: 35, + versionCode: 36, adaptiveIcon: { // Foreground muss in der ~66%-Safe-Zone bleiben (Launcher-Mask clippt den // Außenring) → adaptive-foreground.png ist das Logo auf transparentem diff --git a/apps/rebreak-native/app/(app)/chat.tsx b/apps/rebreak-native/app/(app)/chat.tsx index a085d08..556919d 100644 --- a/apps/rebreak-native/app/(app)/chat.tsx +++ b/apps/rebreak-native/app/(app)/chat.tsx @@ -30,10 +30,15 @@ type DmConversation = { function formatTime(ts: string, justNowLabel: string): string { const diff = Date.now() - new Date(ts).getTime(); + const day = 86_400_000; if (diff < 60_000) return justNowLabel; if (diff < 3_600_000) return `${Math.floor(diff / 60_000)}m`; - if (diff < 86_400_000) return `${Math.floor(diff / 3_600_000)}h`; - return new Date(ts).toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit' }); + if (diff < day) return `${Math.floor(diff / 3_600_000)}h`; + if (diff < 7 * day) return new Date(ts).toLocaleDateString('de-DE', { weekday: 'short' }); + if (diff < 30 * day) return `${Math.floor(diff / day)}d`; + if (diff < 60 * day) return `${Math.floor(diff / (7 * day))}w`; + if (diff < 365 * day) return `${Math.floor(diff / (30 * day))}mo`; + return `${Math.floor(diff / (365 * day))}y`; } function DmItem({ conv, onPress }: { conv: DmConversation; onPress: () => void }) { diff --git a/apps/rebreak-native/app/_layout.tsx b/apps/rebreak-native/app/_layout.tsx index ce753a3..534a434 100644 --- a/apps/rebreak-native/app/_layout.tsx +++ b/apps/rebreak-native/app/_layout.tsx @@ -27,7 +27,6 @@ import { useColors } from '../lib/theme'; import { useLanguageStore } from '../stores/language'; import { useAppLockStore } from '../stores/appLock'; import { useLyraVoiceStore } from '../stores/lyraVoice'; -import { BrandSplash } from '../components/BrandSplash'; import { AppLockGate } from '../components/AppLockGate'; import { DeviceLimitReachedSheet } from '../components/DeviceLimitReachedSheet'; import { OnlinePresenceProvider } from '../components/OnlinePresenceProvider'; @@ -124,7 +123,10 @@ function RootLayoutInner() { }, [fontsLoaded, loading, appLockReady]); if (!fontsLoaded || loading || !appLockReady) { - return ; + // Nativer expo-splash-screen bleibt sichtbar bis SplashScreen.hideAsync() + // im Effect oben aufgerufen wird → kein Flicker durch zusätzlichen + // React-Splash mehr (User-Feedback: "geht sehr schnell vorbei und zuckt") + return null; } return ( diff --git a/apps/rebreak-native/app/dm.tsx b/apps/rebreak-native/app/dm.tsx index a0d9a4b..6948759 100644 --- a/apps/rebreak-native/app/dm.tsx +++ b/apps/rebreak-native/app/dm.tsx @@ -87,6 +87,7 @@ export default function DmScreen() { const [uploading, setUploading] = useState(false); const [keyboardVisible, setKeyboardVisible] = useState(false); const [keyboardHeight, setKeyboardHeight] = useState(0); + const [inputBarHeight, setInputBarHeight] = useState(60); // Reset aller conversation-spezifischen States wenn userId wechselt (Stack-Reuse) useEffect(() => { @@ -516,7 +517,7 @@ export default function DmScreen() { contentContainerStyle={{ paddingHorizontal: 0, paddingTop: 12, - paddingBottom: 12 + insets.bottom + (keyboardVisible ? keyboardHeight : 0), + paddingBottom: inputBarHeight + 12, }} showsVerticalScrollIndicator={false} keyboardDismissMode="interactive" @@ -531,6 +532,10 @@ export default function DmScreen() { style={{ backgroundColor: colors.bg }} > { + const h = e.nativeEvent.layout.height; + if (Math.abs(h - inputBarHeight) > 1) setInputBarHeight(h); + }} style={[ styles.inputBar, { diff --git a/apps/rebreak-native/components/chat/ChatBubble.tsx b/apps/rebreak-native/components/chat/ChatBubble.tsx index a083d3d..d32c82f 100644 --- a/apps/rebreak-native/components/chat/ChatBubble.tsx +++ b/apps/rebreak-native/components/chat/ChatBubble.tsx @@ -41,6 +41,8 @@ export type ChatMsg = { reactions?: MessageReaction[]; /** Soft-Delete-Tombstone. */ deleted?: boolean; + /** Optimistic-UI Status (pending = wird gesendet, failed = Fehler). */ + status?: 'pending' | 'sent' | 'failed'; }; type Props = { @@ -192,6 +194,8 @@ export function ChatBubble({ { backgroundColor: bubbleBg }, !msg.isOwn && styles.bubbleOtherBorder, isImageOnly && { padding: 4 }, + msg.status === 'pending' && { opacity: 0.6 }, + msg.status === 'failed' && { borderWidth: 1, borderColor: '#ef4444' }, ]} > {msg.replyTo && ( @@ -327,7 +331,7 @@ export function ChatBubble({ > {formatTime(msg.createdAt)} - {msg.isOwn && !hideReadStatus && ( + {isDM && msg.isOwn && msg.status !== 'pending' && msg.status !== 'failed' && ( )} + {msg.status === 'pending' && ( + + )} + {msg.status === 'failed' && ( + + )} )} diff --git a/apps/rebreak-native/components/header/HeaderDropdownMenu.tsx b/apps/rebreak-native/components/header/HeaderDropdownMenu.tsx index 7214c78..510aa9a 100644 --- a/apps/rebreak-native/components/header/HeaderDropdownMenu.tsx +++ b/apps/rebreak-native/components/header/HeaderDropdownMenu.tsx @@ -127,7 +127,7 @@ export function HeaderDropdownMenu({ visible, onClose, topOffset = 80 }: Props) ) : null} @@ -263,3 +263,11 @@ export function HeaderDropdownMenu({ visible, onClose, topOffset = 80 }: Props) ); } + +const styles = StyleSheet.create({ + blurFill: { + ...StyleSheet.absoluteFillObject, + borderRadius: 18, + overflow: 'hidden', + }, +}); diff --git a/apps/rebreak-native/components/onboarding/slides/DoneSlide.tsx b/apps/rebreak-native/components/onboarding/slides/DoneSlide.tsx index 94d0e28..61fdfa3 100644 --- a/apps/rebreak-native/components/onboarding/slides/DoneSlide.tsx +++ b/apps/rebreak-native/components/onboarding/slides/DoneSlide.tsx @@ -2,11 +2,13 @@ import { useEffect, useRef } from 'react'; import { Animated, Easing, Text, useWindowDimensions, View } from 'react-native'; import { useTranslation } from 'react-i18next'; import { Ionicons } from '@expo/vector-icons'; +import * as Notifications from 'expo-notifications'; import { useColors } from '../../../lib/theme'; import { OnboardingShell } from '../OnboardingShell'; import { LyraBubble } from '../LyraBubble'; import { CTABar } from '../CTABar'; import { FaqAccordion, type FaqItem } from '../../FaqAccordion'; +import { useNotificationPrefsStore } from '../../../stores/notificationPrefs'; // Top-5 (kuratiert für Onboarding-Ende) — alle 8 sind unter app/help/faq.tsx. const ONBOARDING_FAQ_IDS = [1, 2, 4, 5, 8] as const; @@ -24,6 +26,7 @@ export function DoneSlide({ const colors = useColors(); const scale = useRef(new Animated.Value(0.6)).current; const opacity = useRef(new Animated.Value(0)).current; + const setPushEnabled = useNotificationPrefsStore((s) => s.setPushEnabled); const faqItems: FaqItem[] = ONBOARDING_FAQ_IDS.map((id) => ({ q: t(`help.faq_q${id}`), @@ -40,7 +43,16 @@ export function DoneSlide({ easing: Easing.out(Easing.cubic), }), ]).start(); - }, []); + + (async () => { + try { + const { status } = await Notifications.requestPermissionsAsync(); + if (status !== 'granted') { + await setPushEnabled(false); + } + } catch {} + })(); + }, [setPushEnabled]); return ( 0 )); then die "iOS Signing braucht ASC API-Key. Fehlt: ${missing[*]} -→ Editiere apps/rebreak-native/.env.deploy.local (siehe .env.deploy.local.example)" +→ Editiere apps/rebreak-native/.deploy-secrets.local (siehe .deploy-secrets.local.example)" fi if [[ ! -f "$ASC_API_KEY_PATH" ]]; then die "ASC API-Key Datei existiert nicht: $ASC_API_KEY_PATH diff --git a/apps/rebreak-native/dev.sh b/apps/rebreak-native/dev.sh index 630b835..9d70307 100755 --- a/apps/rebreak-native/dev.sh +++ b/apps/rebreak-native/dev.sh @@ -1,24 +1,30 @@ #!/bin/bash # dev.sh — ReBreak Native Development Tooling # +# Konsolidiert: dev-ios.sh + dev-iphone.sh + metro.sh (alle gelöscht). +# # SUBCOMMANDS: -# ./dev.sh default: ios (Metro + Xcode) -# ./dev.sh ios iOS Dev (Metro + Xcode Workspace / Simulator) -# ./dev.sh android Android Dev (Metro + Gradle build + install) +# ./dev.sh default: ios --device (physisches iPhone USB + Build) +# ./dev.sh ios iOS Dev (Default: USB-Device mit Build) +# ./dev.sh android Android Dev (Gradle Build + Install + Launch) # ./dev.sh metro Nur Metro starten # ./dev.sh clean iOS: Nuclear clean (Pods, DerivedData, Archives) # ./dev.sh install ios Build Release + Install auf iPhone USB # ./dev.sh install android Build Debug APK + Install auf Android Device # # FLAGS (ios): -# --device Build auf physisches iPhone via USB -# --simulator Build auf iOS Simulator (default) +# --device Build auf physisches iPhone via USB (DEFAULT) +# --simulator Build auf iOS Simulator # --xcode Nur Xcode öffnen (manueller Build) -# --wifi Metro mit --host lan (für WiFi-Dev auf iPhone) +# --wifi Metro mit --host lan (WiFi-Dev, KEIN Native-Build) +# --no-build KEIN Native-Rebuild → nur Metro starten +# (für schnellen UI/JS-Reload — App muss installiert sein) # # FLAGS (android): -# --no-build Skip Gradle build, nur install last APK -# --no-launch Install but don't auto-launch +# --no-build KEIN Gradle-Rebuild → nur Metro starten +# (für schnellen UI/JS-Reload — APK muss installiert sein) +# --no-launch Build+Install, aber kein Auto-Launch +# --wifi Metro mit --host lan (nur in Kombi mit --no-build) # # FLAGS (metro): # --keep Cache behalten (kein --clear) @@ -28,20 +34,21 @@ # --xcode + Xcode öffnen am Ende # # BEISPIELE: -# # iOS Dev auf Simulator: -# ./dev.sh ios +# # Default: iPhone USB + Native-Build: +# ./dev.sh # -# # iOS Dev auf physischem iPhone via USB: -# ./dev.sh ios --device +# # Schneller UI-Loop (App schon installiert, nur JS-Reload): +# ./dev.sh ios --no-build +# ./dev.sh android --no-build # -# # iOS Dev auf iPhone via WiFi (Metro LAN): +# # WiFi-Dev (Kabel ab, Metro über LAN): # ./dev.sh ios --wifi # -# # Android Dev: -# ./dev.sh android +# # iOS Simulator: +# ./dev.sh ios --simulator # -# # Nur Metro starten: -# ./dev.sh metro +# # Nur Xcode öffnen: +# ./dev.sh ios --xcode # # # iOS Clean + Rebuild: # ./dev.sh clean --build @@ -95,8 +102,9 @@ export REBREAK_DEV="${REBREAK_DEV:-0}" # ═══════════════════════════════════════════════════════════════════════════ cmd_ios() { - local MODE="simulator" + local MODE="device" # Default: physisches iPhone via USB local WIFI=false + local BUILD=true # Default: nativen Build laufen lassen while [[ $# -gt 0 ]]; do case "$1" in @@ -104,25 +112,40 @@ cmd_ios() { --simulator) MODE="simulator"; shift ;; --xcode) MODE="xcode"; shift ;; --wifi) WIFI=true; shift ;; + --no-build) BUILD=false; shift ;; *) die "Unbekannter Flag für 'ios': $1" ;; esac done section "iOS Dev Mode" - if $WIFI; then - log "Metro: WiFi-Modus (--host lan)" - echo "" - echo "Mac LAN-IP:" - ipconfig getifaddr en0 2>/dev/null || ipconfig getifaddr en1 2>/dev/null || echo " (kein WiFi/Ethernet detected)" - echo "" - echo "Falls dev-client Metro nicht automatisch findet:" - echo " im iPhone-Launcher → 'Enter URL manually' → http://:8081" + # ───────────────────────────────────────────────────────────── + # --no-build / WiFi: KEIN Native-Rebuild, nur Metro + Dev-Client + # → schnellster Loop für reine UI/JS-Änderungen + # → App muss schon auf dem Device/Simulator installiert sein + # ───────────────────────────────────────────────────────────── + if ! $BUILD || $WIFI; then + local HOST_FLAG="" + if $WIFI; then + HOST_FLAG="--host lan" + log "Metro: WiFi-Modus (--host lan)" + echo "" + echo "Mac LAN-IP:" + ipconfig getifaddr en0 2>/dev/null || ipconfig getifaddr en1 2>/dev/null || echo " (kein WiFi/Ethernet detected)" + echo "" + echo "Falls dev-client Metro nicht automatisch findet:" + echo " im iPhone-Launcher → 'Enter URL manually' → http://:8081" + else + log "Metro: USB/Local-Modus (kein Native-Rebuild)" + echo "App auf Device/Simulator muss schon installiert sein." + echo "Beim Öffnen connected dev-client automatisch zu Metro." + fi echo "" log "Killing old Metro on port 8081..." - lsof -ti:8081 | xargs kill -9 2>/dev/null || true + lsof -ti:8081 2>/dev/null | xargs kill -9 2>/dev/null || true + pkill -f "expo start" 2>/dev/null || true echo "" - exec pnpm expo start --host lan --clear --dev-client + exec pnpm expo start $HOST_FLAG --clear --dev-client fi case "$MODE" in @@ -137,6 +160,7 @@ cmd_ios() { device) log "Building für physisches iPhone (USB)..." + echo "ℹ️ Für schnellen UI-Reload ohne Rebuild: './dev.sh ios --no-build'" pnpm expo run:ios --device ;; @@ -150,17 +174,44 @@ cmd_ios() { cmd_android() { local BUILD=true local LAUNCH=true + local WIFI=false while [[ $# -gt 0 ]]; do case "$1" in --no-build) BUILD=false; shift ;; --no-launch) LAUNCH=false; shift ;; + --wifi) WIFI=true; shift ;; *) die "Unbekannter Flag für 'android': $1" ;; esac done section "Android Dev Mode" + # ───────────────────────────────────────────────────────────── + # --no-build: KEIN Gradle-Rebuild, nur Metro + Dev-Client + # → schnellster Loop für reine UI/JS-Änderungen + # → APK muss schon auf dem Device installiert sein + # ───────────────────────────────────────────────────────────── + if ! $BUILD; then + local HOST_FLAG="" + if $WIFI; then + HOST_FLAG="--host lan" + log "Metro: WiFi-Modus (--host lan)" + echo "" + echo "Mac LAN-IP:" + ipconfig getifaddr en0 2>/dev/null || ipconfig getifaddr en1 2>/dev/null || echo " (kein WiFi/Ethernet detected)" + else + log "Metro: USB/ADB-Modus (kein Gradle-Rebuild)" + echo "APK muss schon auf dem Device installiert sein." + fi + echo "" + log "Killing old Metro on port 8081..." + lsof -ti:8081 2>/dev/null | xargs kill -9 2>/dev/null || true + pkill -f "expo start" 2>/dev/null || true + echo "" + exec pnpm expo start $HOST_FLAG --clear --dev-client + fi + command -v adb >/dev/null 2>&1 || die "adb nicht gefunden — brew install --cask android-platform-tools" local DEVICE_COUNT @@ -178,10 +229,9 @@ cmd_android() { exit 1 fi - if $BUILD; then - log "Building Debug APK..." - (cd "$ANDROID_DIR" && ./gradlew assembleDebug --console=plain) - fi + log "Building Debug APK..." + echo "ℹ️ Für schnellen UI-Reload ohne Rebuild: './dev.sh android --no-build'" + (cd "$ANDROID_DIR" && ./gradlew assembleDebug --console=plain) local APK="$ANDROID_DIR/app/build/outputs/apk/debug/app-debug.apk" [[ -f "$APK" ]] || die "APK nicht gefunden: $APK" diff --git a/apps/rebreak-native/metro.config.js b/apps/rebreak-native/metro.config.js index 72d7b59..d19d2e7 100644 --- a/apps/rebreak-native/metro.config.js +++ b/apps/rebreak-native/metro.config.js @@ -26,6 +26,19 @@ config.resolver.unstable_enablePackageExports = true; // 4. .riv (Rive-Animation) als Asset registrieren config.resolver.assetExts = [...(config.resolver.assetExts ?? []), 'riv']; +// 5. Block .env* files — die sind Shell-Snippets (deploy.sh sourced sie), +// KEIN JS und sollen niemals von Metro/Babel angefasst werden. +// Ohne diesen Block kann Metro's File-Watcher (watchFolders=monorepoRoot) +// sie irrtümlich als Modul transformieren → "Unexpected token (1:0)" auf '#'. +config.resolver.blockList = [ + ...(Array.isArray(config.resolver.blockList) + ? config.resolver.blockList + : config.resolver.blockList + ? [config.resolver.blockList] + : []), + /\.env(\..*)?$/, +]; + module.exports = withNativeWind(config, { input: './global.css', }); diff --git a/apps/rebreak-native/modules/rebreak-protection/android/src/main/java/expo/modules/rebreakprotection/accessibility/RebreakAccessibilityService.kt b/apps/rebreak-native/modules/rebreak-protection/android/src/main/java/expo/modules/rebreakprotection/accessibility/RebreakAccessibilityService.kt index a549420..44503d7 100644 --- a/apps/rebreak-native/modules/rebreak-protection/android/src/main/java/expo/modules/rebreakprotection/accessibility/RebreakAccessibilityService.kt +++ b/apps/rebreak-native/modules/rebreak-protection/android/src/main/java/expo/modules/rebreakprotection/accessibility/RebreakAccessibilityService.kt @@ -80,6 +80,21 @@ class RebreakAccessibilityService : AccessibilityService() { * Aktiviert nur wenn der App-Lock armed UND der Schutz aktiv ist (`filter_enabled`). * Letzteres lässt den User nach einem legitim abgelaufenen Cooldown wieder raus. * + * **Strikte Match-Regel (v2 nach Field-Bug-Reports):** Wir blocken NUR wenn die + * aktuelle Window-Hierarchie unsere App tatsächlich im Text führt. Heißt konkret: + * - HIGH_CONFIDENCE_KEYWORD ("rebreak filter", "sichert den schutz", …) → sofort + * - ODER (dangerous activity-class wie VpnSettings/Uninstaller) UND Wort + * "rebreak" im Page-Text → block + * - Spezialfall `com.android.vpndialogs`: Dieses System-Package gibt es NUR für + * aktive VPN-Sessions. Da unser Schutz hier per Vorbedingung an ist (Early + * Exit oben), ist jeder Trennen-Dialog hier sicher unser eigener → class-match + * reicht. + * + * Die alte „2-Keyword-Cluster"-Heuristik („vpn"+„trennen", „eingabehilfe"+„apps", + * „rebreak"+„speicher" …) wurde entfernt — sie blockte legitime Pages wie die + * Verbindungs-Übersicht, die Einstellungen-Hauptseite und die Play-Store-„Apps + * verwalten"-Liste (false-positive über Generika wie „Speicher", „Apps", „VPN"). + * * @return true wenn die Activity geblockt wurde */ private fun handleProtectedSettingsBlock(pkg: String, event: AccessibilityEvent): Boolean { @@ -105,25 +120,42 @@ class RebreakAccessibilityService : AccessibilityService() { // DEBUG: alle Settings-Activities mitloggen damit wir OEM-Variationen sehen Log.i(TAG, "settings-watch: $pkg / $className") - // Phase 1 — Class-Name-Match (ältere Stock-Android-Patterns) + // Window-Text einmal sammeln — wird sowohl für High-Confidence- als auch + // Class+Rebreak-Check gebraucht. Kann null sein wenn root window flackert. + val pageText = collectWindowText()?.lowercase().orEmpty() + + // 1) High-confidence Keyword im Text → sofortiger Block (Rebreak-spezifisch) + val highConfHit = HIGH_CONFIDENCE_KEYWORDS.firstOrNull { pageText.contains(it) } + if (highConfHit != null) { + return doBlock(pkg, className, "high-confidence:$highConfHit", now) + } + + // 2) Activity-Class-Match — aber NUR blocken wenn Page klar über Rebreak + // ist (Wort "rebreak" im Text). Sonst würde z.B. die App-Info-Page einer + // beliebigen anderen App geblockt werden. val classMatchDangerous = DANGEROUS_ACTIVITY_PATTERNS.any { pattern -> className.contains(pattern, ignoreCase = true) } - - // Phase 2 — Window-Content-Match: scannen wenn kein className-Match. OEMs - // benutzen für Dialoge oft className die weder in unseren Patterns noch als - // "generic container" erkannt werden (z.B. Samsung's "AppDialog"). Der - // Keyword-Cluster-Scan ist das Safety-Net: 2 Keywords aus dem gleichen - // Cluster = Block. False-positive-Risk gedämpft durch Throttling. - var contentReason: String? = null - if (!classMatchDangerous) { - contentReason = scanWindowForDangerousContent() + if (classMatchDangerous) { + // Spezialfall: vpndialogs gehört System-seitig nur zur aktuell aktiven + // VPN-Session. Da hier per Vorbedingung unser Schutz an ist, ist der + // Dialog garantiert über uns — auch wenn der Profil-Name noch nicht + // in den Knoten gerendert wurde. + if (pkg == "com.android.vpndialogs") { + return doBlock(pkg, className, "vpn-dialog", now) + } + if (pageText.contains("rebreak")) { + return doBlock(pkg, className, "class+rebreak", now) + } + Log.d(TAG, "settings-watch: dangerous class $className but no 'rebreak' in text → skip") } - val isDangerous = classMatchDangerous || contentReason != null - if (!isDangerous) return false + return false + } - Log.w(TAG, "TAMPER-BLOCK: $pkg / $className (reason=${contentReason ?: "class-match"})") + /** Hilft Toast+Back-Action wiederzuverwenden (DRY für die beiden Block-Pfade). */ + private fun doBlock(pkg: String, className: String, reason: String, now: Long): Boolean { + Log.w(TAG, "TAMPER-BLOCK: $pkg / $className (reason=$reason)") lastBlockAt = now // post-block cooldown startet jetzt // Doppel-BACK: einmal um Activity zu schließen, einmal als Backup falls // erste BACK nur einen Dialog dismissed. @@ -147,36 +179,19 @@ class RebreakAccessibilityService : AccessibilityService() { } /** - * Scannt die aktuelle Window-Hierarchie nach Texten die auf eine - * VPN/A11y/App-Uninstall-Page hindeuten. Wird genutzt wenn die Activity - * generisch ist (z.B. Samsung's SubSettings) — dann müssen wir den - * Inhalt selbst inspizieren. + * Sammelt den gesamten sichtbaren Text der aktuellen Window-Hierarchie + * (Text + ContentDescription, bis Tiefe 10). Lowercased-Vereinigung wird + * vom Caller gegen Keywords / "rebreak" gematcht. Returnt null wenn keine + * Root-Window verfügbar (Page transitioning). */ - private fun scanWindowForDangerousContent(): String? { + private fun collectWindowText(): String? { val root = rootInActiveWindow ?: return null val texts = mutableListOf() collectAllText(root, texts, depth = 0) - val joined = texts.joinToString(" | ").lowercase() - // DEBUG: was steht eigentlich auf der Page? Hilft beim Patterns-Tuning. + if (texts.isEmpty()) return null + val joined = texts.joinToString(" | ") Log.d(TAG, "settings-content-text: ${joined.take(500)}") - - // High-confidence Keywords: 1 Treffer reicht (sehr spezifisch zu uns) - for (keyword in HIGH_CONFIDENCE_KEYWORDS) { - if (joined.contains(keyword)) { - Log.d(TAG, "settings-watch: high-confidence keyword match: '$keyword'") - return "high-confidence:$keyword" - } - } - - // Standard-Cluster: min 2 Keywords nötig (false-positive-Schutz) - for ((cluster, keywords) in DANGEROUS_TEXT_CLUSTERS) { - val matchCount = keywords.count { joined.contains(it) } - if (matchCount >= 2) { - Log.d(TAG, "settings-watch: cluster $cluster matched $matchCount keywords") - return cluster - } - } - return null + return joined } private fun collectAllText(node: AccessibilityNodeInfo?, sink: MutableList, depth: Int) { @@ -239,6 +254,8 @@ class RebreakAccessibilityService : AccessibilityService() { * High-confidence Keywords — wenn EINER davon im Window-Content auftaucht, * blocken wir sofort. Hochspezifisch zu uns. Enthält sowohl die aktuelle * a11y-Service-Summary als auch die alte (für stale Installs / OEM-Cache). + * + * Müssen lowercase sein (Text wird vor Match lowercased). */ val HIGH_CONFIDENCE_KEYWORDS = listOf( "rebreak filter", // VPN-Profil-Name aus Builder.setSession @@ -249,55 +266,6 @@ class RebreakAccessibilityService : AccessibilityService() { "rebreak löschen", ) - /** - * Standard-Cluster — min 2 Keywords pro Cluster nötig damit - * harmlose Settings-Suche keine false-positives auslöst. - */ - val DANGEROUS_TEXT_CLUSTERS = mapOf( - "vpn-page" to listOf( - "vpn", - "rebreak filter", // unser Profil-Name (siehe Builder.setSession) - "always-on", - "always-on-vpn", - "verbindung trennen", - "trennen", - "verbindungen ohne vpn", - "block connections", - "vpn-profil", - "konto entfernen", - "vergessen", - "always-on vpn", - ), - "a11y-page" to listOf( - "bedienungshilfe", - "eingabehilfe", - "accessibility", - "sichert den schutz", // unsere aktuelle a11y-Service-Summary - "filtert glücksspiel", // alte a11y-Service-Summary (legacy installs) - "rebreak filter", - "installierte apps", - "installed services", - "downloaded apps", - "berechtigung erteilen", - "service deaktivieren", - "service ausschalten", - ), - "uninstall-page" to listOf( - "deinstallieren", - "uninstall", - "rebreak", - "möchten sie diese app", - "do you want to uninstall", - "app entfernen", - "force stop", - "stopp erzwingen", - "speicher", - "daten löschen", - "clear data", - "cache leeren", - ), - ) - val DANGEROUS_ACTIVITY_PATTERNS = listOf( // VPN-Settings + VPN-Profil-Dialoge "VpnSettings", diff --git a/apps/rebreak-native/modules/rebreak-protection/ios/RebreakContentFilter/Info.plist b/apps/rebreak-native/modules/rebreak-protection/ios/RebreakContentFilter/Info.plist index 9555002..d4b279b 100644 --- a/apps/rebreak-native/modules/rebreak-protection/ios/RebreakContentFilter/Info.plist +++ b/apps/rebreak-native/modules/rebreak-protection/ios/RebreakContentFilter/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString 0.3.13 CFBundleVersion - 41 + 45 NSExtension NSExtensionPointIdentifier diff --git a/apps/rebreak-native/modules/rebreak-protection/ios/RebreakPacketTunnelExtension/Info.plist b/apps/rebreak-native/modules/rebreak-protection/ios/RebreakPacketTunnelExtension/Info.plist index f71f74d..0fa1e6f 100644 --- a/apps/rebreak-native/modules/rebreak-protection/ios/RebreakPacketTunnelExtension/Info.plist +++ b/apps/rebreak-native/modules/rebreak-protection/ios/RebreakPacketTunnelExtension/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString 0.3.13 CFBundleVersion - 41 + 45 NSExtension NSExtensionPointIdentifier diff --git a/apps/rebreak-native/modules/rebreak-protection/ios/RebreakURLFilterExtension/Info.plist b/apps/rebreak-native/modules/rebreak-protection/ios/RebreakURLFilterExtension/Info.plist index 21d165b..f6c8668 100644 --- a/apps/rebreak-native/modules/rebreak-protection/ios/RebreakURLFilterExtension/Info.plist +++ b/apps/rebreak-native/modules/rebreak-protection/ios/RebreakURLFilterExtension/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString 0.3.13 CFBundleVersion - 41 + 45 EXAppExtensionAttributes EXExtensionPointIdentifier diff --git a/apps/rebreak-native/stores/appLock.ts b/apps/rebreak-native/stores/appLock.ts index 5c84aac..7dce9ef 100644 --- a/apps/rebreak-native/stores/appLock.ts +++ b/apps/rebreak-native/stores/appLock.ts @@ -56,7 +56,7 @@ type AppLockState = { }; export const useAppLockStore = create((set, get) => ({ - enabled: false, + enabled: true, locked: false, available: false, ready: false, diff --git a/apps/rebreak-native/stores/notificationPrefs.ts b/apps/rebreak-native/stores/notificationPrefs.ts index ba59aef..dd4a0a9 100644 --- a/apps/rebreak-native/stores/notificationPrefs.ts +++ b/apps/rebreak-native/stores/notificationPrefs.ts @@ -23,16 +23,19 @@ async function persist(patch: Partial((set, get) => ({ - pushEnabled: false, + pushEnabled: true, streakReminderEnabled: false, streakReminderTime: { hour: 9, minute: 0 }, init: async () => { const stored = await AsyncStorage.getItem(STORAGE_KEY); - if (!stored) return; + if (!stored) { + await persist({ pushEnabled: true }); + return; + } const parsed = JSON.parse(stored); set({ - pushEnabled: parsed.pushEnabled ?? false, + pushEnabled: parsed.pushEnabled ?? true, streakReminderEnabled: parsed.streakReminderEnabled ?? false, streakReminderTime: parsed.streakReminderTime ?? { hour: 9, minute: 0 }, }); diff --git a/apps/rebreak-native/tmp/.deploy-runtimes b/apps/rebreak-native/tmp/.deploy-runtimes new file mode 100644 index 0000000..80fd825 --- /dev/null +++ b/apps/rebreak-native/tmp/.deploy-runtimes @@ -0,0 +1,17 @@ +Validating IPA (App-Store Connect)|60 +Uploading zu App-Store Connect (TestFlight)|90 +Android: Gradle Bundle Release|180 +Validating IPA (App-Store Connect)|75 +Uploading zu App-Store Connect (TestFlight)|115 +Validating IPA (App-Store Connect)|98 +Uploading zu App-Store Connect (TestFlight)|166 +Building Release AAB (gradlew bundleRelease)|344 +Validating IPA (App-Store Connect)|83 +Uploading zu App-Store Connect (TestFlight)|102 +Building Release AAB (gradlew bundleRelease)|356 +Building xcarchive|225 +Exporting Ad-Hoc IPA|18 +Exporting App-Store IPA|24 +Validating IPA (App-Store Connect)|94 +Uploading zu App-Store Connect (TestFlight)|105 +Building Release AAB (gradlew bundleRelease)|356 diff --git a/ops/BUSINESS_PLAN_NBANK.md b/ops/BUSINESS_PLAN_NBANK.md index 50d5350..90da5cc 100644 --- a/ops/BUSINESS_PLAN_NBANK.md +++ b/ops/BUSINESS_PLAN_NBANK.md @@ -39,11 +39,11 @@ 2. **Echtzeit-Mail-Schutz** — IMAP-IDLE-Daemon, der Casino-Werbemails löscht, bevor die Push-Benachrichtigung am Gerät auslöst (eindeutiges Alleinstellungsmerkmal im Markt). 3. **24/7-KI-Begleitung in Drucksituationen** — der KI-Coach „Lyra" liefert Soforthilfe zwischen Beratungsterminen, verweist aktiv an Profi-Strukturen (DigiSucht, lokale Fachstellen) und ersetzt diese ausdrücklich nicht. -Optional steht der **Selbstbindungs-Modus „RebReakBinder"** zur Verfügung (Lock-Architektur, die App und Filter ohne Vertrauensperson nicht mehr deinstallierbar macht) — ein Schutz-Layer, den kein anderer Wettbewerber in Deutschland anbietet. +Optional steht der **Selbstbindungs-Modus „Rebreak Magic"** zur Verfügung (Lock-Architektur, die App und Filter ohne Vertrauensperson nicht mehr deinstallierbar macht) — ein Schutz-Layer, den kein anderer Wettbewerber in Deutschland anbietet. **Marktpotenzial (konservativ).** Zielmarkt Deutschland: ca. 1,3 Mio. problematische/pathologische Spielende + ca. 367.000 OASIS-Gesperrte + Angehörige (Faktor ~3 pro Betroffenem). **Erreichbarer Markt (SOM) Jahr 3: 30.000 zahlende Nutzer** ≙ ca. 0,8 % Penetration der Kernzielgruppe. Sekundär: **B2B-Lizenzierung** an Suchtberatungs-Träger (~1.400 Fachstellen DE) und mittelfristig **DiGA-Listung (BfArM)** mit Erstattung durch gesetzliche Krankenkassen (Größenordnung ~200 € / Quartal / Nutzer). -**Stand heute.** App **live in geschlossener Beta**. Kernfeatures (DNS-Block, IMAP-IDLE-Mail-Schutz, Lyra, Streak-Tracker, Multi-Device, RebReakBinder Build 19) sind ausgerollt und in Praxistest. Outreach an Suchtfachstellen Niedersachsen (LSG-Nds, NLS, STEP, Lukas-Werk, MHH) hat begonnen. **Pricing: Pro 3,99 €/Monat · Legend 7,99 €/Monat** (zzgl. 14-Tage-Trial; **kein Free-Tier**). Stripe-Web-Checkout (kein In-App-Purchase wegen Apple/Google-Glücksspiel-Policies). +**Stand heute.** App **live in geschlossener Beta**. Kernfeatures (DNS-Block, IMAP-IDLE-Mail-Schutz, Lyra, Streak-Tracker, Multi-Device, Rebreak Magic Build 19) sind ausgerollt und in Praxistest. Outreach an Suchtfachstellen Niedersachsen (LSG-Nds, NLS, STEP, Lukas-Werk, MHH) hat begonnen. **Pricing: Pro 3,99 €/Monat · Legend 7,99 €/Monat** (zzgl. 14-Tage-Trial; **kein Free-Tier**). Stripe-Web-Checkout (kein In-App-Purchase wegen Apple/Google-Glücksspiel-Policies). **Finanzierungsbedarf.** **75.000 € NBank-Gründungskredit** zur Finanzierung von DiGA-Wirksamkeitsstudie, IT-Sicherheit/ISMS-Aufbau, Marketing/B2B-Outreach, Gründer-Lohn-Puffer und externer Beratung (BfArM, Datenschutz). Damit erreicht Rebreak innerhalb von 24 Monaten eine **belastbare Marktposition als deutscher Schutz-Tech-Anbieter mit eingereichtem BfArM-Antrag und ersten institutionellen Kooperationen** (LOI Lukas-Werk Q3/2026 angestrebt). @@ -106,7 +106,7 @@ Rebreak ist eine **native Multi-Plattform-App** (iOS, Android, macOS) mit server | 1. Geräteschutz | DNS-/URL-Filter + plattformspezifische Schutzmechaniken | ja | | 2. Mail-Schutz | IMAP-IDLE-Daemon, Echtzeit-Filterung von Casino-Werbemails | ja | | 3. Begleitung | KI-Coach „Lyra" (Crisis-Mode + Coach-Mode), Streak, Community | ja | -| 4. Selbstbindung | RebReakBinder (optionaler Lock-Modus für iOS) | ja (Build 19) | +| 4. Selbstbindung | Rebreak Magic (optionaler Lock-Modus für iOS) | ja (Build 19) | ## 3.2 Geräteschutz (Layer 1) @@ -153,7 +153,7 @@ Rebreak ist eine **native Multi-Plattform-App** (iOS, Android, macOS) mit server - **Community-Bereich** — moderierte Posts, Reaktion-System, keine privaten DMs (bewusste Friktion zur Vermeidung von Wett-Verabredungen). - **Onboarding** — Selbsteinschätzung-Fragebogen (angelehnt an SOGS-Items), Setup-Assistent für Mail-Konten, optionale Trustee-Verbindung. -## 3.6 RebReakBinder — Selbstbindungs-Modus (optional) +## 3.6 Rebreak Magic — Selbstbindungs-Modus (optional) - macOS-Begleit-App (Build 19, seit 29.05.2026), die das **Self-Bind-MDM-Setup** auf wenige Klicks reduziert. - Ergebnis: iPhone ist supervised, Rebreak-App + DNS-Filter **können nicht mehr via Settings entfernt werden**. @@ -229,7 +229,7 @@ Rebreak ist bewusst **plattformübergreifend nativ**, nicht hybrid. Grund: der S ### Lücke 4 — Geräte-Bypass - Auch wer freiwillig DNS-Filter setzt, kann sie in 30 Sekunden wieder löschen — meist im Moment des stärksten Impulses. -- **Rebreak schließt diese Lücke** durch optionalen RebReakBinder (Selbstbindung mit Trustee-Recovery). +- **Rebreak schließt diese Lücke** durch optionalen Rebreak Magic (Selbstbindung mit Trustee-Recovery). ## 4.3 Marktgröße (Top-Down + Bottom-Up) @@ -280,7 +280,7 @@ Rebreak ist bewusst **plattformübergreifend nativ**, nicht hybrid. Grund: der S |---|---| | Beziehung zur betroffenen Person | Partnerin / Mutter / Schwester | | Hauptbedürfnis | Kontrolle ohne Konfrontation, technische Hilfe ohne Detektivarbeit | -| Rolle in Rebreak | Trustee (RebReakBinder-Recovery), passive Mit-Nutzung der Streak-Sicht, evtl. eigener Account für Selbsthilfe | +| Rolle in Rebreak | Trustee (Rebreak Magic-Recovery), passive Mit-Nutzung der Streak-Sicht, evtl. eigener Account für Selbsthilfe | ## 5.3 Tertiäre Persona — B2B-Multiplikator „Fachstellenleiter Dr. K." (Phase 2) @@ -299,7 +299,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) | |---|---|---|---|---|---|---|---|---| -| **Rebreak** | DE | iOS, Android, **macOS** | ✅ (~330k Domains) | ✅ **IMAP-IDLE Echtzeit** | ✅ Lyra (DE) | ✅ | ✅ RebReakBinder | 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.) | | BetBlocker | UK (Charity) | iOS, Android, Win, Mac | ✅ | ❌ | ❌ | nein | timer-basiert | kostenlos | | GamBlock | AU | Win, Mac, Android | ✅ | ❌ | ❌ | nein | stark | ~5–9 € | @@ -314,7 +314,7 @@ Rebreak ist bewusst **plattformübergreifend nativ**, nicht hybrid. Grund: der S | **Einziger Anbieter mit IMAP-IDLE-Mail-Schutz** in DE | Schließt Trigger-Kanal Werbemail vollständig | Technisch aufwändig (Server-Infra, OAuth-Integration); 12–18 Monate Vorsprung | | **macOS-nativer DNS-Schutz** kombiniert mit iOS/Android | Wettbewerber decken meist nur Mobile oder nur Desktop | Mittel (Apple-Tech-Investment nötig) | | **Deutscher Sitz, DSGVO-konform, deutschsprachiger KI-Coach** | Akzeptanz bei Fachstellen, Krankenkassen, BfArM | Hoch — internationale Player können das nicht „nachrüsten" | -| **Selbstbindungs-Modus (RebReakBinder)** mit Trustee | Stärkster verfügbarer Anti-Rückfall-Mechanismus auf iOS am Markt | Mittel — Apple-Policy-Risiko vorhanden, Architektur empirisch validiert | +| **Selbstbindungs-Modus (Rebreak Magic)** mit Trustee | Stärkster verfügbarer Anti-Rückfall-Mechanismus auf iOS am Markt | Mittel — Apple-Policy-Risiko vorhanden, Architektur empirisch validiert | | **Lyra — KI-Begleiter in DE Sprache** | Brückenfunktion zwischen Beratungsterminen | Mittel — andere können nachziehen, aber Persona/Vokabular sind Asset | | **Stripe-Web-Checkout** | Vermeidet Apple/Google-Cut **und** Glücksspiel-Store-Policies | Hoch — strukturell | | **B2B-Anschlussfähigkeit Fachstellen DE** | Vertriebskanal jenseits ASO | Hoch (Beziehungs-Asset) | @@ -418,7 +418,7 @@ Drei Kern-Botschaften: 1. **„Schutz, wo OASIS endet."** — sachlich, faktisch, kein Pathos. 2. **„Lyra ist da, wenn die Beraterin gerade nicht da ist."** — Komplementär-Position, keine Therapie-Behauptung. -3. **„Du entscheidest. Auch wenn du später anders entscheidest."** — Selbstbindungs-Logik (RebReakBinder) ohne Bevormundung. +3. **„Du entscheidest. Auch wenn du später anders entscheidest."** — Selbstbindungs-Logik (Rebreak Magic) ohne Bevormundung. ## 8.5 PR-Anker 2026/27 @@ -516,7 +516,7 @@ Antrag │ FAGS-2.Welle │ Pen-Test bestanden │ Start delphi/MHH │ er ### Q2/2026 (jetzt, vor Förderung) - NBank-Antragsunterlagen final. -- Beta-Stabilisierung Build 19 (RebReakBinder). +- Beta-Stabilisierung Build 19 (Rebreak Magic). - Outreach-Welle 1 Niedersachsen (LSG, NLS, MHH, STEP). - Eigenmittel-Runway: [PLATZHALTER: Monate]. @@ -664,12 +664,12 @@ Die NBank-Förderung deckt damit explizit die **Wachstums- und Compliance-Posten |---|---|---|---|---| | 1 | DiGA-Antrag wird abgelehnt | Mittel (BfArM lehnt ~40 % erstmaliger Anträge teilweise ab) | Hoch | **B2B-First-Pivot:** Lizenzierung an Fachstellen-Träger als Haupt-Erlöskanal; DiGA-Pfad als langfristige Option neu aufsetzen. Studienergebnisse bleiben verwertbar (B2B-Argument, PR, wissenschaftliche Glaubwürdigkeit). | | 2 | Apple / Google App-Store-Policy-Änderung | Mittel | Hoch | **Web-First-Failover:** macOS-App + PWA-Variante bereits in Architektur vorgesehen; Mail-Schutz funktioniert plattformunabhängig; Stripe-Web-Checkout schon heute Standard (keine IAP-Abhängigkeit). | -| 3 | OASIS-Reform 2026/27 deckt Offshore mit ab | Niedrig–Mittel | Mittel | OASIS-Reform würde Jahre brauchen, regulatorisch komplex (kein Hoheitsrecht über Offshore-Server). Mail-Schutz, Lyra, RebReakBinder bleiben einzigartig. Rebreak positioniert sich offen als **OASIS-Ergänzung**, nicht -Konkurrenz — Reform wäre eher Rückenwind. | +| 3 | OASIS-Reform 2026/27 deckt Offshore mit ab | Niedrig–Mittel | Mittel | OASIS-Reform würde Jahre brauchen, regulatorisch komplex (kein Hoheitsrecht über Offshore-Server). Mail-Schutz, Lyra, Rebreak Magic bleiben einzigartig. Rebreak positioniert sich offen als **OASIS-Ergänzung**, nicht -Konkurrenz — Reform wäre eher Rückenwind. | | 4 | Solo-Founder-Bus-Factor | Mittel | Hoch | **Hire-Plan Q1/2027** (CTO/Full-Stack); zwischenzeitlich: Tech-Stack vollständig dokumentiert, Code-Reviews extern, Notfall-Mandat („Bus-Factor-Treuhänder") mit klar definiertem Zugang zu kritischer Infrastruktur. | | 5 | DSGVO-Vorfall im Mail-Schutz | Niedrig | Sehr hoch | DPIA vor Skalierung (Anwaltsbudget); Mail-Inhalte nicht persistiert (in-memory only); regelmäßiger Pen-Test (12 k€-Posten); Audit-Logs revisionssicher. | | 6 | Großer internationaler Player lokalisiert auf DE | Niedrig | Mittel | Trust-Vorsprung durch Fachstellen-LOIs, DE-Sitz, DiGA-Pfad. Internationale Player haben Schwierigkeiten, deutschsprachige Fachstellen-Strukturen zu durchdringen. | | 7 | Marketing-Wirkung bleibt hinter Plan | Mittel | Mittel | B2B-Multiplikator-Kanal hat geringere Stückkosten als Performance-Ads; Conversion-Erwartungen sind konservativ angesetzt (s. Kapitel 4); Pricing leicht reduzierbar bei Bedarf (kein Markenschaden, da von Beginn an niedriges Niveau). | -| 8 | RebReakBinder-Architektur wird von Apple sanktioniert | Niedrig–Mittel | Mittel | RebReakBinder ist **opt-in**, basiert auf dokumentierten Apple-Konfigurations-Mechanismen, keine Jailbreaks/Exploits. Fallback: klassisches Lock-Modus-Profil via Safari (vor RebReakBinder-Build 19 etablierter Weg). | +| 8 | Rebreak Magic-Architektur wird von Apple sanktioniert | Niedrig–Mittel | Mittel | Rebreak Magic ist **opt-in**, basiert auf dokumentierten Apple-Konfigurations-Mechanismen, keine Jailbreaks/Exploits. Fallback: klassisches Lock-Modus-Profil via Safari (vor Rebreak Magic-Build 19 etablierter Weg). | | 9 | KI-Anbieter-Lock-in (Groq/Anthropic) | Niedrig | Niedrig–Mittel | Lyra-Persona ist anbieter-unabhängig spezifiziert (Single Source of Truth); Wechsel zu alternativem LLM in ~2 Wochen Engineering machbar. | | 10 | Wirksamkeitsstudie zeigt keinen signifikanten Effekt | Mittel | Hoch | Studiendesign so wählen, dass Sekundärendpunkte (Nutzungsdauer, Streak-Längen, Selbstwirksamkeitsscores) belastbar erhoben werden — auch ohne Primärendpunkt-Signifikanz lässt sich Versorgungs-Nutzen argumentieren. Vor Studienstart **Pre-Registration** und gut definierte Endpunkte. | @@ -724,11 +724,11 @@ Reale LOI-Originale werden im Nachgang separat eingereicht, sobald unterschriebe ## E. Screenshots der App -[PLATZHALTER: Onboarding-Screen, Streak-Tab, Lyra-Coach-Chat, Mail-Schutz-Übersicht, Schutz-Status-Screen, RebReakBinder-Setup-Screen — 6 Screenshots, idealerweise iOS und macOS.] +[PLATZHALTER: Onboarding-Screen, Streak-Tab, Lyra-Coach-Chat, Mail-Schutz-Übersicht, Schutz-Status-Screen, Rebreak Magic-Setup-Screen — 6 Screenshots, idealerweise iOS und macOS.] ## F. Lyra-Persona / Produkt-Spezifikation (Auszug) -Auf Anforderung wird die vollständige Lyra-Persona-Spezifikation und die technische Architektur-Übersicht (RebReakBinder, IMAP-IDLE-Daemon, NEFilter-Setup) als separates Anlagen-Dokument bereitgestellt. +Auf Anforderung wird die vollständige Lyra-Persona-Spezifikation und die technische Architektur-Übersicht (Rebreak Magic, IMAP-IDLE-Daemon, NEFilter-Setup) als separates Anlagen-Dokument bereitgestellt. ## G. Kontakt diff --git a/ops/mdm/MAC_SUPERVISION_RESEARCH.md b/ops/mdm/MAC_SUPERVISION_RESEARCH.md new file mode 100644 index 0000000..05724e7 --- /dev/null +++ b/ops/mdm/MAC_SUPERVISION_RESEARCH.md @@ -0,0 +1,546 @@ +# Mac "Supervision" Research — TechLockdown-Analyse & Replizierbarkeit + +**Research-Datum:** 30. Mai 2026 +**Ziel:** Verstehen wie TechLockdown Mac-"Supervision" implementiert und ob/wie wir das für RebreakBinder-Mac replizieren können. + +--- + +## Executive Summary + +**KRITISCH: macOS hat KEIN "Supervised Mode" im iOS-Sinn.** + +Was iOS "Supervision" ist (Device-Wipe via Apple Configurator oder DEP/ABM, persistent supervised-Flag, volle MDM-Control) **existiert auf Mac NICHT**. Was es gibt: + +1. **UAMDM** (User-Approved MDM Enrollment) — User installiert MDM-Profil manuell, kein Wipe +2. **Automated Device Enrollment (ADE) via ABM** — Device-Zuordnung über Apple Business Manager, **KANN** Wipe erfordern oder Apple-Configurator-Re-Add +3. **Configuration Profiles** (.mobileconfig) — können viele Restrictions setzen, OHNE MDM oder ABM + +**TechLockdown-Kernfinding:** +- ✅ Nutzen Configuration Profiles (.mobileconfig) — "Config Files" in ihrem Marketing +- ✅ KEIN Device-Wipe erforderlich +- ✅ KEIN ABM/Apple Business Manager erforderlich (nach Public-Info) +- ⚠️ Unklar: betreiben sie eigenes MDM (UAMDM) oder nur statische .mobileconfig-Downloads? +- ✅ Profile-Schutz: "Uninstall Prevented" → RemovalPassword-Feld in .mobileconfig +- ✅ Dashboard-Feature "Profile Locking" (unlock delay, random text entry, accountability notifications) ist ZUSÄTZLICH zum technischen Schutz + +**Replizierbarkeit für RebreakBinder-Mac: HOCH (80-90%)** +- Technisch machbar in 2-4 Wochen für MVP +- Hauptdifferenz zu iOS: keine No-Erase-Supervision (weil es das Konzept auf Mac nicht gibt) +- Pfad: UAMDM + .mobileconfig mit Restrictions + Dashboard-generierte Profiles + +--- + +## 1. TechLockdown-Findings (verifiziert via techlockdown.com) + +### 1.1 Hauptmechanik: Apple Configuration Profiles + +**Quelle:** https://techlockdown.com/articles/block-porn-mac (fetched 30.05.2026) + +TechLockdown nennt Configuration Profiles "Config Files" und beschreibt sie als: +> "Config Files, also known as Apple Configuration Profiles, are used to set restrictions on a Mac computer, similar to Screen Time or parental controls." + +**Was sie damit machen:** + +| Feature | Mechanik | Verifiziert? | +|---------|----------|--------------| +| Browser-Content-Filter | Configuration Profile mit Custom-Payload für Safari/Chrome Web Content Filter | ✅ (erwähnt in Article) | +| Private-Browsing blockieren | Restrictions-Payload → `allowPrivateBrowsing: false` (Safari/Chrome) | ✅ | +| SafeSearch erzwingen | DNS-Content-Policy (Google: 216.239.38.120, Bing: DNS-Filter) | ✅ | +| Browser-Extensions blocken | Restrictions-Payload → Extension-Allowlist/Blocklist | ✅ (erwähnt) | +| Extension-Store blocken | `allowExtensionsStore: false` (Chrome/Edge) | ✅ (erwähnt) | +| DNS-Filter | Configuration Profile mit DNS-Settings oder DNSProxy-Payload | ✅ | +| hosts-File-Modifikation | Erwähnt als Alternative zu DNS | ✅ (nicht via Profil, manuell) | + +### 1.2 Profile-Schutz-Mechanik + +**TechLockdown-Feature: "Uninstall Prevented"** + +Aus ihrem Mac-Article: +> "When you download your Config Presets, you have the option to require a password in order to remove your Config Files." + +**Technische Umsetzung (Standard-Apple-Mechanik):** +```xml +PayloadRemovalDisallowed + +``` +ODER (älter, deprecated): +```xml +RemovalPassword +hashed_password_here +``` + +**Wichtig:** `PayloadRemovalDisallowed` verhindert User-Removal NUR wenn Profil via MDM gepusht wurde (nicht bei manueller Installation). Bei manueller Installation ist `RemovalPassword` der einzige Schutz → User muss Passwort kennen um Profil zu löschen. + +### 1.3 "Profile Locking" — Dashboard-Feature (NICHT Apple-Mechanik) + +**Quelle:** https://techlockdown.com/features/profile-locking + +TechLockdown's Dashboard bietet zusätzliche Schutz-Layer: +- **Unlock Delay:** Profile erst nach z.B. 5 Minuten unlockbar (künstliche Verzögerung) +- **Random Text Entry:** User muss exakten String abtippen (z.B. `aB3cD5eF7gH9{J1k<3m#5oP7qR9sT1uV3wX5&Z7`) +- **Accountability Notifications:** Email an Trusted Person bei Unlock/Lock + +**KRITISCH:** Dies ist KEIN Apple-OS-Feature. Das ist ihre Web-App-Logik. Der User muss sich auf techlockdown.com einloggen um das Password für Profile-Removal zu sehen → während des Logins erzwingen sie delay+random-text+notification. + +**Hypothese (NICHT verifiziert, aber logisch):** +1. User installiert .mobileconfig mit `RemovalPassword: "random_secure_hash"` +2. User kennt Passwort NICHT (TechLockdown generiert es, zeigt es nicht sofort) +3. Wenn User Profil entfernen will → macOS fragt nach Password +4. User muss auf TechLockdown-Dashboard gehen → "Unlock Profile" klicken +5. Dashboard zeigt Password erst nach delay+random-text+notification + +**Das bedeutet:** Technisch ist es nur RemovalPassword. Der "Locking"-Layer ist Web-App-UX. + +### 1.4 "Managed Mode" für Browser-Extensions + +TechLockdown erwähnt: +> "Tech Lockdown members will also have the option to enforce browser restrictions with managed mode on their Mac devices: You can hide the option to uninstall any extension you choose." + +**Was "Managed Mode" bedeutet (Apple-Definition):** +- Browser-Extensions können als "managed" markiert werden wenn sie via MDM/Configuration-Profile installiert werden +- Managed Extensions: User kann sie nicht deinstallieren (Option ist ausgegraut/versteckt) +- Extension-Allowlist/Blocklist via Configuration Profile + +**Payload-Beispiel (Chrome):** +```xml +ExtensionSettings + + EXTENSION_ID + + installation_mode + force_installed + update_url + https://clients2.google.com/service/update2/crx + + +``` + +**WICHTIG:** Managed Extension Installation funktioniert NUR mit: +- MDM-gepushten Profiles ODER +- Configuration Profiles die vom User approved sind (UAMDM) ODER +- Profiles signiert mit Device Management Cert + +### 1.5 Was TechLockdown NICHT erwähnt + +❌ Device-Wipe / Erase +❌ Apple Configurator +❌ Apple Business Manager (ABM) +❌ Supervision (iOS-Konzept) +❌ System Extensions (NEFilterProvider etc.) +❌ Device Enrollment Program (DEP) +❌ "Automated Device Enrollment" + +**Interpretation:** TechLockdown verwendet den **simpelsten Pfad** — Configuration Profiles + optional UAMDM (wenn sie eigenes MDM haben). + +--- + +## 2. Apple-Mechanik Deep-Dive: macOS MDM vs. iOS MDM + +### 2.1 "Supervision" auf Mac existiert NICHT + +| Konzept | iOS | macOS | +|---------|-----|-------| +| **Supervised Mode** | ✅ Existiert (via Configurator oder DEP) | ❌ Existiert nicht | +| **Supervised-Flag** | ✅ Persistent nach Wipe/Setup-Assistant | ❌ Kein Äquivalent | +| **Requires Device-Wipe** | ✅ Ja (außer via DEP/ABM) | N/A | +| **Full MDM Control** | ✅ Supervised = volle Restrictions | ⚠️ Abhängig von UAMDM vs. ADE | +| **User-Removal von Profilen** | Supervised: Nur MDM kann entfernen | UAMDM: User KANN entfernen (außer RemovalPassword) | + +### 2.2 macOS Enrollment-Pfade + +#### Option 1: **UAMDM (User-Approved MDM Enrollment)** + +**Flow:** +1. User bekommt .mobileconfig-Download-Link (via Safari) +2. User öffnet .mobileconfig → macOS zeigt "Profil heruntergeladen" Notification +3. User geht zu **System Settings → Privacy & Security → Profiles** +4. User klickt "Install" → **macOS zeigt MDM-Consent-Dialog** ("This will allow XYZ to manage this Mac") +5. User muss Admin-Passwort eingeben → MDM-Enrollment abgeschlossen + +**Eigenschaften:** +- ✅ KEIN Device-Wipe +- ✅ KEIN Apple Business Manager nötig +- ✅ User behält Control (kann MDM-Profil jederzeit entfernen — außer RemovalPassword gesetzt) +- ⚠️ Einige MDM-Payloads funktionieren NUR mit UAMDM (z.B. Kernel-Extension-Approval, SystemExtension-Silent-Install) +- ⚠️ User sieht dass MDM installiert ist (in System Settings) + +**Schutz gegen Removal:** +```xml +RemovalPassword +HASHED_PASSWORD +``` +→ User muss Passwort kennen um Profil zu löschen + +#### Option 2: **Automated Device Enrollment (ADE) via ABM** + +**Flow:** +1. Device wird in Apple Business Manager (ABM) registriert + - Via Apple Authorized Reseller (bei Kauf) + - Via Apple Configurator iPhone-App (nachträglich, nur während Setup-Assistant) +2. Device aktiviert → Setup-Assistant zeigt "Remote Management" Screen +3. Device enrollt automatisch in vorkonfiguriertes MDM +4. MDM pusht Profiles → User kann NICHT ablehnen + +**Eigenschaften:** +- ✅ Automatisch, kein User-Klick-Fiesta +- ✅ Profile sind "supervised-like" (User kann sie nicht entfernen) +- ❌ **Kann** Device-Wipe erfordern (abhängig ob Device schon eingerichtet war) +- ❌ **Braucht ABM-Account** (kostenlos, aber Apple-Business-Verifizierung nötig) +- ❌ Device muss bei Apple als "managed" registriert sein + +**KRITISCH für RebreakBinder:** ADE ist **overkill** für Consumer-Use-Case. ABM ist für Unternehmen/Schulen gedacht. Consumer-Devices nachträglich zu ABM hinzuzufügen ist kompliziert (nur via Configurator-iPhone-App während Setup-Assistant → Device-Wipe). + +#### Option 3: **Statische Configuration Profiles (kein MDM)** + +**Flow:** +1. User lädt .mobileconfig herunter +2. User öffnet → System Settings → Profiles → Install +3. Profil ist installiert + +**Eigenschaften:** +- ✅ Einfachst möglich +- ✅ KEIN MDM-Server nötig +- ❌ KEINE "managed" Features (z.B. Extensions können nicht force-installed werden) +- ❌ User kann Profil jederzeit entfernen (außer RemovalPassword) +- ❌ MDM-Push-Updates nicht möglich (User muss neue Profiles manuell installieren) + +**Nutzbar für:** DNS-Filter, Browser-Restrictions, SafeSearch, aber NICHT für Managed-Extension-Install + +### 2.3 Welche Restrictions brauchen Supervision/ABM? + +**Apple's offizielle Doku ist unklar** — meine Research zeigt: + +| Restriction/Payload | UAMDM (ohne ABM) | ADE (via ABM) | Static Profile | +|---------------------|------------------|---------------|----------------| +| DNS-Settings | ✅ | ✅ | ✅ | +| com.apple.applicationaccess (Restrictions) | ✅ | ✅ | ✅ | +| Browser-Restrictions (Private-Browsing etc.) | ✅ | ✅ | ✅ | +| SystemExtensions-Silent-Approval | ✅ (UAMDM required) | ✅ | ❌ | +| Managed Browser Extensions | ✅ (mit UAMDM-MDM) | ✅ | ❌ | +| com.apple.webcontent-filter (iOS-only?) | ❌ (iOS-only laut Doku) | ❌ | ❌ | +| NEFilterProvider System Extension | ⚠️ Kompliziert (siehe 2.4) | ✅ | ❌ | + +### 2.4 System Extensions auf Mac (NEFilterProvider) + +**Wenn wir eine NEFilterProvider System Extension für Mac bauen wollen (analog zu iOS NEFilter):** + +**Requirements:** +1. macOS App mit System Extension Target +2. Developer-ID-Signierung (NICHT App-Store-Signierung) +3. Notarization via Apple +4. System Extension Entitlement: `com.apple.developer.system-extension.install` +5. Network Extension Entitlement: `com.apple.developer.networking.networkextension` + +**Installation-Pfade:** + +| Pfad | User-Approval nötig? | Silent Install möglich? | +|------|----------------------|-------------------------| +| Direkt aus App | ✅ Ja (User muss in System Settings → Security klicken) | ❌ | +| Via MDM (UAMDM) + SystemExtensions-Payload | ⚠️ Reduziert (User muss MDM approven, dann silent) | ✅ (nach MDM-Enrollment) | +| Via ABM/ADE | ✅ Silent (kein User-Click) | ✅ | + +**SystemExtensions Payload-Beispiel:** +```xml +PayloadType +com.apple.system-extension-policy +AllowUserOverrides + +AllowedSystemExtensions + + YOUR_TEAM_ID + + org.rebreak.nefilter.extension + + +``` + +**WICHTIG:** Diese Payload funktioniert NUR wenn via **MDM** gepusht (UAMDM oder ADE). Static Profile-Install funktioniert NICHT. + +--- + +## 3. Replizierbarkeits-Matrix: TechLockdown vs. RebreakBinder-Mac + +| Feature | TechLockdown (angenommen) | RebreakBinder-Mac (Pfad 1: UAMDM) | RebreakBinder-Mac (Pfad 2: Static Profiles) | Effort | +|---------|---------------------------|-----------------------------------|---------------------------------------------|--------| +| **DNS-Filter** | ✅ Configuration Profile | ✅ .mobileconfig mit DNS-Proxy/Settings | ✅ .mobileconfig | 2-3 Tage | +| **Browser-Restrictions** (Private-Browsing, Extension-Store) | ✅ Restrictions-Payload | ✅ Restrictions-Payload | ✅ Restrictions-Payload | 3-5 Tage | +| **SafeSearch erzwingen** | ✅ DNS + hosts | ✅ DNS + Restrictions | ✅ DNS + Restrictions | 1-2 Tage | +| **Managed Browser-Extensions** | ✅ (mit UAMDM-MDM) | ✅ (braucht UAMDM-MDM) | ❌ (geht nicht ohne MDM) | 1 Woche (MDM-Server-Setup) | +| **Profile-Schutz** (RemovalPassword) | ✅ RemovalPassword-Feld | ✅ RemovalPassword-Feld | ✅ RemovalPassword-Feld | 1 Tag | +| **"Profile Locking"** (delay, random text, accountability) | ✅ Dashboard-Logic | ✅ Dashboard-Logic (replizierbar) | ✅ Dashboard-Logic | 3-5 Tage (Backend) | +| **NEFilterProvider System-Extension** | ❌ (nicht erwähnt, vermutlich nicht) | ⚠️ Möglich mit UAMDM | ❌ (geht nicht ohne MDM) | 2-3 Wochen (Extension bauen + Signing) | +| **App nicht löschbar** | ❌ (nicht möglich auf Mac ohne ADE) | ❌ (auch mit UAMDM nicht) | ❌ | N/A | +| **Kein Device-Wipe** | ✅ | ✅ | ✅ | N/A | +| **ABM-Account nötig** | ❌ (nach Public-Info) | ❌ (UAMDM = kein ABM) | ❌ | N/A | + +**Legende:** +- ✅ Funktioniert / replizierbar +- ⚠️ Funktioniert mit Einschränkungen +- ❌ Funktioniert nicht / nicht replizierbar + +--- + +## 4. Empfehlung für RebreakBinder-Mac + +### 4.1 MVP-Pfad (2-4 Wochen, 80% TechLockdown-Feature-Parity) + +**Techstack:** +1. **UAMDM-MDM-Server** (NanoMDM — wir haben das schon für iOS) + - Selbst-gehostet auf `mdm-mac.rebreak.org` (oder Subdomain von mdm.rebreak.org) + - Verwendet für: Profile-Push + Updates +2. **Configuration Profiles (.mobileconfig)** mit: + - DNS-Proxy-Payload (AdGuard DNS mit ClientID) + - Restrictions-Payload (Browser-Restrictions, SafeSearch, Extension-Control) + - RemovalPassword (generiert auf Backend, nicht sofort sichtbar) +3. **RebreakBinder-Mac App** (SwiftUI macOS): + - Wizard für MDM-Enrollment (UAMDM-Flow) + - Lokale Checks (iCloud-Locked?, FileVault?, Admin-User?) + - Profile-Download + Install-Anleitung +4. **Backend-Extension** (Nitro): + - `/api/binder/mac/enroll` → generiert UAMDM-Enrollment-Profil + - `/api/binder/mac/profiles` → generiert Restriction-Profiles mit RemovalPassword + - "Profile Locking" Logic (unlock-delay, random-text, accountability-email) + +**Flow:** +1. User öffnet RebreakBinder-Mac → Wizard startet +2. Pre-Flight: Check ob iCloud-Locked, Admin-User, etc. +3. MDM-Enrollment: + - App zeigt `.mobileconfig`-Download für NanoMDM-Enrollment + - User installiert → macOS zeigt UAMDM-Consent + - User approved → Device enrollt in NanoMDM +4. Restriction-Profiles: + - NanoMDM pusht DNS-Filter + Browser-Restrictions + SafeSearch + - Profile hat `RemovalPassword` gesetzt (User kennt es NICHT) +5. Lock-Mechanik: + - User will Profil entfernen → macOS fragt nach Password + - User geht auf rebreak.org/settings → "Unlock Mac Profile" + - Backend zeigt Password erst nach delay + random-text + Email an Parent + +**Vorteile:** +- ✅ KEIN Device-Wipe +- ✅ KEIN ABM nötig +- ✅ Repliziert 80% von TechLockdown +- ✅ Verwendet bestehende NanoMDM-Infrastruktur + +**Nachteile:** +- ⚠️ User kann MDM-Profil theoretisch entfernen (wenn er Admin-Passwort hat) → dann sind alle Restrictions weg +- ⚠️ Weniger "locked-down" als iOS-Supervision (weil Mac kein Supervision-Konzept hat) + +### 4.2 Advanced-Pfad (4-8 Wochen, 95% Feature-Parity + System-Extension) + +**Zusätzlich zu MVP:** +1. **NEFilterProvider System-Extension** (analog zu iOS): + - Mac-App mit System-Extension-Target + - Network-Extension-Filter (blockt auf Netzwerk-Layer) + - Via UAMDM-MDM silent-installed (SystemExtensions-Payload) +2. **Managed Browser-Extension** (Chrome/Safari): + - Extension als "managed" via MDM installiert + - User kann Extension nicht deinstallieren + - Backup-Layer falls DNS-Filter gebypassed wird + +**Vorteile:** +- ✅ Multi-Layer-Blocking (DNS + System-Extension + Browser-Extension) +- ✅ Schwerer zu bypassen als nur DNS-Filter + +**Nachteile:** +- ⚠️ System-Extension braucht Developer-ID + Notarization (2-3 Tage Setup) +- ⚠️ User muss UAMDM approven (sichtbar in System Settings) +- ⚠️ 2-3 Wochen zusätzlicher Entwicklungsaufwand + +### 4.3 Was wir NICHT replizieren können (Mac-Limitierungen) + +❌ **App nicht löschbar** — nur via ADE/ABM möglich, nicht für Consumer-Devices +❌ **"Supervised" Status** — existiert auf Mac nicht +❌ **Erzwungenes MDM** (wie iOS DEP) — User kann UAMDM-Profil entfernen wenn Admin +❌ **Settings-Toggle disablen** (wie iOS NEFilter-Settings) — Mac-System-Settings sind offen + +**ABER:** Mit RemovalPassword + "Profile Locking" (delay+random-text+email) erreichen wir ähnlichen Schutz → User muss bewusst umgehen, kann nicht "aus Versehen" deaktivieren. + +--- + +## 5. Apple Business Manager (ABM) — Do We Need It? + +### 5.1 Was ist ABM? + +- Kostenloser Apple-Service für Unternehmen/Schulen +- Ermöglicht Device-Management ohne User-Touch (Automated Device Enrollment) +- Devices werden bei Kauf vom Reseller automatisch zu ABM hinzugefügt + +### 5.2 ABM-Setup-Prozess + +1. **Apple Business Manager Account erstellen:** + - https://business.apple.com + - Braucht: Business-Name, DUNS-Nummer (oder Business-Verifizierung) + - Approval-Zeit: 1-3 Tage +2. **Devices registrieren:** + - Via Apple Authorized Reseller (bei Neukauf) + - Via Apple Configurator iPhone-App (nachträglich, **nur während Setup-Assistant** → Device-Wipe) + - Via CSV-Upload (Device-Serial-Numbers) +3. **MDM-Server verlinken:** + - ABM → Settings → MDM-Server hinzufügen + - Download ABM-Public-Key + Upload MDM-Server-Cert +4. **Enrollment-Profile erstellen:** + - Definiert welches MDM, welche Restrictions, welcher Setup-Assistant-Skip +5. **Device aktiviert → Auto-Enrollment** + +### 5.3 Brauchen wir das? + +**TechLockdown:** Vermutlich NEIN (keine Erwähnung in Public-Docs) +**RebreakBinder-Mac MVP:** NEIN — UAMDM reicht +**RebreakBinder-Mac Advanced:** NEIN — ABM macht nur Sinn für "owned devices" (Unternehmen kauft Macs für Employees) + +**Für unseren Use-Case (Consumer-Family, eigenes Device):** +- ABM = Overkill +- UAMDM = Sweet Spot (User behält Ownership, wir bekommen MDM-Control) + +**AUSNAHME:** Wenn wir jemals ein "ReBreak-Family-Mac-Rental-Programm" machen (wir kaufen Macs, leasen sie an Families) → dann ABM sinnvoll. + +--- + +## 6. Offene Fragen / Risiken / User-Decisions-Needed + +### 6.1 Offene Fragen + +1. **Kann TechLockdown's "managed mode" für Extensions wirklich ohne MDM?** + - **Hypothese:** NEIN — "managed" braucht MDM. Entweder TechLockdown hat UAMDM oder sie meinen was anderes mit "managed mode" + - **To-Do:** Test mit ihrer Trial → Mac-Setup durchgehen, schauen ob MDM-Enrollment nötig ist + +2. **Wie handlen sie Updates?** + - Wenn UAMDM: MDM-Push für neue Profiles + - Wenn Static: User muss neue .mobileconfig manuell installieren + - **To-Do:** TechLockdown-Trial testen + +3. **Was passiert wenn User Admin-Passwort vergisst?** + - User kann MDM-Profil NICHT entfernen (braucht Admin-PW für System Settings → Profiles → Remove) + - Aber User kann Mac komplett wipen via Recovery-Mode + - **Mitigation:** In unserer Doku klar machen dass Wipe immer möglich ist + +### 6.2 Risiken + +| Risiko | Impact | Likelihood | Mitigation | +|--------|--------|------------|------------| +| User entfernt MDM-Profil via Recovery-Mode-Wipe | HIGH (alles weg) | MEDIUM (motivated user) | Accountability-Layer (Email an Parent bei Profil-Removal-Versuch) | +| User findet RemovalPassword via Keychain/Logs | MEDIUM (Profil entfernbar) | LOW (braucht Tech-Skills) | Password-Hash statt Plaintext, nie loggen | +| macOS-Update bricht MDM-Enrollment | MEDIUM (Re-Enrollment nötig) | LOW (Apple testet MDM gut) | Health-Check im Backend, Push-Notification an Parent bei Device-Offline | +| Apple ändert UAMDM-Mechanik in macOS 16 | HIGH (Flow bricht) | LOW (UAMDM ist stable API) | Stay updated mit Apple-Developer-Betas | + +### 6.3 User-Decisions-Needed + +**Vor Implementation Start:** + +1. **Gehen wir mit UAMDM-MDM oder Static Profiles?** + - UAMDM = mehr Features (managed extensions, silent system extension), aber sichtbar in Settings + - Static = simpler, aber weniger Schutz + - **Empfehlung:** UAMDM (wir haben NanoMDM schon, Effort ist gering) + +2. **System-Extension ja/nein im MVP?** + - Ja = 2-3 Wochen länger, aber besserer Bypass-Schutz + - Nein = schneller MVP, DNS-only-Blocking + - **Empfehlung:** NEIN im MVP, Phase 2 + +3. **Wie kommunizieren wir dass Mac weniger "locked-down" ist als iOS?** + - Mac = User behält mehr Control (ist macOS-Philosophy) + - iOS = Supervision = volle Lockdown + - **Empfehlung:** Transparent sein: "Mac-Blocking ist effektiv aber nicht unbreakable wie iOS" + +4. **ABM-Account beantragen (future-proofing)?** + - Kostet nichts außer Setup-Zeit + - Könnte nützlich sein für Enterprise-Use-Cases später + - **Empfehlung:** JA, aber low-priority (nicht für MVP nötig) + +--- + +## 7. Implementation-Roadmap (wenn GO-Entscheidung) + +### Phase 1: MVP (2-4 Wochen) + +**Week 1: NanoMDM-Mac-Setup + Backend** +- [ ] NanoMDM-Subdomain für Mac (`mdm-mac.rebreak.org` oder Shared mit iOS) +- [ ] Backend-Endpoints: + - `POST /api/binder/mac/enroll` → generiert UAMDM-Enrollment-Profil + - `POST /api/binder/mac/profiles/restrictions` → generiert Restriction-Profile + - `POST /api/binder/mac/unlock` → Profile-Locking-Logic (delay+random-text) +- [ ] Profile-Templates (.mobileconfig): + - DNS-Proxy (AdGuard mit ClientID) + - Restrictions (Browser, SafeSearch, Extension-Control) + - RemovalPassword-Handling + +**Week 2-3: RebreakBinder-Mac App (SwiftUI)** +- [ ] Wizard-UI (analog zu iOS-Version) +- [ ] Pre-Flight-Checks: + - iCloud-Account-Status + - FileVault-Status + - Admin-User-Check +- [ ] MDM-Enrollment-Flow: + - `.mobileconfig`-Download via Safari + - System-Settings-Anleitung (Screenshots) + - Verification (Device erscheint in NanoMDM) +- [ ] Success-Screen + Setup-Completion + +**Week 4: Testing + Docs** +- [ ] Test auf verschiedenen macOS-Versionen (Ventura, Sonoma, Sequoia) +- [ ] User-Doku: "Mac-Setup-Guide" +- [ ] Runbook: "Mac-MDM-Recovery" (was tun wenn Profil weg ist) +- [ ] Beta-Test mit Chahine + Olfa-Macs + +### Phase 2: Advanced (4-8 Wochen, optional) + +**NEFilterProvider System-Extension:** +- [ ] Xcode-Projekt: Mac-App + System-Extension-Target +- [ ] Network-Extension-Code (analog zu iOS NEFilter) +- [ ] Developer-ID-Signierung + Notarization +- [ ] SystemExtensions-Payload für silent install via MDM +- [ ] Testing + macOS-Security-Approval-Flow + +**Managed Browser-Extension:** +- [ ] Chrome-Extension (Web-Content-Blocker) +- [ ] Safari-Extension (Content-Blocker) +- [ ] MDM-Payload für force-install + managed-mode +- [ ] Testing + +--- + +## 8. Quellen & Verifikations-Status + +| Quelle | URL | Verifiziert? | Datum | +|--------|-----|--------------|-------| +| TechLockdown Mac Article | https://techlockdown.com/articles/block-porn-mac | ✅ Fetched & parsed | 30.05.2026 | +| TechLockdown Profile-Locking | https://techlockdown.com/features/profile-locking | ✅ Fetched | 30.05.2026 | +| Apple Configuration Profile Reference | developer.apple.com/business/documentation/ | ⚠️ Fetched PDF, parsing incomplete | 30.05.2026 | +| Apple MDM Protocol Reference | developer.apple.com/business/documentation/ | ⚠️ Fetched, parsing incomplete | 30.05.2026 | +| Apple UAMDM Docs | developer.apple.com/documentation/devicemanagement | ❌ JS-required, couldn't fetch | 30.05.2026 | + +**Verifikations-Grad:** +- ✅ **TechLockdown-Mechanik:** 80% verifiziert (Public-Info, keine Trial getestet) +- ⚠️ **Apple-Mechanik:** 60% verifiziert (Doku-Access limited, basiert auf Known-Knowledge + PDF-Snippets) +- ❌ **TechLockdown-MDM-Server:** 0% verifiziert (nicht public, Hypothese) + +**Nächste Schritte für 100% Verifikation:** +1. TechLockdown-Trial-Account → Mac-Setup durchgehen → schauen ob MDM-Enrollment oder nur .mobileconfig +2. `tcpdump` auf Mac während TechLockdown-Setup → sehen ob MDM-Server-Traffic +3. Installiertes Profil inspizieren: `sudo profiles show -all` → sehen ob RemovalPassword, MDM-Server-URL, etc. + +--- + +## 9. Fazit + +**TechLockdown's Mac-"Supervision" ist KEIN Supervision** (weil das auf Mac nicht existiert). Es ist **UAMDM (wahrscheinlich) + Configuration Profiles + RemovalPassword + clevere Dashboard-UX**. + +**Wir können 80-90% davon replizieren** in 2-4 Wochen mit: +- NanoMDM (haben wir schon) +- .mobileconfig-Profile (DNS, Restrictions, Browser) +- RemovalPassword + "Profile Locking" Backend-Logic +- RebreakBinder-Mac SwiftUI-App als Wizard + +**Was wir NICHT replizieren können:** +- "Supervision" (existiert auf Mac nicht) +- App-nicht-löschbar (nur via ADE/ABM, nicht Consumer-freundlich) + +**Empfehlung:** GO für MVP-Pfad (UAMDM + Profiles), SKIP System-Extension im MVP (Phase 2), SKIP ABM (nicht nötig). + +**Ehrlichkeits-Disclaimer für User:** +"Mac-Blocking ist effektiv und multi-layered (DNS + Browser + Accountability), aber nicht unbreakable wie iOS-Supervision. Ein Mac-User mit Admin-Zugriff kann theoretisch alles umgehen (wie bei jedem Mac-Parental-Control-Tool). Unser Schutz basiert auf Accountability + technischen Hürden, nicht auf absolutem Lock-Down." + +--- + +**Nächster Schritt:** User-Entscheidung über Pfad → dann detailed Implementation-Planning für RebreakBinder-Mac. diff --git a/ops/strategy/OUTREACH_MAILS_READY.md b/ops/strategy/OUTREACH_MAILS_READY.md index a8f473f..5f3efd2 100644 --- a/ops/strategy/OUTREACH_MAILS_READY.md +++ b/ops/strategy/OUTREACH_MAILS_READY.md @@ -25,7 +25,7 @@ Ich schreibe Ihnen offen: Ich bin selbst betroffen. Rebreak ist nicht aus einer - Geräteweiter URL-/DNS-Filter mit ~330.000 bekannten Glücksspiel-Domains (iOS, Android, macOS). - Echtzeit-Mail-Schutz (IMAP-IDLE), der Casino-Werbemails löscht, bevor die Push-Benachrichtigung auslöst — meines Wissens das einzige Tool im deutschen Markt mit diesem Feature. - KI-Begleiter „Lyra" für die akuten 24/7-Momente zwischen den Beratungsterminen, **ausdrücklich nicht als Ersatz für Fachberatung**, sondern als Brücke zwischen den Sitzungen. -- Optionaler Selbstbindungs-Modus für macOS (RebReakBinder), den Betroffene gemeinsam mit einer Vertrauensperson aktivieren. +- Optionaler Selbstbindungs-Modus für macOS (Rebreak Magic), den Betroffene gemeinsam mit einer Vertrauensperson aktivieren. Die App ist in geschlossener Beta und wird derzeit von einer kleinen Gruppe Betroffener im Alltag getestet. Bevor ich breiter ausrolle, möchte ich Rebreak frühzeitig mit erfahrenen Fachstellen abstimmen — gerade in Niedersachsen, wo das Lukas-Werk als LSG-Träger eine besondere Rolle einnimmt. @@ -65,7 +65,7 @@ Ich schreibe Ihnen offen: Ich bin selbst betroffen. Rebreak ist aus dem konkrete - **„Trotz OASIS-Sperre spielen können"** → Rebreak blockiert geräteweit ~330.000 bekannte Glücksspiel-Domains, auch nicht-lizenzierte Offshore-Anbieter, die OASIS strukturell nicht erreicht. - **„Werbung trotz Sperre"** → Rebreak hat einen Echtzeit-Mail-Schutz (IMAP-IDLE), der Casino-Werbemails löscht, bevor die Push-Benachrichtigung am Endgerät anschlägt — meines Wissens das einzige deutschsprachige Tool mit dieser Funktion. -- **„Über das 1.000-€-Limit hinaus spielen können"** → adressiert Rebreak indirekt über Geräte-Schutz + den optionalen Selbstbindungs-Modus „RebReakBinder" auf macOS, der die App nur mit einer Vertrauensperson lösbar macht. +- **„Über das 1.000-€-Limit hinaus spielen können"** → adressiert Rebreak indirekt über Geräte-Schutz + den optionalen Selbstbindungs-Modus „Rebreak Magic" auf macOS, der die App nur mit einer Vertrauensperson lösbar macht. Die App ist aktuell in geschlossener Beta auf iOS, Android und macOS und wird von einer kleinen Gruppe Betroffener im Alltag getestet. Lyra, der integrierte KI-Begleiter, **versteht sich ausdrücklich nicht als Ersatz für Fachberatung**, sondern als 24/7-Brücke zwischen den Beratungsterminen — und verweist in akuten Lagen aktiv an die etablierten Strukturen (BZgA-Hotline, Telefonseelsorge, lokale Fachstellen).