chore(release): v0.3.13 build 46 / vc36 — DM scroll fix + chat timestamps weekday/days/weeks/months

This commit is contained in:
chahinebrini 2026-05-31 07:33:06 +02:00
parent 2715d2620b
commit 578abfe3bb
34 changed files with 1451 additions and 354 deletions

View File

@ -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 <udid>
# 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`)

View File

@ -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
}
}
}
}
}

View File

@ -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)
}
}
}
}

View File

@ -5,7 +5,7 @@
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>ReBreak Binder</string>
<string>Rebreak Magic</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>

View File

@ -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 {

View File

@ -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()
}

View File

@ -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<Void, Never>?
@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
}
}
}
}
}

View File

@ -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-<version>.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 ""

View File

@ -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)

View File

@ -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
#
# ──────────────────────────────────────────────────────────────────────────

View File

@ -39,6 +39,7 @@ yarn-error.*
# local env files
.env*.local
.deploy-secrets.local
# typescript
*.tsbuildinfo

View File

@ -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

View File

@ -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).

View File

@ -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)

View File

@ -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

View File

@ -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 }) {

View File

@ -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 <BrandSplash />;
// 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 (

View File

@ -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 }}
>
<View
onLayout={(e) => {
const h = e.nativeEvent.layout.height;
if (Math.abs(h - inputBarHeight) > 1) setInputBarHeight(h);
}}
style={[
styles.inputBar,
{

View File

@ -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)}
</Text>
{msg.isOwn && !hideReadStatus && (
{isDM && msg.isOwn && msg.status !== 'pending' && msg.status !== 'failed' && (
<Ionicons
name={msg.readAt ? 'checkmark-done' : 'checkmark'}
size={12}
@ -335,6 +339,22 @@ export function ChatBubble({
style={{ marginLeft: 2 }}
/>
)}
{msg.status === 'pending' && (
<Ionicons
name="time-outline"
size={11}
color="rgba(0,0,0,0.35)"
style={{ marginLeft: 2 }}
/>
)}
{msg.status === 'failed' && (
<Ionicons
name="alert-circle"
size={11}
color="#ef4444"
style={{ marginLeft: 2 }}
/>
)}
</View>
)}
</TouchableOpacity>

View File

@ -127,7 +127,7 @@ export function HeaderDropdownMenu({ visible, onClose, topOffset = 80 }: Props)
<BlurView
intensity={85}
tint={scheme === 'dark' ? 'systemThickMaterialDark' : 'systemThickMaterialLight'}
style={StyleSheet.absoluteFill}
style={styles.blurFill}
/>
) : null}
@ -263,3 +263,11 @@ export function HeaderDropdownMenu({ visible, onClose, topOffset = 80 }: Props)
</Modal>
);
}
const styles = StyleSheet.create({
blurFill: {
...StyleSheet.absoluteFillObject,
borderRadius: 18,
overflow: 'hidden',
},
});

View File

@ -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 (
<OnboardingShell

View File

@ -36,8 +36,8 @@
# ./deploy.sh all --dry-run
#
# CREDENTIALS:
# Persistenz (empfohlen): siehe .env.deploy.local.example
# cp .env.deploy.local.example .env.deploy.local # gitignored
# Persistenz (empfohlen): siehe .deploy-secrets.local.example
# cp .deploy-secrets.local.example .deploy-secrets.local # gitignored
# # einmalig editieren — deploy.sh source'd das automatisch
#
# iOS TestFlight / Ad-Hoc:
@ -346,12 +346,14 @@ while [[ $# -gt 0 ]]; do
done
# ═══════════════════════════════════════════════════════════════════════════
# Secrets-File auto-loading (NICHT committen — siehe .env.deploy.local.example)
# Secrets-File auto-loading (NICHT committen — siehe .deploy-secrets.local.example)
# ═══════════════════════════════════════════════════════════════════════════
# Lädt automatisch:
# apps/rebreak-native/.env.deploy.local (lokal, gitignored)
# apps/rebreak-native/.deploy-secrets.local (lokal, gitignored)
# ~/.config/rebreak/deploy.env (global fallback, optional)
for secrets_file in "$SCRIPT_DIR/.env.deploy.local" "$HOME/.config/rebreak/deploy.env"; do
# Hinweis: Datei heißt bewusst NICHT .env.* — sonst greift Metro's File-Watcher
# zu und versucht die Shell-Exports als JS zu parsen (SyntaxError im Bundle).
for secrets_file in "$SCRIPT_DIR/.deploy-secrets.local" "$HOME/.config/rebreak/deploy.env"; do
if [[ -f "$secrets_file" ]]; then
# shellcheck disable=SC1090
set -a; source "$secrets_file"; set +a
@ -402,7 +404,7 @@ require_asc_api_key() {
[[ -n "$ASC_API_KEY_PATH" ]] || missing+=("ASC_API_KEY_PATH")
if (( ${#missing[@]} > 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

View File

@ -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,13 +112,22 @@ 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"
# ─────────────────────────────────────────────────────────────
# --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:"
@ -118,11 +135,17 @@ cmd_ios() {
echo ""
echo "Falls dev-client Metro nicht automatisch findet:"
echo " im iPhone-Launcher → 'Enter URL manually' → http://<LAN-IP>: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..."
echo " Für schnellen UI-Reload ohne Rebuild: './dev.sh android --no-build'"
(cd "$ANDROID_DIR" && ./gradlew assembleDebug --console=plain)
fi
local APK="$ANDROID_DIR/app/build/outputs/apk/debug/app-debug.apk"
[[ -f "$APK" ]] || die "APK nicht gefunden: $APK"

View File

@ -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',
});

View File

@ -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<String>()
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<String>, 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",

View File

@ -19,7 +19,7 @@
<key>CFBundleShortVersionString</key>
<string>0.3.13</string>
<key>CFBundleVersion</key>
<string>41</string>
<string>45</string>
<key>NSExtension</key>
<dict>
<key>NSExtensionPointIdentifier</key>

View File

@ -19,7 +19,7 @@
<key>CFBundleShortVersionString</key>
<string>0.3.13</string>
<key>CFBundleVersion</key>
<string>41</string>
<string>45</string>
<key>NSExtension</key>
<dict>
<key>NSExtensionPointIdentifier</key>

View File

@ -19,7 +19,7 @@
<key>CFBundleShortVersionString</key>
<string>0.3.13</string>
<key>CFBundleVersion</key>
<string>41</string>
<string>45</string>
<key>EXAppExtensionAttributes</key>
<dict>
<key>EXExtensionPointIdentifier</key>

View File

@ -56,7 +56,7 @@ type AppLockState = {
};
export const useAppLockStore = create<AppLockState>((set, get) => ({
enabled: false,
enabled: true,
locked: false,
available: false,
ready: false,

View File

@ -23,16 +23,19 @@ async function persist(patch: Partial<Pick<NotificationPrefsState, 'pushEnabled'
}
export const useNotificationPrefsStore = create<NotificationPrefsState>((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 },
});

View File

@ -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

View File

@ -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 | ~59 € |
@ -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); 1218 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 | NiedrigMittel | 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 | NiedrigMittel | 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 | NiedrigMittel | 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 | NiedrigMittel | 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 | NiedrigMittel | 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

View File

@ -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
<key>PayloadRemovalDisallowed</key>
<true/>
```
ODER (älter, deprecated):
```xml
<key>RemovalPassword</key>
<string>hashed_password_here</string>
```
**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
<key>ExtensionSettings</key>
<dict>
<key>EXTENSION_ID</key>
<dict>
<key>installation_mode</key>
<string>force_installed</string> <!-- managed, nicht entfernbar -->
<key>update_url</key>
<string>https://clients2.google.com/service/update2/crx</string>
</dict>
</dict>
```
**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
<key>RemovalPassword</key>
<string>HASHED_PASSWORD</string>
```
→ 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
<key>PayloadType</key>
<string>com.apple.system-extension-policy</string>
<key>AllowUserOverrides</key>
<false/>
<key>AllowedSystemExtensions</key>
<dict>
<key>YOUR_TEAM_ID</key>
<array>
<string>org.rebreak.nefilter.extension</string>
</array>
</dict>
```
**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.

View File

@ -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).