chore(release): v0.3.13 build 46 / vc36 — DM scroll fix + chat timestamps weekday/days/weeks/months
This commit is contained in:
parent
2715d2620b
commit
578abfe3bb
@ -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)
|
1. **Welcome** — Detect iPhone via USB (lockdownd)
|
||||||
2. **Pre-Flight** — Find-My-iPhone + Stolen-Device-Protection prüfen/ausschalten
|
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
|
4. **Enroll** — MDM-Enrollment-Profile auf iPhone installieren
|
||||||
5. **Configure** — NanoMDM pusht: Lock-Profile + Take-Management + Settings(mdmSupervised=true)
|
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
|
## Status
|
||||||
|
|
||||||
@ -28,8 +61,7 @@ Resultat: iPhone supervised by "ReBreak", App nicht löschbar, NEFilter aktiv (k
|
|||||||
|
|
||||||
## Build
|
## Build
|
||||||
|
|
||||||
```bash
|
### Development-magic-mac
|
||||||
cd apps/rebreak-binder-mac
|
|
||||||
|
|
||||||
# Einmalig: dependencies + supervise-magic-binary bauen
|
# Einmalig: dependencies + supervise-magic-binary bauen
|
||||||
(cd ../../ops/mdm/supervise-magic && make tidy && make build)
|
(cd ../../ops/mdm/supervise-magic && make tidy && make build)
|
||||||
@ -38,15 +70,58 @@ cd apps/rebreak-binder-mac
|
|||||||
xcodegen generate
|
xcodegen generate
|
||||||
|
|
||||||
# Bauen + öffnen
|
# Bauen + öffnen
|
||||||
open RebreakBinder.xcodeproj
|
open RebreakMagic.xcodeproj
|
||||||
# → ⌘R in Xcode
|
# → ⌘R in Xcode
|
||||||
```
|
```
|
||||||
|
|
||||||
Oder CLI-only:
|
Oder CLI-only:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
xcodebuild -project RebreakBinder.xcodeproj -scheme RebreakBinder -configuration Debug build
|
xcodebuild -project RebreakMagic.xcodeproj -scheme RebreakMagic -configuration Debug build
|
||||||
open build/Debug/RebreakBinder.app
|
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)
|
## Config (lokal)
|
||||||
@ -66,6 +141,47 @@ chmod 600 ~/.config/rebreak-binder/config.json
|
|||||||
|
|
||||||
Production-Version legt das in Keychain ab — heute reicht plain 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)
|
## 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).
|
- [ ] **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
|
## Sicherheit
|
||||||
|
|
||||||
- API-Key sollte langfristig in Keychain (heute: plain JSON, chmod 600)
|
- 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`)
|
- Process-Spawn von go-binaries braucht **disabled App-Sandbox** (gesetzt in `project.yml`)
|
||||||
|
|||||||
@ -1,6 +1,22 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import Observation
|
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
|
@MainActor
|
||||||
@Observable
|
@Observable
|
||||||
final class WizardModel {
|
final class WizardModel {
|
||||||
@ -23,6 +39,15 @@ final class WizardModel {
|
|||||||
|
|
||||||
var cooldownEndsAt: Date?
|
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() {
|
func advance() {
|
||||||
if let next = WizardStep(rawValue: step.rawValue + 1) {
|
if let next = WizardStep(rawValue: step.rawValue + 1) {
|
||||||
step = next
|
step = next
|
||||||
@ -44,5 +69,85 @@ final class WizardModel {
|
|||||||
configureError = nil
|
configureError = nil
|
||||||
showAdvancedLogs = false
|
showAdvancedLogs = false
|
||||||
cooldownEndsAt = nil
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,16 +1,47 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
@main
|
@main
|
||||||
struct RebreakBinderApp: App {
|
struct RebreakMagicApp: App {
|
||||||
@State private var model = WizardModel()
|
@State private var model = WizardModel()
|
||||||
|
|
||||||
var body: some Scene {
|
var body: some Scene {
|
||||||
WindowGroup("ReBreak Binder") {
|
WindowGroup("Rebreak Magic") {
|
||||||
ContentView()
|
ContentView()
|
||||||
.environment(model)
|
.environment(model)
|
||||||
.frame(minWidth: 720, idealWidth: 800, minHeight: 600, idealHeight: 720)
|
.frame(minWidth: 720, idealWidth: 800, minHeight: 600, idealHeight: 720)
|
||||||
}
|
}
|
||||||
.windowResizability(.contentSize)
|
.windowResizability(.contentSize)
|
||||||
.windowStyle(.titleBar)
|
.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)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,7 +5,7 @@
|
|||||||
<key>CFBundleDevelopmentRegion</key>
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||||
<key>CFBundleDisplayName</key>
|
<key>CFBundleDisplayName</key>
|
||||||
<string>ReBreak Binder</string>
|
<string>Rebreak Magic</string>
|
||||||
<key>CFBundleExecutable</key>
|
<key>CFBundleExecutable</key>
|
||||||
<string>$(EXECUTABLE_NAME)</string>
|
<string>$(EXECUTABLE_NAME)</string>
|
||||||
<key>CFBundleIdentifier</key>
|
<key>CFBundleIdentifier</key>
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import SwiftUI
|
|||||||
|
|
||||||
struct ContentView: View {
|
struct ContentView: View {
|
||||||
@Environment(WizardModel.self) private var model
|
@Environment(WizardModel.self) private var model
|
||||||
|
@State private var showingHelp = false
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
@ -11,17 +12,29 @@ struct ContentView: View {
|
|||||||
appBadge
|
appBadge
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 1) {
|
VStack(alignment: .leading, spacing: 1) {
|
||||||
Text("ReBreak Binder")
|
Text("Rebreak Magic")
|
||||||
.font(.headline)
|
.font(.headline)
|
||||||
Text("macOS supervision tool")
|
Text("iPhone bind ohne Werks-Reset")
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
}
|
}
|
||||||
Spacer()
|
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 {
|
if model.step != .done {
|
||||||
Text("Schritt \(model.step.stepNumber) von \(WizardStep.total)")
|
Text("Schritt \(model.step.stepNumber) von \(WizardStep.total)")
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
|
.padding(.leading, 12)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(.horizontal, 20)
|
.padding(.horizontal, 20)
|
||||||
@ -33,6 +46,9 @@ struct ContentView: View {
|
|||||||
|
|
||||||
Divider()
|
Divider()
|
||||||
|
|
||||||
|
.sheet(isPresented: $showingHelp) {
|
||||||
|
HelpView()
|
||||||
|
}
|
||||||
// Main content
|
// Main content
|
||||||
Group {
|
Group {
|
||||||
switch model.step {
|
switch model.step {
|
||||||
|
|||||||
92
apps/rebreak-magic-mac/Sources/Views/HelpView.swift
Normal file
92
apps/rebreak-magic-mac/Sources/Views/HelpView.swift
Normal 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()
|
||||||
|
}
|
||||||
@ -1,34 +1,11 @@
|
|||||||
import SwiftUI
|
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 {
|
struct WelcomeView: View {
|
||||||
@Environment(WizardModel.self) private var model
|
@Environment(WizardModel.self) private var model
|
||||||
|
|
||||||
@State private var detecting = false
|
@State private var detecting = false
|
||||||
@State private var error: String?
|
@State private var error: String?
|
||||||
@State private var pollTask: Task<Void, Never>?
|
@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 {
|
var body: some View {
|
||||||
VStack(spacing: 24) {
|
VStack(spacing: 24) {
|
||||||
@ -70,71 +47,12 @@ struct WelcomeView: View {
|
|||||||
.buttonStyle(.borderedProminent)
|
.buttonStyle(.borderedProminent)
|
||||||
.disabled(model.device == nil)
|
.disabled(model.device == nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
resetSection
|
|
||||||
}
|
}
|
||||||
.padding(40)
|
.padding(40)
|
||||||
.onAppear { startDetection() }
|
.onAppear { startDetection() }
|
||||||
.onDisappear { pollTask?.cancel() }
|
.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 {
|
private var nextButtonLabel: String {
|
||||||
if model.device?.isFullyBound == true {
|
if model.device?.isFullyBound == true {
|
||||||
return "Weiter → Schutz aktivieren"
|
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
226
apps/rebreak-magic-mac/build-dmg.sh
Executable file
226
apps/rebreak-magic-mac/build-dmg.sh
Executable 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 ""
|
||||||
@ -1,6 +1,6 @@
|
|||||||
name: RebreakBinder
|
name: RebreakMagic
|
||||||
options:
|
options:
|
||||||
bundleIdPrefix: org.rebreak.binder
|
bundleIdPrefix: org.rebreak.magic
|
||||||
deploymentTarget:
|
deploymentTarget:
|
||||||
macOS: "14.0"
|
macOS: "14.0"
|
||||||
createIntermediateGroups: true
|
createIntermediateGroups: true
|
||||||
@ -10,7 +10,7 @@ settings:
|
|||||||
base:
|
base:
|
||||||
SWIFT_VERSION: "5.10"
|
SWIFT_VERSION: "5.10"
|
||||||
MACOSX_DEPLOYMENT_TARGET: "14.0"
|
MACOSX_DEPLOYMENT_TARGET: "14.0"
|
||||||
PRODUCT_BUNDLE_IDENTIFIER: org.rebreak.binder.mac
|
PRODUCT_BUNDLE_IDENTIFIER: org.rebreak.magic.mac
|
||||||
MARKETING_VERSION: "0.1.0"
|
MARKETING_VERSION: "0.1.0"
|
||||||
CURRENT_PROJECT_VERSION: "1"
|
CURRENT_PROJECT_VERSION: "1"
|
||||||
DEVELOPMENT_TEAM: ""
|
DEVELOPMENT_TEAM: ""
|
||||||
@ -20,7 +20,7 @@ settings:
|
|||||||
ENABLE_APP_SANDBOX: NO
|
ENABLE_APP_SANDBOX: NO
|
||||||
|
|
||||||
targets:
|
targets:
|
||||||
RebreakBinder:
|
RebreakMagic:
|
||||||
type: application
|
type: application
|
||||||
platform: macOS
|
platform: macOS
|
||||||
sources:
|
sources:
|
||||||
@ -33,7 +33,7 @@ targets:
|
|||||||
info:
|
info:
|
||||||
path: Sources/Resources/Info.plist
|
path: Sources/Resources/Info.plist
|
||||||
properties:
|
properties:
|
||||||
CFBundleDisplayName: ReBreak Binder
|
CFBundleDisplayName: Rebreak Magic
|
||||||
CFBundleShortVersionString: $(MARKETING_VERSION)
|
CFBundleShortVersionString: $(MARKETING_VERSION)
|
||||||
CFBundleVersion: $(CURRENT_PROJECT_VERSION)
|
CFBundleVersion: $(CURRENT_PROJECT_VERSION)
|
||||||
LSMinimumSystemVersion: $(MACOSX_DEPLOYMENT_TARGET)
|
LSMinimumSystemVersion: $(MACOSX_DEPLOYMENT_TARGET)
|
||||||
|
|||||||
@ -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):
|
# 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
|
# 2. ~/.config/rebreak/deploy.env
|
||||||
#
|
#
|
||||||
# ──────────────────────────────────────────────────────────────────────────
|
# ──────────────────────────────────────────────────────────────────────────
|
||||||
|
|||||||
1
apps/rebreak-native/.gitignore
vendored
1
apps/rebreak-native/.gitignore
vendored
@ -39,6 +39,7 @@ yarn-error.*
|
|||||||
|
|
||||||
# local env files
|
# local env files
|
||||||
.env*.local
|
.env*.local
|
||||||
|
.deploy-secrets.local
|
||||||
|
|
||||||
# typescript
|
# typescript
|
||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
|
|||||||
@ -43,10 +43,17 @@ pnpm exec expo run:ios
|
|||||||
pnpm exec expo run:ios --device "iPhone 15"
|
pnpm exec expo run:ios --device "iPhone 15"
|
||||||
```
|
```
|
||||||
|
|
||||||
Alternativ (wenn dev-iphone.sh vorhanden):
|
Alternativ via konsolidiertem Dev-Script:
|
||||||
|
|
||||||
```bash
|
```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
|
### Android Emulator
|
||||||
|
|||||||
@ -1,6 +1,9 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
All notable changes to rebreak-native will be documented in this file.
|
All notable changes to rebreak-native will be documented in this file.
|
||||||
|
## v0.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.
|
## 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).
|
Lyra-Sprachnachrichten: Wenn du auf Arabisch oder Türkisch sprichst, antwortet Lyra jetzt auch in der richtigen Sprache (Backend-Fix).
|
||||||
|
|||||||
@ -4,16 +4,23 @@
|
|||||||
|
|
||||||
### Development
|
### Development
|
||||||
```bash
|
```bash
|
||||||
# iOS Dev (Metro + Xcode):
|
# Default = iPhone USB + Native-Build:
|
||||||
./dev.sh ios
|
./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
|
./dev.sh ios --device
|
||||||
|
|
||||||
# iOS Dev auf iPhone via WiFi:
|
# iOS Dev auf iPhone via WiFi (Kabel ab):
|
||||||
./dev.sh ios --wifi
|
./dev.sh ios --wifi
|
||||||
|
|
||||||
# Android Dev:
|
# iOS Simulator:
|
||||||
|
./dev.sh ios --simulator
|
||||||
|
|
||||||
|
# Android Dev (Build + Install + Launch):
|
||||||
./dev.sh android
|
./dev.sh android
|
||||||
|
|
||||||
# Nur Metro starten:
|
# Nur Metro starten:
|
||||||
@ -89,14 +96,16 @@
|
|||||||
- `install android` — Debug-APK auf Android Device installieren
|
- `install android` — Debug-APK auf Android Device installieren
|
||||||
|
|
||||||
### Flags (ios)
|
### Flags (ios)
|
||||||
- `--device` — Build auf physisches iPhone via USB
|
- `--device` — Build auf physisches iPhone via USB **(default)**
|
||||||
- `--simulator` — Build auf iOS Simulator (default)
|
- `--simulator` — Build auf iOS Simulator
|
||||||
- `--xcode` — Nur Xcode öffnen (manueller Build)
|
- `--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)
|
### Flags (android)
|
||||||
- `--no-build` — Skip Gradle build, nur install last APK
|
- `--no-build` — **KEIN Gradle-Rebuild** → nur Metro starten (APK muss schon installiert sein, schnellster UI/JS-Loop)
|
||||||
- `--no-launch` — Install but don't auto-launch
|
- `--no-launch` — Build+Install, aber kein Auto-Launch
|
||||||
|
- `--wifi` — Metro mit --host lan (nur in Kombi mit `--no-build`)
|
||||||
|
|
||||||
### Flags (metro)
|
### Flags (metro)
|
||||||
- `--keep` — Cache behalten (kein --clear)
|
- `--keep` — Cache behalten (kein --clear)
|
||||||
|
|||||||
@ -36,7 +36,7 @@ export default ({ config }: ConfigContext): ExpoConfig => ({
|
|||||||
ios: {
|
ios: {
|
||||||
supportsTablet: true,
|
supportsTablet: true,
|
||||||
bundleIdentifier: MAIN_BUNDLE,
|
bundleIdentifier: MAIN_BUNDLE,
|
||||||
buildNumber: "45",
|
buildNumber: "46",
|
||||||
// Apple Sign-In Entitlement — Pflicht für expo-apple-authentication nativen
|
// Apple Sign-In Entitlement — Pflicht für expo-apple-authentication nativen
|
||||||
// signInAsync()-Flow. Ohne flag generiert Expo's prebuild den
|
// signInAsync()-Flow. Ohne flag generiert Expo's prebuild den
|
||||||
// com.apple.developer.applesignin-Entitlement nicht in die .entitlements.
|
// com.apple.developer.applesignin-Entitlement nicht in die .entitlements.
|
||||||
@ -59,7 +59,7 @@ export default ({ config }: ConfigContext): ExpoConfig => ({
|
|||||||
|
|
||||||
android: {
|
android: {
|
||||||
package: "org.rebreak.app",
|
package: "org.rebreak.app",
|
||||||
versionCode: 35,
|
versionCode: 36,
|
||||||
adaptiveIcon: {
|
adaptiveIcon: {
|
||||||
// Foreground muss in der ~66%-Safe-Zone bleiben (Launcher-Mask clippt den
|
// Foreground muss in der ~66%-Safe-Zone bleiben (Launcher-Mask clippt den
|
||||||
// Außenring) → adaptive-foreground.png ist das Logo auf transparentem
|
// Außenring) → adaptive-foreground.png ist das Logo auf transparentem
|
||||||
|
|||||||
@ -30,10 +30,15 @@ type DmConversation = {
|
|||||||
|
|
||||||
function formatTime(ts: string, justNowLabel: string): string {
|
function formatTime(ts: string, justNowLabel: string): string {
|
||||||
const diff = Date.now() - new Date(ts).getTime();
|
const diff = Date.now() - new Date(ts).getTime();
|
||||||
|
const day = 86_400_000;
|
||||||
if (diff < 60_000) return justNowLabel;
|
if (diff < 60_000) return justNowLabel;
|
||||||
if (diff < 3_600_000) return `${Math.floor(diff / 60_000)}m`;
|
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`;
|
if (diff < day) return `${Math.floor(diff / 3_600_000)}h`;
|
||||||
return new Date(ts).toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit' });
|
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 }) {
|
function DmItem({ conv, onPress }: { conv: DmConversation; onPress: () => void }) {
|
||||||
|
|||||||
@ -27,7 +27,6 @@ import { useColors } from '../lib/theme';
|
|||||||
import { useLanguageStore } from '../stores/language';
|
import { useLanguageStore } from '../stores/language';
|
||||||
import { useAppLockStore } from '../stores/appLock';
|
import { useAppLockStore } from '../stores/appLock';
|
||||||
import { useLyraVoiceStore } from '../stores/lyraVoice';
|
import { useLyraVoiceStore } from '../stores/lyraVoice';
|
||||||
import { BrandSplash } from '../components/BrandSplash';
|
|
||||||
import { AppLockGate } from '../components/AppLockGate';
|
import { AppLockGate } from '../components/AppLockGate';
|
||||||
import { DeviceLimitReachedSheet } from '../components/DeviceLimitReachedSheet';
|
import { DeviceLimitReachedSheet } from '../components/DeviceLimitReachedSheet';
|
||||||
import { OnlinePresenceProvider } from '../components/OnlinePresenceProvider';
|
import { OnlinePresenceProvider } from '../components/OnlinePresenceProvider';
|
||||||
@ -124,7 +123,10 @@ function RootLayoutInner() {
|
|||||||
}, [fontsLoaded, loading, appLockReady]);
|
}, [fontsLoaded, loading, appLockReady]);
|
||||||
|
|
||||||
if (!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 (
|
return (
|
||||||
|
|||||||
@ -87,6 +87,7 @@ export default function DmScreen() {
|
|||||||
const [uploading, setUploading] = useState(false);
|
const [uploading, setUploading] = useState(false);
|
||||||
const [keyboardVisible, setKeyboardVisible] = useState(false);
|
const [keyboardVisible, setKeyboardVisible] = useState(false);
|
||||||
const [keyboardHeight, setKeyboardHeight] = useState(0);
|
const [keyboardHeight, setKeyboardHeight] = useState(0);
|
||||||
|
const [inputBarHeight, setInputBarHeight] = useState(60);
|
||||||
|
|
||||||
// Reset aller conversation-spezifischen States wenn userId wechselt (Stack-Reuse)
|
// Reset aller conversation-spezifischen States wenn userId wechselt (Stack-Reuse)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -516,7 +517,7 @@ export default function DmScreen() {
|
|||||||
contentContainerStyle={{
|
contentContainerStyle={{
|
||||||
paddingHorizontal: 0,
|
paddingHorizontal: 0,
|
||||||
paddingTop: 12,
|
paddingTop: 12,
|
||||||
paddingBottom: 12 + insets.bottom + (keyboardVisible ? keyboardHeight : 0),
|
paddingBottom: inputBarHeight + 12,
|
||||||
}}
|
}}
|
||||||
showsVerticalScrollIndicator={false}
|
showsVerticalScrollIndicator={false}
|
||||||
keyboardDismissMode="interactive"
|
keyboardDismissMode="interactive"
|
||||||
@ -531,6 +532,10 @@ export default function DmScreen() {
|
|||||||
style={{ backgroundColor: colors.bg }}
|
style={{ backgroundColor: colors.bg }}
|
||||||
>
|
>
|
||||||
<View
|
<View
|
||||||
|
onLayout={(e) => {
|
||||||
|
const h = e.nativeEvent.layout.height;
|
||||||
|
if (Math.abs(h - inputBarHeight) > 1) setInputBarHeight(h);
|
||||||
|
}}
|
||||||
style={[
|
style={[
|
||||||
styles.inputBar,
|
styles.inputBar,
|
||||||
{
|
{
|
||||||
|
|||||||
@ -41,6 +41,8 @@ export type ChatMsg = {
|
|||||||
reactions?: MessageReaction[];
|
reactions?: MessageReaction[];
|
||||||
/** Soft-Delete-Tombstone. */
|
/** Soft-Delete-Tombstone. */
|
||||||
deleted?: boolean;
|
deleted?: boolean;
|
||||||
|
/** Optimistic-UI Status (pending = wird gesendet, failed = Fehler). */
|
||||||
|
status?: 'pending' | 'sent' | 'failed';
|
||||||
};
|
};
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@ -192,6 +194,8 @@ export function ChatBubble({
|
|||||||
{ backgroundColor: bubbleBg },
|
{ backgroundColor: bubbleBg },
|
||||||
!msg.isOwn && styles.bubbleOtherBorder,
|
!msg.isOwn && styles.bubbleOtherBorder,
|
||||||
isImageOnly && { padding: 4 },
|
isImageOnly && { padding: 4 },
|
||||||
|
msg.status === 'pending' && { opacity: 0.6 },
|
||||||
|
msg.status === 'failed' && { borderWidth: 1, borderColor: '#ef4444' },
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
{msg.replyTo && (
|
{msg.replyTo && (
|
||||||
@ -327,7 +331,7 @@ export function ChatBubble({
|
|||||||
>
|
>
|
||||||
{formatTime(msg.createdAt)}
|
{formatTime(msg.createdAt)}
|
||||||
</Text>
|
</Text>
|
||||||
{msg.isOwn && !hideReadStatus && (
|
{isDM && msg.isOwn && msg.status !== 'pending' && msg.status !== 'failed' && (
|
||||||
<Ionicons
|
<Ionicons
|
||||||
name={msg.readAt ? 'checkmark-done' : 'checkmark'}
|
name={msg.readAt ? 'checkmark-done' : 'checkmark'}
|
||||||
size={12}
|
size={12}
|
||||||
@ -335,6 +339,22 @@ export function ChatBubble({
|
|||||||
style={{ marginLeft: 2 }}
|
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>
|
</View>
|
||||||
)}
|
)}
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|||||||
@ -127,7 +127,7 @@ export function HeaderDropdownMenu({ visible, onClose, topOffset = 80 }: Props)
|
|||||||
<BlurView
|
<BlurView
|
||||||
intensity={85}
|
intensity={85}
|
||||||
tint={scheme === 'dark' ? 'systemThickMaterialDark' : 'systemThickMaterialLight'}
|
tint={scheme === 'dark' ? 'systemThickMaterialDark' : 'systemThickMaterialLight'}
|
||||||
style={StyleSheet.absoluteFill}
|
style={styles.blurFill}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
@ -263,3 +263,11 @@ export function HeaderDropdownMenu({ visible, onClose, topOffset = 80 }: Props)
|
|||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
blurFill: {
|
||||||
|
...StyleSheet.absoluteFillObject,
|
||||||
|
borderRadius: 18,
|
||||||
|
overflow: 'hidden',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|||||||
@ -2,11 +2,13 @@ import { useEffect, useRef } from 'react';
|
|||||||
import { Animated, Easing, Text, useWindowDimensions, View } from 'react-native';
|
import { Animated, Easing, Text, useWindowDimensions, View } from 'react-native';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Ionicons } from '@expo/vector-icons';
|
import { Ionicons } from '@expo/vector-icons';
|
||||||
|
import * as Notifications from 'expo-notifications';
|
||||||
import { useColors } from '../../../lib/theme';
|
import { useColors } from '../../../lib/theme';
|
||||||
import { OnboardingShell } from '../OnboardingShell';
|
import { OnboardingShell } from '../OnboardingShell';
|
||||||
import { LyraBubble } from '../LyraBubble';
|
import { LyraBubble } from '../LyraBubble';
|
||||||
import { CTABar } from '../CTABar';
|
import { CTABar } from '../CTABar';
|
||||||
import { FaqAccordion, type FaqItem } from '../../FaqAccordion';
|
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.
|
// 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;
|
const ONBOARDING_FAQ_IDS = [1, 2, 4, 5, 8] as const;
|
||||||
@ -24,6 +26,7 @@ export function DoneSlide({
|
|||||||
const colors = useColors();
|
const colors = useColors();
|
||||||
const scale = useRef(new Animated.Value(0.6)).current;
|
const scale = useRef(new Animated.Value(0.6)).current;
|
||||||
const opacity = useRef(new Animated.Value(0)).current;
|
const opacity = useRef(new Animated.Value(0)).current;
|
||||||
|
const setPushEnabled = useNotificationPrefsStore((s) => s.setPushEnabled);
|
||||||
|
|
||||||
const faqItems: FaqItem[] = ONBOARDING_FAQ_IDS.map((id) => ({
|
const faqItems: FaqItem[] = ONBOARDING_FAQ_IDS.map((id) => ({
|
||||||
q: t(`help.faq_q${id}`),
|
q: t(`help.faq_q${id}`),
|
||||||
@ -40,7 +43,16 @@ export function DoneSlide({
|
|||||||
easing: Easing.out(Easing.cubic),
|
easing: Easing.out(Easing.cubic),
|
||||||
}),
|
}),
|
||||||
]).start();
|
]).start();
|
||||||
}, []);
|
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
const { status } = await Notifications.requestPermissionsAsync();
|
||||||
|
if (status !== 'granted') {
|
||||||
|
await setPushEnabled(false);
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
})();
|
||||||
|
}, [setPushEnabled]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<OnboardingShell
|
<OnboardingShell
|
||||||
|
|||||||
@ -36,8 +36,8 @@
|
|||||||
# ./deploy.sh all --dry-run
|
# ./deploy.sh all --dry-run
|
||||||
#
|
#
|
||||||
# CREDENTIALS:
|
# CREDENTIALS:
|
||||||
# Persistenz (empfohlen): siehe .env.deploy.local.example
|
# Persistenz (empfohlen): siehe .deploy-secrets.local.example
|
||||||
# cp .env.deploy.local.example .env.deploy.local # gitignored
|
# cp .deploy-secrets.local.example .deploy-secrets.local # gitignored
|
||||||
# # einmalig editieren — deploy.sh source'd das automatisch
|
# # einmalig editieren — deploy.sh source'd das automatisch
|
||||||
#
|
#
|
||||||
# iOS TestFlight / Ad-Hoc:
|
# iOS TestFlight / Ad-Hoc:
|
||||||
@ -346,12 +346,14 @@ while [[ $# -gt 0 ]]; do
|
|||||||
done
|
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:
|
# 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)
|
# ~/.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
|
if [[ -f "$secrets_file" ]]; then
|
||||||
# shellcheck disable=SC1090
|
# shellcheck disable=SC1090
|
||||||
set -a; source "$secrets_file"; set +a
|
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")
|
[[ -n "$ASC_API_KEY_PATH" ]] || missing+=("ASC_API_KEY_PATH")
|
||||||
if (( ${#missing[@]} > 0 )); then
|
if (( ${#missing[@]} > 0 )); then
|
||||||
die "iOS Signing braucht ASC API-Key. Fehlt: ${missing[*]}
|
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
|
fi
|
||||||
if [[ ! -f "$ASC_API_KEY_PATH" ]]; then
|
if [[ ! -f "$ASC_API_KEY_PATH" ]]; then
|
||||||
die "ASC API-Key Datei existiert nicht: $ASC_API_KEY_PATH
|
die "ASC API-Key Datei existiert nicht: $ASC_API_KEY_PATH
|
||||||
|
|||||||
@ -1,24 +1,30 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# dev.sh — ReBreak Native Development Tooling
|
# dev.sh — ReBreak Native Development Tooling
|
||||||
#
|
#
|
||||||
|
# Konsolidiert: dev-ios.sh + dev-iphone.sh + metro.sh (alle gelöscht).
|
||||||
|
#
|
||||||
# SUBCOMMANDS:
|
# SUBCOMMANDS:
|
||||||
# ./dev.sh default: ios (Metro + Xcode)
|
# ./dev.sh default: ios --device (physisches iPhone USB + Build)
|
||||||
# ./dev.sh ios iOS Dev (Metro + Xcode Workspace / Simulator)
|
# ./dev.sh ios iOS Dev (Default: USB-Device mit Build)
|
||||||
# ./dev.sh android Android Dev (Metro + Gradle build + install)
|
# ./dev.sh android Android Dev (Gradle Build + Install + Launch)
|
||||||
# ./dev.sh metro Nur Metro starten
|
# ./dev.sh metro Nur Metro starten
|
||||||
# ./dev.sh clean iOS: Nuclear clean (Pods, DerivedData, Archives)
|
# ./dev.sh clean iOS: Nuclear clean (Pods, DerivedData, Archives)
|
||||||
# ./dev.sh install ios Build Release + Install auf iPhone USB
|
# ./dev.sh install ios Build Release + Install auf iPhone USB
|
||||||
# ./dev.sh install android Build Debug APK + Install auf Android Device
|
# ./dev.sh install android Build Debug APK + Install auf Android Device
|
||||||
#
|
#
|
||||||
# FLAGS (ios):
|
# FLAGS (ios):
|
||||||
# --device Build auf physisches iPhone via USB
|
# --device Build auf physisches iPhone via USB (DEFAULT)
|
||||||
# --simulator Build auf iOS Simulator (default)
|
# --simulator Build auf iOS Simulator
|
||||||
# --xcode Nur Xcode öffnen (manueller Build)
|
# --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):
|
# FLAGS (android):
|
||||||
# --no-build Skip Gradle build, nur install last APK
|
# --no-build KEIN Gradle-Rebuild → nur Metro starten
|
||||||
# --no-launch Install but don't auto-launch
|
# (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):
|
# FLAGS (metro):
|
||||||
# --keep Cache behalten (kein --clear)
|
# --keep Cache behalten (kein --clear)
|
||||||
@ -28,20 +34,21 @@
|
|||||||
# --xcode + Xcode öffnen am Ende
|
# --xcode + Xcode öffnen am Ende
|
||||||
#
|
#
|
||||||
# BEISPIELE:
|
# BEISPIELE:
|
||||||
# # iOS Dev auf Simulator:
|
# # Default: iPhone USB + Native-Build:
|
||||||
# ./dev.sh ios
|
# ./dev.sh
|
||||||
#
|
#
|
||||||
# # iOS Dev auf physischem iPhone via USB:
|
# # Schneller UI-Loop (App schon installiert, nur JS-Reload):
|
||||||
# ./dev.sh ios --device
|
# ./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
|
# ./dev.sh ios --wifi
|
||||||
#
|
#
|
||||||
# # Android Dev:
|
# # iOS Simulator:
|
||||||
# ./dev.sh android
|
# ./dev.sh ios --simulator
|
||||||
#
|
#
|
||||||
# # Nur Metro starten:
|
# # Nur Xcode öffnen:
|
||||||
# ./dev.sh metro
|
# ./dev.sh ios --xcode
|
||||||
#
|
#
|
||||||
# # iOS Clean + Rebuild:
|
# # iOS Clean + Rebuild:
|
||||||
# ./dev.sh clean --build
|
# ./dev.sh clean --build
|
||||||
@ -95,8 +102,9 @@ export REBREAK_DEV="${REBREAK_DEV:-0}"
|
|||||||
# ═══════════════════════════════════════════════════════════════════════════
|
# ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
cmd_ios() {
|
cmd_ios() {
|
||||||
local MODE="simulator"
|
local MODE="device" # Default: physisches iPhone via USB
|
||||||
local WIFI=false
|
local WIFI=false
|
||||||
|
local BUILD=true # Default: nativen Build laufen lassen
|
||||||
|
|
||||||
while [[ $# -gt 0 ]]; do
|
while [[ $# -gt 0 ]]; do
|
||||||
case "$1" in
|
case "$1" in
|
||||||
@ -104,25 +112,40 @@ cmd_ios() {
|
|||||||
--simulator) MODE="simulator"; shift ;;
|
--simulator) MODE="simulator"; shift ;;
|
||||||
--xcode) MODE="xcode"; shift ;;
|
--xcode) MODE="xcode"; shift ;;
|
||||||
--wifi) WIFI=true; shift ;;
|
--wifi) WIFI=true; shift ;;
|
||||||
|
--no-build) BUILD=false; shift ;;
|
||||||
*) die "Unbekannter Flag für 'ios': $1" ;;
|
*) die "Unbekannter Flag für 'ios': $1" ;;
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
|
|
||||||
section "iOS Dev Mode"
|
section "iOS Dev Mode"
|
||||||
|
|
||||||
if $WIFI; then
|
# ─────────────────────────────────────────────────────────────
|
||||||
log "Metro: WiFi-Modus (--host lan)"
|
# --no-build / WiFi: KEIN Native-Rebuild, nur Metro + Dev-Client
|
||||||
echo ""
|
# → schnellster Loop für reine UI/JS-Änderungen
|
||||||
echo "Mac LAN-IP:"
|
# → App muss schon auf dem Device/Simulator installiert sein
|
||||||
ipconfig getifaddr en0 2>/dev/null || ipconfig getifaddr en1 2>/dev/null || echo " (kein WiFi/Ethernet detected)"
|
# ─────────────────────────────────────────────────────────────
|
||||||
echo ""
|
if ! $BUILD || $WIFI; then
|
||||||
echo "Falls dev-client Metro nicht automatisch findet:"
|
local HOST_FLAG=""
|
||||||
echo " im iPhone-Launcher → 'Enter URL manually' → http://<LAN-IP>:8081"
|
if $WIFI; then
|
||||||
|
HOST_FLAG="--host lan"
|
||||||
|
log "Metro: WiFi-Modus (--host lan)"
|
||||||
|
echo ""
|
||||||
|
echo "Mac LAN-IP:"
|
||||||
|
ipconfig getifaddr en0 2>/dev/null || ipconfig getifaddr en1 2>/dev/null || echo " (kein WiFi/Ethernet detected)"
|
||||||
|
echo ""
|
||||||
|
echo "Falls dev-client Metro nicht automatisch findet:"
|
||||||
|
echo " im iPhone-Launcher → 'Enter URL manually' → http://<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 ""
|
echo ""
|
||||||
log "Killing old Metro on port 8081..."
|
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 ""
|
echo ""
|
||||||
exec pnpm expo start --host lan --clear --dev-client
|
exec pnpm expo start $HOST_FLAG --clear --dev-client
|
||||||
fi
|
fi
|
||||||
|
|
||||||
case "$MODE" in
|
case "$MODE" in
|
||||||
@ -137,6 +160,7 @@ cmd_ios() {
|
|||||||
|
|
||||||
device)
|
device)
|
||||||
log "Building für physisches iPhone (USB)..."
|
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
|
pnpm expo run:ios --device
|
||||||
;;
|
;;
|
||||||
|
|
||||||
@ -150,17 +174,44 @@ cmd_ios() {
|
|||||||
cmd_android() {
|
cmd_android() {
|
||||||
local BUILD=true
|
local BUILD=true
|
||||||
local LAUNCH=true
|
local LAUNCH=true
|
||||||
|
local WIFI=false
|
||||||
|
|
||||||
while [[ $# -gt 0 ]]; do
|
while [[ $# -gt 0 ]]; do
|
||||||
case "$1" in
|
case "$1" in
|
||||||
--no-build) BUILD=false; shift ;;
|
--no-build) BUILD=false; shift ;;
|
||||||
--no-launch) LAUNCH=false; shift ;;
|
--no-launch) LAUNCH=false; shift ;;
|
||||||
|
--wifi) WIFI=true; shift ;;
|
||||||
*) die "Unbekannter Flag für 'android': $1" ;;
|
*) die "Unbekannter Flag für 'android': $1" ;;
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
|
|
||||||
section "Android Dev Mode"
|
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"
|
command -v adb >/dev/null 2>&1 || die "adb nicht gefunden — brew install --cask android-platform-tools"
|
||||||
|
|
||||||
local DEVICE_COUNT
|
local DEVICE_COUNT
|
||||||
@ -178,10 +229,9 @@ cmd_android() {
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if $BUILD; then
|
log "Building Debug APK..."
|
||||||
log "Building Debug APK..."
|
echo "ℹ️ Für schnellen UI-Reload ohne Rebuild: './dev.sh android --no-build'"
|
||||||
(cd "$ANDROID_DIR" && ./gradlew assembleDebug --console=plain)
|
(cd "$ANDROID_DIR" && ./gradlew assembleDebug --console=plain)
|
||||||
fi
|
|
||||||
|
|
||||||
local APK="$ANDROID_DIR/app/build/outputs/apk/debug/app-debug.apk"
|
local APK="$ANDROID_DIR/app/build/outputs/apk/debug/app-debug.apk"
|
||||||
[[ -f "$APK" ]] || die "APK nicht gefunden: $APK"
|
[[ -f "$APK" ]] || die "APK nicht gefunden: $APK"
|
||||||
|
|||||||
@ -26,6 +26,19 @@ config.resolver.unstable_enablePackageExports = true;
|
|||||||
// 4. .riv (Rive-Animation) als Asset registrieren
|
// 4. .riv (Rive-Animation) als Asset registrieren
|
||||||
config.resolver.assetExts = [...(config.resolver.assetExts ?? []), 'riv'];
|
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, {
|
module.exports = withNativeWind(config, {
|
||||||
input: './global.css',
|
input: './global.css',
|
||||||
});
|
});
|
||||||
|
|||||||
@ -80,6 +80,21 @@ class RebreakAccessibilityService : AccessibilityService() {
|
|||||||
* Aktiviert nur wenn der App-Lock armed UND der Schutz aktiv ist (`filter_enabled`).
|
* 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.
|
* 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
|
* @return true wenn die Activity geblockt wurde
|
||||||
*/
|
*/
|
||||||
private fun handleProtectedSettingsBlock(pkg: String, event: AccessibilityEvent): Boolean {
|
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
|
// DEBUG: alle Settings-Activities mitloggen damit wir OEM-Variationen sehen
|
||||||
Log.i(TAG, "settings-watch: $pkg / $className")
|
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 ->
|
val classMatchDangerous = DANGEROUS_ACTIVITY_PATTERNS.any { pattern ->
|
||||||
className.contains(pattern, ignoreCase = true)
|
className.contains(pattern, ignoreCase = true)
|
||||||
}
|
}
|
||||||
|
if (classMatchDangerous) {
|
||||||
// Phase 2 — Window-Content-Match: scannen wenn kein className-Match. OEMs
|
// Spezialfall: vpndialogs gehört System-seitig nur zur aktuell aktiven
|
||||||
// benutzen für Dialoge oft className die weder in unseren Patterns noch als
|
// VPN-Session. Da hier per Vorbedingung unser Schutz an ist, ist der
|
||||||
// "generic container" erkannt werden (z.B. Samsung's "AppDialog"). Der
|
// Dialog garantiert über uns — auch wenn der Profil-Name noch nicht
|
||||||
// Keyword-Cluster-Scan ist das Safety-Net: 2 Keywords aus dem gleichen
|
// in den Knoten gerendert wurde.
|
||||||
// Cluster = Block. False-positive-Risk gedämpft durch Throttling.
|
if (pkg == "com.android.vpndialogs") {
|
||||||
var contentReason: String? = null
|
return doBlock(pkg, className, "vpn-dialog", now)
|
||||||
if (!classMatchDangerous) {
|
}
|
||||||
contentReason = scanWindowForDangerousContent()
|
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
|
return false
|
||||||
if (!isDangerous) 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
|
lastBlockAt = now // post-block cooldown startet jetzt
|
||||||
// Doppel-BACK: einmal um Activity zu schließen, einmal als Backup falls
|
// Doppel-BACK: einmal um Activity zu schließen, einmal als Backup falls
|
||||||
// erste BACK nur einen Dialog dismissed.
|
// erste BACK nur einen Dialog dismissed.
|
||||||
@ -147,36 +179,19 @@ class RebreakAccessibilityService : AccessibilityService() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Scannt die aktuelle Window-Hierarchie nach Texten die auf eine
|
* Sammelt den gesamten sichtbaren Text der aktuellen Window-Hierarchie
|
||||||
* VPN/A11y/App-Uninstall-Page hindeuten. Wird genutzt wenn die Activity
|
* (Text + ContentDescription, bis Tiefe 10). Lowercased-Vereinigung wird
|
||||||
* generisch ist (z.B. Samsung's SubSettings) — dann müssen wir den
|
* vom Caller gegen Keywords / "rebreak" gematcht. Returnt null wenn keine
|
||||||
* Inhalt selbst inspizieren.
|
* Root-Window verfügbar (Page transitioning).
|
||||||
*/
|
*/
|
||||||
private fun scanWindowForDangerousContent(): String? {
|
private fun collectWindowText(): String? {
|
||||||
val root = rootInActiveWindow ?: return null
|
val root = rootInActiveWindow ?: return null
|
||||||
val texts = mutableListOf<String>()
|
val texts = mutableListOf<String>()
|
||||||
collectAllText(root, texts, depth = 0)
|
collectAllText(root, texts, depth = 0)
|
||||||
val joined = texts.joinToString(" | ").lowercase()
|
if (texts.isEmpty()) return null
|
||||||
// DEBUG: was steht eigentlich auf der Page? Hilft beim Patterns-Tuning.
|
val joined = texts.joinToString(" | ")
|
||||||
Log.d(TAG, "settings-content-text: ${joined.take(500)}")
|
Log.d(TAG, "settings-content-text: ${joined.take(500)}")
|
||||||
|
return joined
|
||||||
// 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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun collectAllText(node: AccessibilityNodeInfo?, sink: MutableList<String>, depth: Int) {
|
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,
|
* High-confidence Keywords — wenn EINER davon im Window-Content auftaucht,
|
||||||
* blocken wir sofort. Hochspezifisch zu uns. Enthält sowohl die aktuelle
|
* blocken wir sofort. Hochspezifisch zu uns. Enthält sowohl die aktuelle
|
||||||
* a11y-Service-Summary als auch die alte (für stale Installs / OEM-Cache).
|
* 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(
|
val HIGH_CONFIDENCE_KEYWORDS = listOf(
|
||||||
"rebreak filter", // VPN-Profil-Name aus Builder.setSession
|
"rebreak filter", // VPN-Profil-Name aus Builder.setSession
|
||||||
@ -249,55 +266,6 @@ class RebreakAccessibilityService : AccessibilityService() {
|
|||||||
"rebreak löschen",
|
"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(
|
val DANGEROUS_ACTIVITY_PATTERNS = listOf(
|
||||||
// VPN-Settings + VPN-Profil-Dialoge
|
// VPN-Settings + VPN-Profil-Dialoge
|
||||||
"VpnSettings",
|
"VpnSettings",
|
||||||
|
|||||||
@ -19,7 +19,7 @@
|
|||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>0.3.13</string>
|
<string>0.3.13</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>41</string>
|
<string>45</string>
|
||||||
<key>NSExtension</key>
|
<key>NSExtension</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>NSExtensionPointIdentifier</key>
|
<key>NSExtensionPointIdentifier</key>
|
||||||
|
|||||||
@ -19,7 +19,7 @@
|
|||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>0.3.13</string>
|
<string>0.3.13</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>41</string>
|
<string>45</string>
|
||||||
<key>NSExtension</key>
|
<key>NSExtension</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>NSExtensionPointIdentifier</key>
|
<key>NSExtensionPointIdentifier</key>
|
||||||
|
|||||||
@ -19,7 +19,7 @@
|
|||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>0.3.13</string>
|
<string>0.3.13</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>41</string>
|
<string>45</string>
|
||||||
<key>EXAppExtensionAttributes</key>
|
<key>EXAppExtensionAttributes</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>EXExtensionPointIdentifier</key>
|
<key>EXExtensionPointIdentifier</key>
|
||||||
|
|||||||
@ -56,7 +56,7 @@ type AppLockState = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const useAppLockStore = create<AppLockState>((set, get) => ({
|
export const useAppLockStore = create<AppLockState>((set, get) => ({
|
||||||
enabled: false,
|
enabled: true,
|
||||||
locked: false,
|
locked: false,
|
||||||
available: false,
|
available: false,
|
||||||
ready: false,
|
ready: false,
|
||||||
|
|||||||
@ -23,16 +23,19 @@ async function persist(patch: Partial<Pick<NotificationPrefsState, 'pushEnabled'
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const useNotificationPrefsStore = create<NotificationPrefsState>((set, get) => ({
|
export const useNotificationPrefsStore = create<NotificationPrefsState>((set, get) => ({
|
||||||
pushEnabled: false,
|
pushEnabled: true,
|
||||||
streakReminderEnabled: false,
|
streakReminderEnabled: false,
|
||||||
streakReminderTime: { hour: 9, minute: 0 },
|
streakReminderTime: { hour: 9, minute: 0 },
|
||||||
|
|
||||||
init: async () => {
|
init: async () => {
|
||||||
const stored = await AsyncStorage.getItem(STORAGE_KEY);
|
const stored = await AsyncStorage.getItem(STORAGE_KEY);
|
||||||
if (!stored) return;
|
if (!stored) {
|
||||||
|
await persist({ pushEnabled: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
const parsed = JSON.parse(stored);
|
const parsed = JSON.parse(stored);
|
||||||
set({
|
set({
|
||||||
pushEnabled: parsed.pushEnabled ?? false,
|
pushEnabled: parsed.pushEnabled ?? true,
|
||||||
streakReminderEnabled: parsed.streakReminderEnabled ?? false,
|
streakReminderEnabled: parsed.streakReminderEnabled ?? false,
|
||||||
streakReminderTime: parsed.streakReminderTime ?? { hour: 9, minute: 0 },
|
streakReminderTime: parsed.streakReminderTime ?? { hour: 9, minute: 0 },
|
||||||
});
|
});
|
||||||
|
|||||||
17
apps/rebreak-native/tmp/.deploy-runtimes
Normal file
17
apps/rebreak-native/tmp/.deploy-runtimes
Normal 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
|
||||||
@ -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).
|
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.
|
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).
|
**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).
|
**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 |
|
| 1. Geräteschutz | DNS-/URL-Filter + plattformspezifische Schutzmechaniken | ja |
|
||||||
| 2. Mail-Schutz | IMAP-IDLE-Daemon, Echtzeit-Filterung von Casino-Werbemails | 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 |
|
| 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)
|
## 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).
|
- **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.
|
- **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.
|
- 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**.
|
- 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
|
### 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.
|
- 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)
|
## 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 |
|
| Beziehung zur betroffenen Person | Partnerin / Mutter / Schwester |
|
||||||
| Hauptbedürfnis | Kontrolle ohne Konfrontation, technische Hilfe ohne Detektivarbeit |
|
| 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)
|
## 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) |
|
| 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.) |
|
| 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 |
|
| BetBlocker | UK (Charity) | iOS, Android, Win, Mac | ✅ | ❌ | ❌ | nein | timer-basiert | kostenlos |
|
||||||
| GamBlock | AU | Win, Mac, Android | ✅ | ❌ | ❌ | nein | stark | ~5–9 € |
|
| GamBlock | AU | Win, Mac, Android | ✅ | ❌ | ❌ | nein | stark | ~5–9 € |
|
||||||
@ -314,7 +314,7 @@ Rebreak ist bewusst **plattformübergreifend nativ**, nicht hybrid. Grund: der S
|
|||||||
| **Einziger Anbieter mit IMAP-IDLE-Mail-Schutz** in DE | Schließt Trigger-Kanal Werbemail vollständig | Technisch aufwändig (Server-Infra, OAuth-Integration); 12–18 Monate Vorsprung |
|
| **Einziger Anbieter mit IMAP-IDLE-Mail-Schutz** in DE | Schließt Trigger-Kanal Werbemail vollständig | Technisch aufwändig (Server-Infra, OAuth-Integration); 12–18 Monate Vorsprung |
|
||||||
| **macOS-nativer DNS-Schutz** kombiniert mit iOS/Android | Wettbewerber decken meist nur Mobile oder nur Desktop | Mittel (Apple-Tech-Investment nötig) |
|
| **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" |
|
| **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 |
|
| **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 |
|
| **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) |
|
| **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.
|
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.
|
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
|
## 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)
|
### Q2/2026 (jetzt, vor Förderung)
|
||||||
- NBank-Antragsunterlagen final.
|
- NBank-Antragsunterlagen final.
|
||||||
- Beta-Stabilisierung Build 19 (RebReakBinder).
|
- Beta-Stabilisierung Build 19 (Rebreak Magic).
|
||||||
- Outreach-Welle 1 Niedersachsen (LSG, NLS, MHH, STEP).
|
- Outreach-Welle 1 Niedersachsen (LSG, NLS, MHH, STEP).
|
||||||
- Eigenmittel-Runway: [PLATZHALTER: Monate].
|
- 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). |
|
| 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). |
|
| 2 | Apple / Google App-Store-Policy-Änderung | Mittel | Hoch | **Web-First-Failover:** macOS-App + PWA-Variante bereits in Architektur vorgesehen; Mail-Schutz funktioniert plattformunabhängig; Stripe-Web-Checkout schon heute Standard (keine IAP-Abhängigkeit). |
|
||||||
| 3 | OASIS-Reform 2026/27 deckt Offshore mit ab | Niedrig–Mittel | Mittel | OASIS-Reform würde Jahre brauchen, regulatorisch komplex (kein Hoheitsrecht über Offshore-Server). Mail-Schutz, Lyra, RebReakBinder bleiben einzigartig. Rebreak positioniert sich offen als **OASIS-Ergänzung**, nicht -Konkurrenz — Reform wäre eher Rückenwind. |
|
| 3 | OASIS-Reform 2026/27 deckt Offshore mit ab | Niedrig–Mittel | Mittel | OASIS-Reform würde Jahre brauchen, regulatorisch komplex (kein Hoheitsrecht über Offshore-Server). Mail-Schutz, Lyra, Rebreak Magic bleiben einzigartig. Rebreak positioniert sich offen als **OASIS-Ergänzung**, nicht -Konkurrenz — Reform wäre eher Rückenwind. |
|
||||||
| 4 | Solo-Founder-Bus-Factor | Mittel | Hoch | **Hire-Plan Q1/2027** (CTO/Full-Stack); zwischenzeitlich: Tech-Stack vollständig dokumentiert, Code-Reviews extern, Notfall-Mandat („Bus-Factor-Treuhänder") mit klar definiertem Zugang zu kritischer Infrastruktur. |
|
| 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. |
|
| 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. |
|
| 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). |
|
| 7 | Marketing-Wirkung bleibt hinter Plan | Mittel | Mittel | B2B-Multiplikator-Kanal hat geringere Stückkosten als Performance-Ads; Conversion-Erwartungen sind konservativ angesetzt (s. Kapitel 4); Pricing leicht reduzierbar bei Bedarf (kein Markenschaden, da von Beginn an niedriges Niveau). |
|
||||||
| 8 | RebReakBinder-Architektur wird von Apple sanktioniert | Niedrig–Mittel | Mittel | RebReakBinder ist **opt-in**, basiert auf dokumentierten Apple-Konfigurations-Mechanismen, keine Jailbreaks/Exploits. Fallback: klassisches Lock-Modus-Profil via Safari (vor RebReakBinder-Build 19 etablierter Weg). |
|
| 8 | Rebreak Magic-Architektur wird von Apple sanktioniert | Niedrig–Mittel | Mittel | Rebreak Magic ist **opt-in**, basiert auf dokumentierten Apple-Konfigurations-Mechanismen, keine Jailbreaks/Exploits. Fallback: klassisches Lock-Modus-Profil via Safari (vor Rebreak Magic-Build 19 etablierter Weg). |
|
||||||
| 9 | KI-Anbieter-Lock-in (Groq/Anthropic) | Niedrig | Niedrig–Mittel | Lyra-Persona ist anbieter-unabhängig spezifiziert (Single Source of Truth); Wechsel zu alternativem LLM in ~2 Wochen Engineering machbar. |
|
| 9 | KI-Anbieter-Lock-in (Groq/Anthropic) | Niedrig | Niedrig–Mittel | Lyra-Persona ist anbieter-unabhängig spezifiziert (Single Source of Truth); Wechsel zu alternativem LLM in ~2 Wochen Engineering machbar. |
|
||||||
| 10 | Wirksamkeitsstudie zeigt keinen signifikanten Effekt | Mittel | Hoch | Studiendesign so wählen, dass Sekundärendpunkte (Nutzungsdauer, Streak-Längen, Selbstwirksamkeitsscores) belastbar erhoben werden — auch ohne Primärendpunkt-Signifikanz lässt sich Versorgungs-Nutzen argumentieren. Vor Studienstart **Pre-Registration** und gut definierte Endpunkte. |
|
| 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
|
## 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)
|
## 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
|
## G. Kontakt
|
||||||
|
|
||||||
|
|||||||
546
ops/mdm/MAC_SUPERVISION_RESEARCH.md
Normal file
546
ops/mdm/MAC_SUPERVISION_RESEARCH.md
Normal 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.
|
||||||
@ -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).
|
- 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.
|
- 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.
|
- 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.
|
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.
|
- **„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.
|
- **„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).
|
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).
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user