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)
|
||||
2. **Pre-Flight** — Find-My-iPhone + Stolen-Device-Protection prüfen/ausschalten
|
||||
@ -8,9 +8,42 @@ End-User-Wizard für Self-Binding eines iPhones an ReBreak. Macht in einem 5-Ste
|
||||
4. **Enroll** — MDM-Enrollment-Profile auf iPhone installieren
|
||||
5. **Configure** — NanoMDM pusht: Lock-Profile + Take-Management + Settings(mdmSupervised=true)
|
||||
|
||||
Resultat: iPhone supervised by "ReBreak", App nicht löschbar, NEFilter aktiv (kein User-Toggle in Settings).
|
||||
Resultat: iPhone supervised by "Rebreak", App nicht löschbar, NEFilter aktiv (kein User-Toggle in Settings).
|
||||
|
||||
**Pre-Requirement**: ReBreak-App muss VOR Wizard-Start aus TestFlight installiert sein. Wizard nutzt `InstallApplication` mit `ChangeManagementState: Managed` (kein ManifestURL nötig, kein ABM-Account). Auto-Install via MDM-Push ist Phase 2 (braucht ABM oder Manifest-Hosting).
|
||||
## Warum "Magic"?
|
||||
|
||||
Normalerweise muss ein iPhone **komplett zurückgesetzt** werden um es zu supervisen (alle Daten weg, Werks-Setup, Apple-Configurator-Kabel-Pairing mit komplexem Setup).
|
||||
|
||||
Rebreak Magic macht das **ohne Reset** — deine Fotos, Apps, Settings bleiben. Das ist in der Branche unüblich und spart den Betroffenen massiv Zeit und Frust beim Onboarding.
|
||||
|
||||
**Pre-Requirement**: Rebreak-App muss VOR Wizard-Start aus TestFlight installiert sein. Wizard nutzt `InstallApplication` mit `ChangeManagementState: Managed` (kein ManifestURL nötig, kein ABM-Account). Auto-Install via MDM-Push ist Phase 2 (braucht ABM oder Manifest-Hosting).
|
||||
|
||||
## Voraussetzungen
|
||||
|
||||
| Tool | Wie |
|
||||
|---|---|
|
||||
| Xcode 16+ | App Store |
|
||||
| xcodegen | `brew install xcodegen` |
|
||||
| libimobiledevice | `brew install libimobiledevice` |
|
||||
| supervise-magic binary | aus `../../ops/mdm/supervise-magic/` (`make build`) |
|
||||
| cfgutil | Apple Configurator (App Store) → `/Applications/Apple Configurator.app/Contents/MacOS/cfgutil` für silent profile install |
|
||||
| create-dmg | `brew install create-dmg` (für DMG-Build)
|
||||
|
||||
Ja. Es nutzt Apple-offizielle MDM-APIs (gleiche wie Schul-iPads). Es installiert nichts Apple-Fremdes. Die Supervision kann jederzeit aufgehoben werden (Settings → Allgemein → VPN & Geräteverwaltung → Profile entfernen → Reboot).
|
||||
|
||||
### Was bedeutet das für mich?
|
||||
|
||||
- Die Rebreak-App ist nicht mehr per "App wackelt → X tippen" löschbar
|
||||
- Der NEFilter (Gambling-Domain-Blocker) lässt sich nicht in den Settings ausschalten
|
||||
- Du brauchst die Rebreak-Vertrauensperson um die Bindung zu lösen
|
||||
|
||||
### Kann ich das rückgängig machen?
|
||||
|
||||
Ja, aber mit Absicht — nicht im Affekt. Siehe Rebreak-App → Settings → Trustee-Override (7-Tage-Cooldown).
|
||||
|
||||
### Welche Daten sieht Rebreak?
|
||||
|
||||
Nur dass dein Device supervised IST + an unseren MDM-Server enrollt. Keine Inhalte, keine Browsing-History, keine Telemetrie über deine Nutzung.
|
||||
|
||||
## Status
|
||||
|
||||
@ -28,8 +61,7 @@ Resultat: iPhone supervised by "ReBreak", App nicht löschbar, NEFilter aktiv (k
|
||||
|
||||
## Build
|
||||
|
||||
```bash
|
||||
cd apps/rebreak-binder-mac
|
||||
### Development-magic-mac
|
||||
|
||||
# Einmalig: dependencies + supervise-magic-binary bauen
|
||||
(cd ../../ops/mdm/supervise-magic && make tidy && make build)
|
||||
@ -38,15 +70,58 @@ cd apps/rebreak-binder-mac
|
||||
xcodegen generate
|
||||
|
||||
# Bauen + öffnen
|
||||
open RebreakBinder.xcodeproj
|
||||
open RebreakMagic.xcodeproj
|
||||
# → ⌘R in Xcode
|
||||
```
|
||||
|
||||
Oder CLI-only:
|
||||
|
||||
```bash
|
||||
xcodebuild -project RebreakBinder.xcodeproj -scheme RebreakBinder -configuration Debug build
|
||||
open build/Debug/RebreakBinder.app
|
||||
xcodebuild -project RebreakMagic.xcodeproj -scheme RebreakMagic -configuration Debug build
|
||||
open build/Build/Products/Debug/RebreakMagic.app
|
||||
```
|
||||
|
||||
### Production-DMG (für Distribution)
|
||||
|
||||
```bash
|
||||
./build-dmg.sh
|
||||
```
|
||||
|
||||
Output: `build/RebreakMagic-0.1.0.dmg`
|
||||
|
||||
**Hinweis**: App ist unsigned (ad-hoc signature). User braucht Right-Click → Öffnen beim ersten Start (Gatekeeper-Warning). Für Production: Developer ID Application Cert nötig.
|
||||
|
||||
Falls das Icon nicht sofort erscheint nach Installation:
|
||||
|
||||
```bash
|
||||
sudo rm -rf /Library/Caches/com.apple.iconservices.store && killall Dock
|
||||
```
|
||||
- Notarization via `xcrun notarytool`
|
||||
- Staple Notarization-Ticket: `xcrun stapler staple`
|
||||
- DMG dann ohne Gatekeeper-Warning installierbar
|
||||
|
||||
### App-Icon
|
||||
|
||||
Das Rebreak-Logo ist im `Sources/Resources/Assets.xcassets/AppIcon.appiconset/` integriert (alle macOS-Größen von 16x16 bis 1024x1024). Icons werden aus `apps/rebreak-native/assets/icon.png` generiert.
|
||||
|
||||
Falls Icons neu generiert werden müssen (z.B. nach Logo-Update):
|
||||
|
||||
```bash
|
||||
# Master-Icon aus rebreak-native kopieren
|
||||
cp ../rebreak-native/assets/icon.png /tmp/master-icon.png
|
||||
|
||||
# macOS-Icon-Größen generieren via sips
|
||||
cd Sources/Resources/Assets.xcassets/AppIcon.appiconset/
|
||||
sips -z 16 16 /tmp/master-icon.png --out icon_16x16.png
|
||||
sips -z 32 32 /tmp/master-icon.png --out icon_16x16@2x.png
|
||||
sips -z 32 32 /tmp/master-icon.png --out icon_32x32.png
|
||||
sips -z 64 64 /tmp/master-icon.png --out icon_32x32@2x.png
|
||||
sips -z 128 128 /tmp/master-icon.png --out icon_128x128.png
|
||||
sips -z 256 256 /tmp/master-icon.png --out icon_128x128@2x.png
|
||||
sips -z 256 256 /tmp/master-icon.png --out icon_256x256.png
|
||||
sips -z 512 512 /tmp/master-icon.png --out icon_256x256@2x.png
|
||||
sips -z 512 512 /tmp/master-icon.png --out icon_512x512.png
|
||||
sips -z 1024 1024 /tmp/master-icon.png --out icon_512x512@2x.png
|
||||
```
|
||||
|
||||
## Config (lokal)
|
||||
@ -66,6 +141,47 @@ chmod 600 ~/.config/rebreak-binder/config.json
|
||||
|
||||
Production-Version legt das in Keychain ab — heute reicht plain JSON.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### iPhone wird nicht erkannt
|
||||
|
||||
```bash
|
||||
# Prüfe libimobiledevice
|
||||
idevice_id -l
|
||||
# Falls leer: USB-Kabel-/Port-Problem oder "Diesem Computer vertrauen?" Dialog nicht bestätigt
|
||||
|
||||
# PRelated Docs
|
||||
|
||||
- [ops/mdm/ARCHITECTURE.md](../../ops/mdm/ARCHITECTURE.md) — MDM-Infrastruktur-Overview
|
||||
- [ops/mdm/PHASES.md](../../ops/mdm/PHASES.md) — Roadmap (Self-Binding → ABM-DEP → Mac-Support)
|
||||
- [ops/mdm/RUNBOOK.md](../../ops/mdm/RUNBOOK.md) — NanoMDM-Server-Operations
|
||||
- [ops/mdm/supervise-magic/README.md](../../ops/mdm/supervise-magic/README.md) — supervise-magic-Technical-Deep-Dive
|
||||
|
||||
## License
|
||||
|
||||
Proprietary. © 2026 Raynis GmbH.
|
||||
|
||||
../../ops/mdm/supervise-magic/bin/supervise-magic --device <udid>
|
||||
# Check stdout/stderr
|
||||
```
|
||||
|
||||
### MDM-Enrollment schlägt fehl
|
||||
|
||||
- Prüfe NanoMDM-Server: `ssh rebreak-mdm 'pm2 status'` → `nanomdm-server` muss `online` sein
|
||||
- Prüfe nginx: `ssh rebreak-mdm 'curl -I https://mdm.rebreak.org'` → 200 OK
|
||||
- Prüfe APNs-Cert-Ablauf: siehe `ops/mdm/RUNBOOK.md` → APNs-Cert-Renewal-Section
|
||||
|
||||
### Icon wird als Placeholder angezeigt
|
||||
|
||||
macOS Icon-Cache clearen:
|
||||
|
||||
```bash
|
||||
sudo rm -rf /Library/Caches/com.apple.iconservices.store
|
||||
killall Dock Finder
|
||||
```
|
||||
|
||||
Dann App neu starten.
|
||||
|
||||
## TODOs (post-Skelett)
|
||||
|
||||
- [ ] **Lock-Profile-Refactor**: `allowAppRemoval=false` GLOBAL raus aus `rebreak-content-filter-sideload.mobileconfig`. Per-App-Lock kommt über Managed-App-State (MDM `InstallApplication` mit `ChangeManagementState: Managed` → iOS deaktiviert App-Wackel-„X" automatisch für managed apps). Andere Apps bleiben löschbar (bessere UX).
|
||||
@ -88,5 +204,6 @@ Production-Version legt das in Keychain ab — heute reicht plain JSON.
|
||||
## Sicherheit
|
||||
|
||||
- API-Key sollte langfristig in Keychain (heute: plain JSON, chmod 600)
|
||||
- App ist **unsigned** für lokales Testen — Gatekeeper-Warning beim ersten Öffnen
|
||||
- App ist **unsigned** für lokales Testen — Gatekeeper-Warning beim ersten Öffnen (Right-Click → Öffnen)
|
||||
- Production: Developer ID Application Cert + Notarization nötig (siehe Build → DMG)
|
||||
- Process-Spawn von go-binaries braucht **disabled App-Sandbox** (gesetzt in `project.yml`)
|
||||
|
||||
@ -1,6 +1,22 @@
|
||||
import Foundation
|
||||
import Observation
|
||||
|
||||
enum DebugSupervisionMode: String, CaseIterable, Identifiable {
|
||||
case none
|
||||
case forceSupervised
|
||||
case forceUnsupervised
|
||||
|
||||
var id: String { rawValue }
|
||||
|
||||
var title: String {
|
||||
switch self {
|
||||
case .none: return "Aus"
|
||||
case .forceSupervised: return "Force Supervised"
|
||||
case .forceUnsupervised: return "Force Unsupervised"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
@Observable
|
||||
final class WizardModel {
|
||||
@ -23,6 +39,15 @@ final class WizardModel {
|
||||
|
||||
var cooldownEndsAt: Date?
|
||||
|
||||
// Debug-Reset State
|
||||
var supervisionMode: DebugSupervisionMode = .none
|
||||
var resetRunning: Bool = false
|
||||
var resetStatus: String?
|
||||
var resetAll: Bool = true
|
||||
var resetEnrollmentProfile: Bool = true
|
||||
var resetLockProfile: Bool = true
|
||||
var resetApp: Bool = true
|
||||
|
||||
func advance() {
|
||||
if let next = WizardStep(rawValue: step.rawValue + 1) {
|
||||
step = next
|
||||
@ -44,5 +69,85 @@ final class WizardModel {
|
||||
configureError = nil
|
||||
showAdvancedLogs = false
|
||||
cooldownEndsAt = nil
|
||||
resetStatus = nil
|
||||
}
|
||||
|
||||
func startDebugReset() {
|
||||
guard device != nil else {
|
||||
resetStatus = "Kein iPhone erkannt."
|
||||
return
|
||||
}
|
||||
resetRunning = true
|
||||
resetStatus = "Führe Debug-Reset aus …"
|
||||
|
||||
Task {
|
||||
do {
|
||||
var changes: [String] = []
|
||||
|
||||
let removeEnrollment = resetAll || resetEnrollmentProfile
|
||||
let removeLock = resetAll || resetLockProfile
|
||||
let removeApp = resetAll || resetApp
|
||||
|
||||
let installedProfileIDs = await DeviceDetector.installedProfileIDs()
|
||||
var profileIDs: [String] = []
|
||||
if removeEnrollment, installedProfileIDs.contains(DeviceState.enrollmentProfileID) {
|
||||
profileIDs.append(DeviceState.enrollmentProfileID)
|
||||
}
|
||||
if removeLock, installedProfileIDs.contains(DeviceState.lockProfileID) {
|
||||
profileIDs.append(DeviceState.lockProfileID)
|
||||
}
|
||||
if !profileIDs.isEmpty {
|
||||
try await DeviceDetector.removeProfiles(identifiers: profileIDs)
|
||||
changes.append("Profile gelöscht: \(profileIDs.joined(separator: ", "))")
|
||||
}
|
||||
|
||||
if removeApp {
|
||||
try await DeviceDetector.removeApp(bundleID: "org.rebreak.app")
|
||||
changes.append("App gelöscht: org.rebreak.app")
|
||||
}
|
||||
|
||||
switch supervisionMode {
|
||||
case .forceSupervised:
|
||||
_ = try await SuperviseRunner.supervise(verbose: false) { _ in }
|
||||
changes.append("Mode gesetzt: supervised")
|
||||
case .forceUnsupervised:
|
||||
_ = try await SuperviseRunner.unsupervise { _ in }
|
||||
changes.append("Mode gesetzt: unsupervised")
|
||||
case .none:
|
||||
break
|
||||
}
|
||||
|
||||
let nowInstalledProfiles = await DeviceDetector.installedProfileIDs()
|
||||
let nowApps = await DeviceDetector.installedAppBundleIDs()
|
||||
let status = await DeviceDetector.readSupervisionStatus()
|
||||
|
||||
await MainActor.run {
|
||||
if changes.isEmpty {
|
||||
resetStatus = "Keine Aktion gewählt."
|
||||
} else {
|
||||
resetStatus = "✓ \(changes.joined(separator: " · "))"
|
||||
}
|
||||
|
||||
if var device = self.device {
|
||||
device.installedProfileIDs = nowInstalledProfiles
|
||||
device.installedAppBundleIDs = nowApps
|
||||
device.isSupervised = status.isSupervised
|
||||
device.supervisorOrgName = status.organizationName
|
||||
device.isFmiOn = status.findMyEnabled
|
||||
device.isEnrolled = nowInstalledProfiles.contains(DeviceState.enrollmentProfileID)
|
||||
if !nowApps.contains("org.rebreak.app") { device.isManaged = false }
|
||||
if !nowInstalledProfiles.contains(DeviceState.lockProfileID) { device.isFilterActive = false }
|
||||
self.device = device
|
||||
}
|
||||
|
||||
resetRunning = false
|
||||
}
|
||||
} catch {
|
||||
await MainActor.run {
|
||||
resetStatus = "✗ Reset fehlgeschlagen: \(error.localizedDescription)"
|
||||
resetRunning = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,16 +1,47 @@
|
||||
import SwiftUI
|
||||
|
||||
@main
|
||||
struct RebreakBinderApp: App {
|
||||
struct RebreakMagicApp: App {
|
||||
@State private var model = WizardModel()
|
||||
|
||||
var body: some Scene {
|
||||
WindowGroup("ReBreak Binder") {
|
||||
WindowGroup("Rebreak Magic") {
|
||||
ContentView()
|
||||
.environment(model)
|
||||
.frame(minWidth: 720, idealWidth: 800, minHeight: 600, idealHeight: 720)
|
||||
}
|
||||
.windowResizability(.contentSize)
|
||||
.windowStyle(.titleBar)
|
||||
.commands {
|
||||
CommandMenu("Aktionen") {
|
||||
Menu("Debug Supervision Mode") {
|
||||
Button(DebugSupervisionMode.none.title) {
|
||||
model.supervisionMode = .none
|
||||
}
|
||||
Button(DebugSupervisionMode.forceSupervised.title) {
|
||||
model.supervisionMode = .forceSupervised
|
||||
}
|
||||
Button(DebugSupervisionMode.forceUnsupervised.title) {
|
||||
model.supervisionMode = .forceUnsupervised
|
||||
}
|
||||
}
|
||||
|
||||
Toggle("Profile + App entfernen", isOn: $model.resetAll)
|
||||
Toggle("MDM Enrollment-Profil", isOn: $model.resetEnrollmentProfile)
|
||||
.disabled(model.resetAll)
|
||||
Toggle("Lock-Profil", isOn: $model.resetLockProfile)
|
||||
.disabled(model.resetAll)
|
||||
Toggle("ReBreak-App", isOn: $model.resetApp)
|
||||
.disabled(model.resetAll)
|
||||
|
||||
Divider()
|
||||
|
||||
Button("Debug-Reset ausführen") {
|
||||
model.startDebugReset()
|
||||
}
|
||||
.keyboardShortcut("r", modifiers: [.command, .shift, .option])
|
||||
.disabled(model.device == nil || model.resetRunning)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>ReBreak Binder</string>
|
||||
<string>Rebreak Magic</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
|
||||
@ -3,6 +3,7 @@ import SwiftUI
|
||||
|
||||
struct ContentView: View {
|
||||
@Environment(WizardModel.self) private var model
|
||||
@State private var showingHelp = false
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
@ -11,17 +12,29 @@ struct ContentView: View {
|
||||
appBadge
|
||||
|
||||
VStack(alignment: .leading, spacing: 1) {
|
||||
Text("ReBreak Binder")
|
||||
Text("Rebreak Magic")
|
||||
.font(.headline)
|
||||
Text("macOS supervision tool")
|
||||
Text("iPhone bind ohne Werks-Reset")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
Spacer()
|
||||
|
||||
// Help-Button
|
||||
Button(action: { showingHelp = true }) {
|
||||
Image(systemName: "questionmark.circle")
|
||||
.font(.title3)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.help("Hilfe & FAQ (⌘?)")
|
||||
.keyboardShortcut("?", modifiers: .command)
|
||||
|
||||
if model.step != .done {
|
||||
Text("Schritt \(model.step.stepNumber) von \(WizardStep.total)")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
.padding(.leading, 12)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 20)
|
||||
@ -33,6 +46,9 @@ struct ContentView: View {
|
||||
|
||||
Divider()
|
||||
|
||||
.sheet(isPresented: $showingHelp) {
|
||||
HelpView()
|
||||
}
|
||||
// Main content
|
||||
Group {
|
||||
switch model.step {
|
||||
|
||||
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
|
||||
|
||||
private enum DebugSupervisionMode: String, CaseIterable, Identifiable {
|
||||
case none
|
||||
case forceSupervised
|
||||
case forceUnsupervised
|
||||
|
||||
var id: String { rawValue }
|
||||
|
||||
var title: String {
|
||||
switch self {
|
||||
case .none: return "Kein Mode-Change"
|
||||
case .forceSupervised: return "Supervised setzen"
|
||||
case .forceUnsupervised: return "Unsupervised setzen"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct WelcomeView: View {
|
||||
@Environment(WizardModel.self) private var model
|
||||
|
||||
@State private var detecting = false
|
||||
@State private var error: String?
|
||||
@State private var pollTask: Task<Void, Never>?
|
||||
@State private var resetRunning = false
|
||||
@State private var resetStatus: String?
|
||||
@State private var resetAll = true
|
||||
@State private var resetEnrollmentProfile = true
|
||||
@State private var resetLockProfile = true
|
||||
@State private var resetApp = true
|
||||
@State private var supervisionMode: DebugSupervisionMode = .none
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 24) {
|
||||
@ -70,71 +47,12 @@ struct WelcomeView: View {
|
||||
.buttonStyle(.borderedProminent)
|
||||
.disabled(model.device == nil)
|
||||
}
|
||||
|
||||
resetSection
|
||||
}
|
||||
.padding(40)
|
||||
.onAppear { startDetection() }
|
||||
.onDisappear { pollTask?.cancel() }
|
||||
}
|
||||
|
||||
private var resetSection: some View {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Divider()
|
||||
Text("Interner Test-Reset")
|
||||
.font(.headline)
|
||||
Text("Wähle gezielt, was entfernt werden soll. Optional kann zusätzlich supervised/unsupervised für Tests gesetzt werden.")
|
||||
.font(.callout)
|
||||
.foregroundStyle(.secondary)
|
||||
|
||||
Toggle("Alles entfernen (Profile + App)", isOn: $resetAll)
|
||||
.toggleStyle(.checkbox)
|
||||
.onChange(of: resetAll) { _, newValue in
|
||||
if newValue {
|
||||
resetEnrollmentProfile = true
|
||||
resetLockProfile = true
|
||||
resetApp = true
|
||||
}
|
||||
}
|
||||
|
||||
Group {
|
||||
Toggle("MDM Enrollment-Profil löschen", isOn: $resetEnrollmentProfile)
|
||||
.toggleStyle(.checkbox)
|
||||
Toggle("Lock-Profil löschen", isOn: $resetLockProfile)
|
||||
.toggleStyle(.checkbox)
|
||||
Toggle("ReBreak-App löschen", isOn: $resetApp)
|
||||
.toggleStyle(.checkbox)
|
||||
}
|
||||
.disabled(resetAll)
|
||||
|
||||
Picker("Test-Mode", selection: $supervisionMode) {
|
||||
ForEach(DebugSupervisionMode.allCases) { mode in
|
||||
Text(mode.title).tag(mode)
|
||||
}
|
||||
}
|
||||
.pickerStyle(.segmented)
|
||||
|
||||
if let resetStatus {
|
||||
Text(resetStatus)
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
|
||||
HStack(spacing: 10) {
|
||||
if resetRunning {
|
||||
ProgressView()
|
||||
.controlSize(.small)
|
||||
}
|
||||
Button("Debug-Reset ausführen") {
|
||||
startDebugReset()
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
.disabled(model.device == nil || resetRunning || detecting)
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: 520, alignment: .leading)
|
||||
}
|
||||
|
||||
private var nextButtonLabel: String {
|
||||
if model.device?.isFullyBound == true {
|
||||
return "Weiter → Schutz aktivieren"
|
||||
@ -279,83 +197,4 @@ struct WelcomeView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func startDebugReset() {
|
||||
guard model.device != nil else {
|
||||
resetStatus = "Kein iPhone erkannt."
|
||||
return
|
||||
}
|
||||
resetRunning = true
|
||||
resetStatus = "Führe Debug-Reset aus …"
|
||||
|
||||
Task {
|
||||
do {
|
||||
var changes: [String] = []
|
||||
|
||||
let removeEnrollment = resetAll || resetEnrollmentProfile
|
||||
let removeLock = resetAll || resetLockProfile
|
||||
let removeApp = resetAll || resetApp
|
||||
|
||||
let installedProfileIDs = await DeviceDetector.installedProfileIDs()
|
||||
var profileIDs: [String] = []
|
||||
if removeEnrollment, installedProfileIDs.contains(DeviceState.enrollmentProfileID) {
|
||||
profileIDs.append(DeviceState.enrollmentProfileID)
|
||||
}
|
||||
if removeLock, installedProfileIDs.contains(DeviceState.lockProfileID) {
|
||||
profileIDs.append(DeviceState.lockProfileID)
|
||||
}
|
||||
if !profileIDs.isEmpty {
|
||||
try await DeviceDetector.removeProfiles(identifiers: profileIDs)
|
||||
changes.append("Profile gelöscht: \(profileIDs.joined(separator: ", "))")
|
||||
}
|
||||
|
||||
if removeApp {
|
||||
try await DeviceDetector.removeApp(bundleID: "org.rebreak.app")
|
||||
changes.append("App gelöscht: org.rebreak.app")
|
||||
}
|
||||
|
||||
switch supervisionMode {
|
||||
case .forceSupervised:
|
||||
_ = try await SuperviseRunner.supervise(verbose: false) { _ in }
|
||||
changes.append("Mode gesetzt: supervised")
|
||||
case .forceUnsupervised:
|
||||
_ = try await SuperviseRunner.unsupervise { _ in }
|
||||
changes.append("Mode gesetzt: unsupervised")
|
||||
case .none:
|
||||
break
|
||||
}
|
||||
|
||||
let nowInstalledProfiles = await DeviceDetector.installedProfileIDs()
|
||||
let nowApps = await DeviceDetector.installedAppBundleIDs()
|
||||
let status = await DeviceDetector.readSupervisionStatus()
|
||||
|
||||
await MainActor.run {
|
||||
if changes.isEmpty {
|
||||
resetStatus = "Keine Aktion gewählt."
|
||||
} else {
|
||||
resetStatus = "✓ \(changes.joined(separator: " · "))"
|
||||
}
|
||||
|
||||
if var device = model.device {
|
||||
device.installedProfileIDs = nowInstalledProfiles
|
||||
device.installedAppBundleIDs = nowApps
|
||||
device.isSupervised = status.isSupervised
|
||||
device.supervisorOrgName = status.organizationName
|
||||
device.isFmiOn = status.findMyEnabled
|
||||
device.isEnrolled = nowInstalledProfiles.contains(DeviceState.enrollmentProfileID)
|
||||
if !nowApps.contains("org.rebreak.app") { device.isManaged = false }
|
||||
if !nowInstalledProfiles.contains(DeviceState.lockProfileID) { device.isFilterActive = false }
|
||||
model.device = device
|
||||
}
|
||||
|
||||
resetRunning = false
|
||||
}
|
||||
} catch {
|
||||
await MainActor.run {
|
||||
resetStatus = "✗ Reset fehlgeschlagen: \(error.localizedDescription)"
|
||||
resetRunning = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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:
|
||||
bundleIdPrefix: org.rebreak.binder
|
||||
bundleIdPrefix: org.rebreak.magic
|
||||
deploymentTarget:
|
||||
macOS: "14.0"
|
||||
createIntermediateGroups: true
|
||||
@ -10,7 +10,7 @@ settings:
|
||||
base:
|
||||
SWIFT_VERSION: "5.10"
|
||||
MACOSX_DEPLOYMENT_TARGET: "14.0"
|
||||
PRODUCT_BUNDLE_IDENTIFIER: org.rebreak.binder.mac
|
||||
PRODUCT_BUNDLE_IDENTIFIER: org.rebreak.magic.mac
|
||||
MARKETING_VERSION: "0.1.0"
|
||||
CURRENT_PROJECT_VERSION: "1"
|
||||
DEVELOPMENT_TEAM: ""
|
||||
@ -20,7 +20,7 @@ settings:
|
||||
ENABLE_APP_SANDBOX: NO
|
||||
|
||||
targets:
|
||||
RebreakBinder:
|
||||
RebreakMagic:
|
||||
type: application
|
||||
platform: macOS
|
||||
sources:
|
||||
@ -33,7 +33,7 @@ targets:
|
||||
info:
|
||||
path: Sources/Resources/Info.plist
|
||||
properties:
|
||||
CFBundleDisplayName: ReBreak Binder
|
||||
CFBundleDisplayName: Rebreak Magic
|
||||
CFBundleShortVersionString: $(MARKETING_VERSION)
|
||||
CFBundleVersion: $(CURRENT_PROJECT_VERSION)
|
||||
LSMinimumSystemVersion: $(MACOSX_DEPLOYMENT_TARGET)
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
# Rebreak Deploy Secrets — Copy to .env.deploy.local (gitignored!)
|
||||
# Rebreak Deploy Secrets — Copy to .deploy-secrets.local (gitignored!)
|
||||
#
|
||||
# Source-Reihenfolge (deploy.sh lädt erstes vorhandenes File):
|
||||
# 1. apps/rebreak-native/.env.deploy.local
|
||||
# 1. apps/rebreak-native/.deploy-secrets.local
|
||||
# 2. ~/.config/rebreak/deploy.env
|
||||
#
|
||||
# ──────────────────────────────────────────────────────────────────────────
|
||||
|
||||
1
apps/rebreak-native/.gitignore
vendored
1
apps/rebreak-native/.gitignore
vendored
@ -39,6 +39,7 @@ yarn-error.*
|
||||
|
||||
# local env files
|
||||
.env*.local
|
||||
.deploy-secrets.local
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
|
||||
@ -43,10 +43,17 @@ pnpm exec expo run:ios
|
||||
pnpm exec expo run:ios --device "iPhone 15"
|
||||
```
|
||||
|
||||
Alternativ (wenn dev-iphone.sh vorhanden):
|
||||
Alternativ via konsolidiertem Dev-Script:
|
||||
|
||||
```bash
|
||||
bash apps/rebreak-native/dev-iphone.sh
|
||||
# Vollbuild auf iPhone via USB:
|
||||
bash apps/rebreak-native/dev.sh ios
|
||||
|
||||
# WiFi-Modus (kein Kabel, Metro über LAN):
|
||||
bash apps/rebreak-native/dev.sh ios --wifi
|
||||
|
||||
# Schneller JS-Reload ohne Native-Rebuild:
|
||||
bash apps/rebreak-native/dev.sh ios --no-build
|
||||
```
|
||||
|
||||
### Android Emulator
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to rebreak-native will be documented in this file.
|
||||
## v0.3.13 (Build 46 / versionCode 36) — 2026-05-31\n\nDM-Chat: Die letzte Nachricht wird jetzt zuverlässig oberhalb der Eingabezeile angezeigt — kein manuelles Nachscrollen mehr beim Öffnen oder nach dem Senden.
|
||||
|
||||
Chat-Übersicht: Zeitangaben sind feiner abgestuft — neben Minuten/Stunden jetzt auch Wochentag (z.B. „Mi"), danach Tage, Wochen, Monate und Jahre statt nur Datum.\n
|
||||
## v0.3.13 (Build 44 / versionCode 35) — 2026-05-31\n\nDM-Chat: scrollt jetzt zuverlässig zur neuesten Nachricht — auch nach eigenen gesendeten Nachrichten und beim Laden von Bildern.
|
||||
|
||||
Lyra-Sprachnachrichten: Wenn du auf Arabisch oder Türkisch sprichst, antwortet Lyra jetzt auch in der richtigen Sprache (Backend-Fix).
|
||||
|
||||
@ -4,16 +4,23 @@
|
||||
|
||||
### Development
|
||||
```bash
|
||||
# iOS Dev (Metro + Xcode):
|
||||
./dev.sh ios
|
||||
# Default = iPhone USB + Native-Build:
|
||||
./dev.sh
|
||||
|
||||
# iOS Dev auf physischem iPhone (USB):
|
||||
# Schneller UI-Loop (kein Rebuild, App schon installiert):
|
||||
./dev.sh ios --no-build
|
||||
./dev.sh android --no-build
|
||||
|
||||
# iOS Dev auf physischem iPhone (USB) mit Build:
|
||||
./dev.sh ios --device
|
||||
|
||||
# iOS Dev auf iPhone via WiFi:
|
||||
# iOS Dev auf iPhone via WiFi (Kabel ab):
|
||||
./dev.sh ios --wifi
|
||||
|
||||
# Android Dev:
|
||||
# iOS Simulator:
|
||||
./dev.sh ios --simulator
|
||||
|
||||
# Android Dev (Build + Install + Launch):
|
||||
./dev.sh android
|
||||
|
||||
# Nur Metro starten:
|
||||
@ -89,14 +96,16 @@
|
||||
- `install android` — Debug-APK auf Android Device installieren
|
||||
|
||||
### Flags (ios)
|
||||
- `--device` — Build auf physisches iPhone via USB
|
||||
- `--simulator` — Build auf iOS Simulator (default)
|
||||
- `--device` — Build auf physisches iPhone via USB **(default)**
|
||||
- `--simulator` — Build auf iOS Simulator
|
||||
- `--xcode` — Nur Xcode öffnen (manueller Build)
|
||||
- `--wifi` — Metro mit --host lan (für WiFi-Dev auf iPhone)
|
||||
- `--wifi` — Metro mit --host lan (für WiFi-Dev, kein Native-Build)
|
||||
- `--no-build` — **KEIN Native-Rebuild** → nur Metro starten (App muss schon installiert sein, schnellster UI/JS-Loop)
|
||||
|
||||
### Flags (android)
|
||||
- `--no-build` — Skip Gradle build, nur install last APK
|
||||
- `--no-launch` — Install but don't auto-launch
|
||||
- `--no-build` — **KEIN Gradle-Rebuild** → nur Metro starten (APK muss schon installiert sein, schnellster UI/JS-Loop)
|
||||
- `--no-launch` — Build+Install, aber kein Auto-Launch
|
||||
- `--wifi` — Metro mit --host lan (nur in Kombi mit `--no-build`)
|
||||
|
||||
### Flags (metro)
|
||||
- `--keep` — Cache behalten (kein --clear)
|
||||
|
||||
@ -36,7 +36,7 @@ export default ({ config }: ConfigContext): ExpoConfig => ({
|
||||
ios: {
|
||||
supportsTablet: true,
|
||||
bundleIdentifier: MAIN_BUNDLE,
|
||||
buildNumber: "45",
|
||||
buildNumber: "46",
|
||||
// Apple Sign-In Entitlement — Pflicht für expo-apple-authentication nativen
|
||||
// signInAsync()-Flow. Ohne flag generiert Expo's prebuild den
|
||||
// com.apple.developer.applesignin-Entitlement nicht in die .entitlements.
|
||||
@ -59,7 +59,7 @@ export default ({ config }: ConfigContext): ExpoConfig => ({
|
||||
|
||||
android: {
|
||||
package: "org.rebreak.app",
|
||||
versionCode: 35,
|
||||
versionCode: 36,
|
||||
adaptiveIcon: {
|
||||
// Foreground muss in der ~66%-Safe-Zone bleiben (Launcher-Mask clippt den
|
||||
// Außenring) → adaptive-foreground.png ist das Logo auf transparentem
|
||||
|
||||
@ -30,10 +30,15 @@ type DmConversation = {
|
||||
|
||||
function formatTime(ts: string, justNowLabel: string): string {
|
||||
const diff = Date.now() - new Date(ts).getTime();
|
||||
const day = 86_400_000;
|
||||
if (diff < 60_000) return justNowLabel;
|
||||
if (diff < 3_600_000) return `${Math.floor(diff / 60_000)}m`;
|
||||
if (diff < 86_400_000) return `${Math.floor(diff / 3_600_000)}h`;
|
||||
return new Date(ts).toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit' });
|
||||
if (diff < day) return `${Math.floor(diff / 3_600_000)}h`;
|
||||
if (diff < 7 * day) return new Date(ts).toLocaleDateString('de-DE', { weekday: 'short' });
|
||||
if (diff < 30 * day) return `${Math.floor(diff / day)}d`;
|
||||
if (diff < 60 * day) return `${Math.floor(diff / (7 * day))}w`;
|
||||
if (diff < 365 * day) return `${Math.floor(diff / (30 * day))}mo`;
|
||||
return `${Math.floor(diff / (365 * day))}y`;
|
||||
}
|
||||
|
||||
function DmItem({ conv, onPress }: { conv: DmConversation; onPress: () => void }) {
|
||||
|
||||
@ -27,7 +27,6 @@ import { useColors } from '../lib/theme';
|
||||
import { useLanguageStore } from '../stores/language';
|
||||
import { useAppLockStore } from '../stores/appLock';
|
||||
import { useLyraVoiceStore } from '../stores/lyraVoice';
|
||||
import { BrandSplash } from '../components/BrandSplash';
|
||||
import { AppLockGate } from '../components/AppLockGate';
|
||||
import { DeviceLimitReachedSheet } from '../components/DeviceLimitReachedSheet';
|
||||
import { OnlinePresenceProvider } from '../components/OnlinePresenceProvider';
|
||||
@ -124,7 +123,10 @@ function RootLayoutInner() {
|
||||
}, [fontsLoaded, loading, appLockReady]);
|
||||
|
||||
if (!fontsLoaded || loading || !appLockReady) {
|
||||
return <BrandSplash />;
|
||||
// Nativer expo-splash-screen bleibt sichtbar bis SplashScreen.hideAsync()
|
||||
// im Effect oben aufgerufen wird → kein Flicker durch zusätzlichen
|
||||
// React-Splash mehr (User-Feedback: "geht sehr schnell vorbei und zuckt")
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@ -87,6 +87,7 @@ export default function DmScreen() {
|
||||
const [uploading, setUploading] = useState(false);
|
||||
const [keyboardVisible, setKeyboardVisible] = useState(false);
|
||||
const [keyboardHeight, setKeyboardHeight] = useState(0);
|
||||
const [inputBarHeight, setInputBarHeight] = useState(60);
|
||||
|
||||
// Reset aller conversation-spezifischen States wenn userId wechselt (Stack-Reuse)
|
||||
useEffect(() => {
|
||||
@ -516,7 +517,7 @@ export default function DmScreen() {
|
||||
contentContainerStyle={{
|
||||
paddingHorizontal: 0,
|
||||
paddingTop: 12,
|
||||
paddingBottom: 12 + insets.bottom + (keyboardVisible ? keyboardHeight : 0),
|
||||
paddingBottom: inputBarHeight + 12,
|
||||
}}
|
||||
showsVerticalScrollIndicator={false}
|
||||
keyboardDismissMode="interactive"
|
||||
@ -531,6 +532,10 @@ export default function DmScreen() {
|
||||
style={{ backgroundColor: colors.bg }}
|
||||
>
|
||||
<View
|
||||
onLayout={(e) => {
|
||||
const h = e.nativeEvent.layout.height;
|
||||
if (Math.abs(h - inputBarHeight) > 1) setInputBarHeight(h);
|
||||
}}
|
||||
style={[
|
||||
styles.inputBar,
|
||||
{
|
||||
|
||||
@ -41,6 +41,8 @@ export type ChatMsg = {
|
||||
reactions?: MessageReaction[];
|
||||
/** Soft-Delete-Tombstone. */
|
||||
deleted?: boolean;
|
||||
/** Optimistic-UI Status (pending = wird gesendet, failed = Fehler). */
|
||||
status?: 'pending' | 'sent' | 'failed';
|
||||
};
|
||||
|
||||
type Props = {
|
||||
@ -192,6 +194,8 @@ export function ChatBubble({
|
||||
{ backgroundColor: bubbleBg },
|
||||
!msg.isOwn && styles.bubbleOtherBorder,
|
||||
isImageOnly && { padding: 4 },
|
||||
msg.status === 'pending' && { opacity: 0.6 },
|
||||
msg.status === 'failed' && { borderWidth: 1, borderColor: '#ef4444' },
|
||||
]}
|
||||
>
|
||||
{msg.replyTo && (
|
||||
@ -327,7 +331,7 @@ export function ChatBubble({
|
||||
>
|
||||
{formatTime(msg.createdAt)}
|
||||
</Text>
|
||||
{msg.isOwn && !hideReadStatus && (
|
||||
{isDM && msg.isOwn && msg.status !== 'pending' && msg.status !== 'failed' && (
|
||||
<Ionicons
|
||||
name={msg.readAt ? 'checkmark-done' : 'checkmark'}
|
||||
size={12}
|
||||
@ -335,6 +339,22 @@ export function ChatBubble({
|
||||
style={{ marginLeft: 2 }}
|
||||
/>
|
||||
)}
|
||||
{msg.status === 'pending' && (
|
||||
<Ionicons
|
||||
name="time-outline"
|
||||
size={11}
|
||||
color="rgba(0,0,0,0.35)"
|
||||
style={{ marginLeft: 2 }}
|
||||
/>
|
||||
)}
|
||||
{msg.status === 'failed' && (
|
||||
<Ionicons
|
||||
name="alert-circle"
|
||||
size={11}
|
||||
color="#ef4444"
|
||||
style={{ marginLeft: 2 }}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
|
||||
@ -127,7 +127,7 @@ export function HeaderDropdownMenu({ visible, onClose, topOffset = 80 }: Props)
|
||||
<BlurView
|
||||
intensity={85}
|
||||
tint={scheme === 'dark' ? 'systemThickMaterialDark' : 'systemThickMaterialLight'}
|
||||
style={StyleSheet.absoluteFill}
|
||||
style={styles.blurFill}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
@ -263,3 +263,11 @@ export function HeaderDropdownMenu({ visible, onClose, topOffset = 80 }: Props)
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
blurFill: {
|
||||
...StyleSheet.absoluteFillObject,
|
||||
borderRadius: 18,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
});
|
||||
|
||||
@ -2,11 +2,13 @@ import { useEffect, useRef } from 'react';
|
||||
import { Animated, Easing, Text, useWindowDimensions, View } from 'react-native';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import * as Notifications from 'expo-notifications';
|
||||
import { useColors } from '../../../lib/theme';
|
||||
import { OnboardingShell } from '../OnboardingShell';
|
||||
import { LyraBubble } from '../LyraBubble';
|
||||
import { CTABar } from '../CTABar';
|
||||
import { FaqAccordion, type FaqItem } from '../../FaqAccordion';
|
||||
import { useNotificationPrefsStore } from '../../../stores/notificationPrefs';
|
||||
|
||||
// Top-5 (kuratiert für Onboarding-Ende) — alle 8 sind unter app/help/faq.tsx.
|
||||
const ONBOARDING_FAQ_IDS = [1, 2, 4, 5, 8] as const;
|
||||
@ -24,6 +26,7 @@ export function DoneSlide({
|
||||
const colors = useColors();
|
||||
const scale = useRef(new Animated.Value(0.6)).current;
|
||||
const opacity = useRef(new Animated.Value(0)).current;
|
||||
const setPushEnabled = useNotificationPrefsStore((s) => s.setPushEnabled);
|
||||
|
||||
const faqItems: FaqItem[] = ONBOARDING_FAQ_IDS.map((id) => ({
|
||||
q: t(`help.faq_q${id}`),
|
||||
@ -40,7 +43,16 @@ export function DoneSlide({
|
||||
easing: Easing.out(Easing.cubic),
|
||||
}),
|
||||
]).start();
|
||||
}, []);
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
const { status } = await Notifications.requestPermissionsAsync();
|
||||
if (status !== 'granted') {
|
||||
await setPushEnabled(false);
|
||||
}
|
||||
} catch {}
|
||||
})();
|
||||
}, [setPushEnabled]);
|
||||
|
||||
return (
|
||||
<OnboardingShell
|
||||
|
||||
@ -36,8 +36,8 @@
|
||||
# ./deploy.sh all --dry-run
|
||||
#
|
||||
# CREDENTIALS:
|
||||
# Persistenz (empfohlen): siehe .env.deploy.local.example
|
||||
# cp .env.deploy.local.example .env.deploy.local # gitignored
|
||||
# Persistenz (empfohlen): siehe .deploy-secrets.local.example
|
||||
# cp .deploy-secrets.local.example .deploy-secrets.local # gitignored
|
||||
# # einmalig editieren — deploy.sh source'd das automatisch
|
||||
#
|
||||
# iOS TestFlight / Ad-Hoc:
|
||||
@ -346,12 +346,14 @@ while [[ $# -gt 0 ]]; do
|
||||
done
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# Secrets-File auto-loading (NICHT committen — siehe .env.deploy.local.example)
|
||||
# Secrets-File auto-loading (NICHT committen — siehe .deploy-secrets.local.example)
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# Lädt automatisch:
|
||||
# apps/rebreak-native/.env.deploy.local (lokal, gitignored)
|
||||
# ~/.config/rebreak/deploy.env (global fallback, optional)
|
||||
for secrets_file in "$SCRIPT_DIR/.env.deploy.local" "$HOME/.config/rebreak/deploy.env"; do
|
||||
# apps/rebreak-native/.deploy-secrets.local (lokal, gitignored)
|
||||
# ~/.config/rebreak/deploy.env (global fallback, optional)
|
||||
# Hinweis: Datei heißt bewusst NICHT .env.* — sonst greift Metro's File-Watcher
|
||||
# zu und versucht die Shell-Exports als JS zu parsen (SyntaxError im Bundle).
|
||||
for secrets_file in "$SCRIPT_DIR/.deploy-secrets.local" "$HOME/.config/rebreak/deploy.env"; do
|
||||
if [[ -f "$secrets_file" ]]; then
|
||||
# shellcheck disable=SC1090
|
||||
set -a; source "$secrets_file"; set +a
|
||||
@ -402,7 +404,7 @@ require_asc_api_key() {
|
||||
[[ -n "$ASC_API_KEY_PATH" ]] || missing+=("ASC_API_KEY_PATH")
|
||||
if (( ${#missing[@]} > 0 )); then
|
||||
die "iOS Signing braucht ASC API-Key. Fehlt: ${missing[*]}
|
||||
→ Editiere apps/rebreak-native/.env.deploy.local (siehe .env.deploy.local.example)"
|
||||
→ Editiere apps/rebreak-native/.deploy-secrets.local (siehe .deploy-secrets.local.example)"
|
||||
fi
|
||||
if [[ ! -f "$ASC_API_KEY_PATH" ]]; then
|
||||
die "ASC API-Key Datei existiert nicht: $ASC_API_KEY_PATH
|
||||
|
||||
@ -1,24 +1,30 @@
|
||||
#!/bin/bash
|
||||
# dev.sh — ReBreak Native Development Tooling
|
||||
#
|
||||
# Konsolidiert: dev-ios.sh + dev-iphone.sh + metro.sh (alle gelöscht).
|
||||
#
|
||||
# SUBCOMMANDS:
|
||||
# ./dev.sh default: ios (Metro + Xcode)
|
||||
# ./dev.sh ios iOS Dev (Metro + Xcode Workspace / Simulator)
|
||||
# ./dev.sh android Android Dev (Metro + Gradle build + install)
|
||||
# ./dev.sh default: ios --device (physisches iPhone USB + Build)
|
||||
# ./dev.sh ios iOS Dev (Default: USB-Device mit Build)
|
||||
# ./dev.sh android Android Dev (Gradle Build + Install + Launch)
|
||||
# ./dev.sh metro Nur Metro starten
|
||||
# ./dev.sh clean iOS: Nuclear clean (Pods, DerivedData, Archives)
|
||||
# ./dev.sh install ios Build Release + Install auf iPhone USB
|
||||
# ./dev.sh install android Build Debug APK + Install auf Android Device
|
||||
#
|
||||
# FLAGS (ios):
|
||||
# --device Build auf physisches iPhone via USB
|
||||
# --simulator Build auf iOS Simulator (default)
|
||||
# --device Build auf physisches iPhone via USB (DEFAULT)
|
||||
# --simulator Build auf iOS Simulator
|
||||
# --xcode Nur Xcode öffnen (manueller Build)
|
||||
# --wifi Metro mit --host lan (für WiFi-Dev auf iPhone)
|
||||
# --wifi Metro mit --host lan (WiFi-Dev, KEIN Native-Build)
|
||||
# --no-build KEIN Native-Rebuild → nur Metro starten
|
||||
# (für schnellen UI/JS-Reload — App muss installiert sein)
|
||||
#
|
||||
# FLAGS (android):
|
||||
# --no-build Skip Gradle build, nur install last APK
|
||||
# --no-launch Install but don't auto-launch
|
||||
# --no-build KEIN Gradle-Rebuild → nur Metro starten
|
||||
# (für schnellen UI/JS-Reload — APK muss installiert sein)
|
||||
# --no-launch Build+Install, aber kein Auto-Launch
|
||||
# --wifi Metro mit --host lan (nur in Kombi mit --no-build)
|
||||
#
|
||||
# FLAGS (metro):
|
||||
# --keep Cache behalten (kein --clear)
|
||||
@ -28,20 +34,21 @@
|
||||
# --xcode + Xcode öffnen am Ende
|
||||
#
|
||||
# BEISPIELE:
|
||||
# # iOS Dev auf Simulator:
|
||||
# ./dev.sh ios
|
||||
# # Default: iPhone USB + Native-Build:
|
||||
# ./dev.sh
|
||||
#
|
||||
# # iOS Dev auf physischem iPhone via USB:
|
||||
# ./dev.sh ios --device
|
||||
# # Schneller UI-Loop (App schon installiert, nur JS-Reload):
|
||||
# ./dev.sh ios --no-build
|
||||
# ./dev.sh android --no-build
|
||||
#
|
||||
# # iOS Dev auf iPhone via WiFi (Metro LAN):
|
||||
# # WiFi-Dev (Kabel ab, Metro über LAN):
|
||||
# ./dev.sh ios --wifi
|
||||
#
|
||||
# # Android Dev:
|
||||
# ./dev.sh android
|
||||
# # iOS Simulator:
|
||||
# ./dev.sh ios --simulator
|
||||
#
|
||||
# # Nur Metro starten:
|
||||
# ./dev.sh metro
|
||||
# # Nur Xcode öffnen:
|
||||
# ./dev.sh ios --xcode
|
||||
#
|
||||
# # iOS Clean + Rebuild:
|
||||
# ./dev.sh clean --build
|
||||
@ -95,8 +102,9 @@ export REBREAK_DEV="${REBREAK_DEV:-0}"
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
cmd_ios() {
|
||||
local MODE="simulator"
|
||||
local MODE="device" # Default: physisches iPhone via USB
|
||||
local WIFI=false
|
||||
local BUILD=true # Default: nativen Build laufen lassen
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
@ -104,25 +112,40 @@ cmd_ios() {
|
||||
--simulator) MODE="simulator"; shift ;;
|
||||
--xcode) MODE="xcode"; shift ;;
|
||||
--wifi) WIFI=true; shift ;;
|
||||
--no-build) BUILD=false; shift ;;
|
||||
*) die "Unbekannter Flag für 'ios': $1" ;;
|
||||
esac
|
||||
done
|
||||
|
||||
section "iOS Dev Mode"
|
||||
|
||||
if $WIFI; then
|
||||
log "Metro: WiFi-Modus (--host lan)"
|
||||
echo ""
|
||||
echo "Mac LAN-IP:"
|
||||
ipconfig getifaddr en0 2>/dev/null || ipconfig getifaddr en1 2>/dev/null || echo " (kein WiFi/Ethernet detected)"
|
||||
echo ""
|
||||
echo "Falls dev-client Metro nicht automatisch findet:"
|
||||
echo " im iPhone-Launcher → 'Enter URL manually' → http://<LAN-IP>:8081"
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# --no-build / WiFi: KEIN Native-Rebuild, nur Metro + Dev-Client
|
||||
# → schnellster Loop für reine UI/JS-Änderungen
|
||||
# → App muss schon auf dem Device/Simulator installiert sein
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
if ! $BUILD || $WIFI; then
|
||||
local HOST_FLAG=""
|
||||
if $WIFI; then
|
||||
HOST_FLAG="--host lan"
|
||||
log "Metro: WiFi-Modus (--host lan)"
|
||||
echo ""
|
||||
echo "Mac LAN-IP:"
|
||||
ipconfig getifaddr en0 2>/dev/null || ipconfig getifaddr en1 2>/dev/null || echo " (kein WiFi/Ethernet detected)"
|
||||
echo ""
|
||||
echo "Falls dev-client Metro nicht automatisch findet:"
|
||||
echo " im iPhone-Launcher → 'Enter URL manually' → http://<LAN-IP>:8081"
|
||||
else
|
||||
log "Metro: USB/Local-Modus (kein Native-Rebuild)"
|
||||
echo "App auf Device/Simulator muss schon installiert sein."
|
||||
echo "Beim Öffnen connected dev-client automatisch zu Metro."
|
||||
fi
|
||||
echo ""
|
||||
log "Killing old Metro on port 8081..."
|
||||
lsof -ti:8081 | xargs kill -9 2>/dev/null || true
|
||||
lsof -ti:8081 2>/dev/null | xargs kill -9 2>/dev/null || true
|
||||
pkill -f "expo start" 2>/dev/null || true
|
||||
echo ""
|
||||
exec pnpm expo start --host lan --clear --dev-client
|
||||
exec pnpm expo start $HOST_FLAG --clear --dev-client
|
||||
fi
|
||||
|
||||
case "$MODE" in
|
||||
@ -137,6 +160,7 @@ cmd_ios() {
|
||||
|
||||
device)
|
||||
log "Building für physisches iPhone (USB)..."
|
||||
echo "ℹ️ Für schnellen UI-Reload ohne Rebuild: './dev.sh ios --no-build'"
|
||||
pnpm expo run:ios --device
|
||||
;;
|
||||
|
||||
@ -150,17 +174,44 @@ cmd_ios() {
|
||||
cmd_android() {
|
||||
local BUILD=true
|
||||
local LAUNCH=true
|
||||
local WIFI=false
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--no-build) BUILD=false; shift ;;
|
||||
--no-launch) LAUNCH=false; shift ;;
|
||||
--wifi) WIFI=true; shift ;;
|
||||
*) die "Unbekannter Flag für 'android': $1" ;;
|
||||
esac
|
||||
done
|
||||
|
||||
section "Android Dev Mode"
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# --no-build: KEIN Gradle-Rebuild, nur Metro + Dev-Client
|
||||
# → schnellster Loop für reine UI/JS-Änderungen
|
||||
# → APK muss schon auf dem Device installiert sein
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
if ! $BUILD; then
|
||||
local HOST_FLAG=""
|
||||
if $WIFI; then
|
||||
HOST_FLAG="--host lan"
|
||||
log "Metro: WiFi-Modus (--host lan)"
|
||||
echo ""
|
||||
echo "Mac LAN-IP:"
|
||||
ipconfig getifaddr en0 2>/dev/null || ipconfig getifaddr en1 2>/dev/null || echo " (kein WiFi/Ethernet detected)"
|
||||
else
|
||||
log "Metro: USB/ADB-Modus (kein Gradle-Rebuild)"
|
||||
echo "APK muss schon auf dem Device installiert sein."
|
||||
fi
|
||||
echo ""
|
||||
log "Killing old Metro on port 8081..."
|
||||
lsof -ti:8081 2>/dev/null | xargs kill -9 2>/dev/null || true
|
||||
pkill -f "expo start" 2>/dev/null || true
|
||||
echo ""
|
||||
exec pnpm expo start $HOST_FLAG --clear --dev-client
|
||||
fi
|
||||
|
||||
command -v adb >/dev/null 2>&1 || die "adb nicht gefunden — brew install --cask android-platform-tools"
|
||||
|
||||
local DEVICE_COUNT
|
||||
@ -178,10 +229,9 @@ cmd_android() {
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if $BUILD; then
|
||||
log "Building Debug APK..."
|
||||
(cd "$ANDROID_DIR" && ./gradlew assembleDebug --console=plain)
|
||||
fi
|
||||
log "Building Debug APK..."
|
||||
echo "ℹ️ Für schnellen UI-Reload ohne Rebuild: './dev.sh android --no-build'"
|
||||
(cd "$ANDROID_DIR" && ./gradlew assembleDebug --console=plain)
|
||||
|
||||
local APK="$ANDROID_DIR/app/build/outputs/apk/debug/app-debug.apk"
|
||||
[[ -f "$APK" ]] || die "APK nicht gefunden: $APK"
|
||||
|
||||
@ -26,6 +26,19 @@ config.resolver.unstable_enablePackageExports = true;
|
||||
// 4. .riv (Rive-Animation) als Asset registrieren
|
||||
config.resolver.assetExts = [...(config.resolver.assetExts ?? []), 'riv'];
|
||||
|
||||
// 5. Block .env* files — die sind Shell-Snippets (deploy.sh sourced sie),
|
||||
// KEIN JS und sollen niemals von Metro/Babel angefasst werden.
|
||||
// Ohne diesen Block kann Metro's File-Watcher (watchFolders=monorepoRoot)
|
||||
// sie irrtümlich als Modul transformieren → "Unexpected token (1:0)" auf '#'.
|
||||
config.resolver.blockList = [
|
||||
...(Array.isArray(config.resolver.blockList)
|
||||
? config.resolver.blockList
|
||||
: config.resolver.blockList
|
||||
? [config.resolver.blockList]
|
||||
: []),
|
||||
/\.env(\..*)?$/,
|
||||
];
|
||||
|
||||
module.exports = withNativeWind(config, {
|
||||
input: './global.css',
|
||||
});
|
||||
|
||||
@ -80,6 +80,21 @@ class RebreakAccessibilityService : AccessibilityService() {
|
||||
* Aktiviert nur wenn der App-Lock armed UND der Schutz aktiv ist (`filter_enabled`).
|
||||
* Letzteres lässt den User nach einem legitim abgelaufenen Cooldown wieder raus.
|
||||
*
|
||||
* **Strikte Match-Regel (v2 nach Field-Bug-Reports):** Wir blocken NUR wenn die
|
||||
* aktuelle Window-Hierarchie unsere App tatsächlich im Text führt. Heißt konkret:
|
||||
* - HIGH_CONFIDENCE_KEYWORD ("rebreak filter", "sichert den schutz", …) → sofort
|
||||
* - ODER (dangerous activity-class wie VpnSettings/Uninstaller) UND Wort
|
||||
* "rebreak" im Page-Text → block
|
||||
* - Spezialfall `com.android.vpndialogs`: Dieses System-Package gibt es NUR für
|
||||
* aktive VPN-Sessions. Da unser Schutz hier per Vorbedingung an ist (Early
|
||||
* Exit oben), ist jeder Trennen-Dialog hier sicher unser eigener → class-match
|
||||
* reicht.
|
||||
*
|
||||
* Die alte „2-Keyword-Cluster"-Heuristik („vpn"+„trennen", „eingabehilfe"+„apps",
|
||||
* „rebreak"+„speicher" …) wurde entfernt — sie blockte legitime Pages wie die
|
||||
* Verbindungs-Übersicht, die Einstellungen-Hauptseite und die Play-Store-„Apps
|
||||
* verwalten"-Liste (false-positive über Generika wie „Speicher", „Apps", „VPN").
|
||||
*
|
||||
* @return true wenn die Activity geblockt wurde
|
||||
*/
|
||||
private fun handleProtectedSettingsBlock(pkg: String, event: AccessibilityEvent): Boolean {
|
||||
@ -105,25 +120,42 @@ class RebreakAccessibilityService : AccessibilityService() {
|
||||
// DEBUG: alle Settings-Activities mitloggen damit wir OEM-Variationen sehen
|
||||
Log.i(TAG, "settings-watch: $pkg / $className")
|
||||
|
||||
// Phase 1 — Class-Name-Match (ältere Stock-Android-Patterns)
|
||||
// Window-Text einmal sammeln — wird sowohl für High-Confidence- als auch
|
||||
// Class+Rebreak-Check gebraucht. Kann null sein wenn root window flackert.
|
||||
val pageText = collectWindowText()?.lowercase().orEmpty()
|
||||
|
||||
// 1) High-confidence Keyword im Text → sofortiger Block (Rebreak-spezifisch)
|
||||
val highConfHit = HIGH_CONFIDENCE_KEYWORDS.firstOrNull { pageText.contains(it) }
|
||||
if (highConfHit != null) {
|
||||
return doBlock(pkg, className, "high-confidence:$highConfHit", now)
|
||||
}
|
||||
|
||||
// 2) Activity-Class-Match — aber NUR blocken wenn Page klar über Rebreak
|
||||
// ist (Wort "rebreak" im Text). Sonst würde z.B. die App-Info-Page einer
|
||||
// beliebigen anderen App geblockt werden.
|
||||
val classMatchDangerous = DANGEROUS_ACTIVITY_PATTERNS.any { pattern ->
|
||||
className.contains(pattern, ignoreCase = true)
|
||||
}
|
||||
|
||||
// Phase 2 — Window-Content-Match: scannen wenn kein className-Match. OEMs
|
||||
// benutzen für Dialoge oft className die weder in unseren Patterns noch als
|
||||
// "generic container" erkannt werden (z.B. Samsung's "AppDialog"). Der
|
||||
// Keyword-Cluster-Scan ist das Safety-Net: 2 Keywords aus dem gleichen
|
||||
// Cluster = Block. False-positive-Risk gedämpft durch Throttling.
|
||||
var contentReason: String? = null
|
||||
if (!classMatchDangerous) {
|
||||
contentReason = scanWindowForDangerousContent()
|
||||
if (classMatchDangerous) {
|
||||
// Spezialfall: vpndialogs gehört System-seitig nur zur aktuell aktiven
|
||||
// VPN-Session. Da hier per Vorbedingung unser Schutz an ist, ist der
|
||||
// Dialog garantiert über uns — auch wenn der Profil-Name noch nicht
|
||||
// in den Knoten gerendert wurde.
|
||||
if (pkg == "com.android.vpndialogs") {
|
||||
return doBlock(pkg, className, "vpn-dialog", now)
|
||||
}
|
||||
if (pageText.contains("rebreak")) {
|
||||
return doBlock(pkg, className, "class+rebreak", now)
|
||||
}
|
||||
Log.d(TAG, "settings-watch: dangerous class $className but no 'rebreak' in text → skip")
|
||||
}
|
||||
|
||||
val isDangerous = classMatchDangerous || contentReason != null
|
||||
if (!isDangerous) return false
|
||||
return false
|
||||
}
|
||||
|
||||
Log.w(TAG, "TAMPER-BLOCK: $pkg / $className (reason=${contentReason ?: "class-match"})")
|
||||
/** Hilft Toast+Back-Action wiederzuverwenden (DRY für die beiden Block-Pfade). */
|
||||
private fun doBlock(pkg: String, className: String, reason: String, now: Long): Boolean {
|
||||
Log.w(TAG, "TAMPER-BLOCK: $pkg / $className (reason=$reason)")
|
||||
lastBlockAt = now // post-block cooldown startet jetzt
|
||||
// Doppel-BACK: einmal um Activity zu schließen, einmal als Backup falls
|
||||
// erste BACK nur einen Dialog dismissed.
|
||||
@ -147,36 +179,19 @@ class RebreakAccessibilityService : AccessibilityService() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Scannt die aktuelle Window-Hierarchie nach Texten die auf eine
|
||||
* VPN/A11y/App-Uninstall-Page hindeuten. Wird genutzt wenn die Activity
|
||||
* generisch ist (z.B. Samsung's SubSettings) — dann müssen wir den
|
||||
* Inhalt selbst inspizieren.
|
||||
* Sammelt den gesamten sichtbaren Text der aktuellen Window-Hierarchie
|
||||
* (Text + ContentDescription, bis Tiefe 10). Lowercased-Vereinigung wird
|
||||
* vom Caller gegen Keywords / "rebreak" gematcht. Returnt null wenn keine
|
||||
* Root-Window verfügbar (Page transitioning).
|
||||
*/
|
||||
private fun scanWindowForDangerousContent(): String? {
|
||||
private fun collectWindowText(): String? {
|
||||
val root = rootInActiveWindow ?: return null
|
||||
val texts = mutableListOf<String>()
|
||||
collectAllText(root, texts, depth = 0)
|
||||
val joined = texts.joinToString(" | ").lowercase()
|
||||
// DEBUG: was steht eigentlich auf der Page? Hilft beim Patterns-Tuning.
|
||||
if (texts.isEmpty()) return null
|
||||
val joined = texts.joinToString(" | ")
|
||||
Log.d(TAG, "settings-content-text: ${joined.take(500)}")
|
||||
|
||||
// High-confidence Keywords: 1 Treffer reicht (sehr spezifisch zu uns)
|
||||
for (keyword in HIGH_CONFIDENCE_KEYWORDS) {
|
||||
if (joined.contains(keyword)) {
|
||||
Log.d(TAG, "settings-watch: high-confidence keyword match: '$keyword'")
|
||||
return "high-confidence:$keyword"
|
||||
}
|
||||
}
|
||||
|
||||
// Standard-Cluster: min 2 Keywords nötig (false-positive-Schutz)
|
||||
for ((cluster, keywords) in DANGEROUS_TEXT_CLUSTERS) {
|
||||
val matchCount = keywords.count { joined.contains(it) }
|
||||
if (matchCount >= 2) {
|
||||
Log.d(TAG, "settings-watch: cluster $cluster matched $matchCount keywords")
|
||||
return cluster
|
||||
}
|
||||
}
|
||||
return null
|
||||
return joined
|
||||
}
|
||||
|
||||
private fun collectAllText(node: AccessibilityNodeInfo?, sink: MutableList<String>, depth: Int) {
|
||||
@ -239,6 +254,8 @@ class RebreakAccessibilityService : AccessibilityService() {
|
||||
* High-confidence Keywords — wenn EINER davon im Window-Content auftaucht,
|
||||
* blocken wir sofort. Hochspezifisch zu uns. Enthält sowohl die aktuelle
|
||||
* a11y-Service-Summary als auch die alte (für stale Installs / OEM-Cache).
|
||||
*
|
||||
* Müssen lowercase sein (Text wird vor Match lowercased).
|
||||
*/
|
||||
val HIGH_CONFIDENCE_KEYWORDS = listOf(
|
||||
"rebreak filter", // VPN-Profil-Name aus Builder.setSession
|
||||
@ -249,55 +266,6 @@ class RebreakAccessibilityService : AccessibilityService() {
|
||||
"rebreak löschen",
|
||||
)
|
||||
|
||||
/**
|
||||
* Standard-Cluster — min 2 Keywords pro Cluster nötig damit
|
||||
* harmlose Settings-Suche keine false-positives auslöst.
|
||||
*/
|
||||
val DANGEROUS_TEXT_CLUSTERS = mapOf(
|
||||
"vpn-page" to listOf(
|
||||
"vpn",
|
||||
"rebreak filter", // unser Profil-Name (siehe Builder.setSession)
|
||||
"always-on",
|
||||
"always-on-vpn",
|
||||
"verbindung trennen",
|
||||
"trennen",
|
||||
"verbindungen ohne vpn",
|
||||
"block connections",
|
||||
"vpn-profil",
|
||||
"konto entfernen",
|
||||
"vergessen",
|
||||
"always-on vpn",
|
||||
),
|
||||
"a11y-page" to listOf(
|
||||
"bedienungshilfe",
|
||||
"eingabehilfe",
|
||||
"accessibility",
|
||||
"sichert den schutz", // unsere aktuelle a11y-Service-Summary
|
||||
"filtert glücksspiel", // alte a11y-Service-Summary (legacy installs)
|
||||
"rebreak filter",
|
||||
"installierte apps",
|
||||
"installed services",
|
||||
"downloaded apps",
|
||||
"berechtigung erteilen",
|
||||
"service deaktivieren",
|
||||
"service ausschalten",
|
||||
),
|
||||
"uninstall-page" to listOf(
|
||||
"deinstallieren",
|
||||
"uninstall",
|
||||
"rebreak",
|
||||
"möchten sie diese app",
|
||||
"do you want to uninstall",
|
||||
"app entfernen",
|
||||
"force stop",
|
||||
"stopp erzwingen",
|
||||
"speicher",
|
||||
"daten löschen",
|
||||
"clear data",
|
||||
"cache leeren",
|
||||
),
|
||||
)
|
||||
|
||||
val DANGEROUS_ACTIVITY_PATTERNS = listOf(
|
||||
// VPN-Settings + VPN-Profil-Dialoge
|
||||
"VpnSettings",
|
||||
|
||||
@ -19,7 +19,7 @@
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>0.3.13</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>41</string>
|
||||
<string>45</string>
|
||||
<key>NSExtension</key>
|
||||
<dict>
|
||||
<key>NSExtensionPointIdentifier</key>
|
||||
|
||||
@ -19,7 +19,7 @@
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>0.3.13</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>41</string>
|
||||
<string>45</string>
|
||||
<key>NSExtension</key>
|
||||
<dict>
|
||||
<key>NSExtensionPointIdentifier</key>
|
||||
|
||||
@ -19,7 +19,7 @@
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>0.3.13</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>41</string>
|
||||
<string>45</string>
|
||||
<key>EXAppExtensionAttributes</key>
|
||||
<dict>
|
||||
<key>EXExtensionPointIdentifier</key>
|
||||
|
||||
@ -56,7 +56,7 @@ type AppLockState = {
|
||||
};
|
||||
|
||||
export const useAppLockStore = create<AppLockState>((set, get) => ({
|
||||
enabled: false,
|
||||
enabled: true,
|
||||
locked: false,
|
||||
available: false,
|
||||
ready: false,
|
||||
|
||||
@ -23,16 +23,19 @@ async function persist(patch: Partial<Pick<NotificationPrefsState, 'pushEnabled'
|
||||
}
|
||||
|
||||
export const useNotificationPrefsStore = create<NotificationPrefsState>((set, get) => ({
|
||||
pushEnabled: false,
|
||||
pushEnabled: true,
|
||||
streakReminderEnabled: false,
|
||||
streakReminderTime: { hour: 9, minute: 0 },
|
||||
|
||||
init: async () => {
|
||||
const stored = await AsyncStorage.getItem(STORAGE_KEY);
|
||||
if (!stored) return;
|
||||
if (!stored) {
|
||||
await persist({ pushEnabled: true });
|
||||
return;
|
||||
}
|
||||
const parsed = JSON.parse(stored);
|
||||
set({
|
||||
pushEnabled: parsed.pushEnabled ?? false,
|
||||
pushEnabled: parsed.pushEnabled ?? true,
|
||||
streakReminderEnabled: parsed.streakReminderEnabled ?? false,
|
||||
streakReminderTime: parsed.streakReminderTime ?? { hour: 9, minute: 0 },
|
||||
});
|
||||
|
||||
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).
|
||||
3. **24/7-KI-Begleitung in Drucksituationen** — der KI-Coach „Lyra" liefert Soforthilfe zwischen Beratungsterminen, verweist aktiv an Profi-Strukturen (DigiSucht, lokale Fachstellen) und ersetzt diese ausdrücklich nicht.
|
||||
|
||||
Optional steht der **Selbstbindungs-Modus „RebReakBinder"** zur Verfügung (Lock-Architektur, die App und Filter ohne Vertrauensperson nicht mehr deinstallierbar macht) — ein Schutz-Layer, den kein anderer Wettbewerber in Deutschland anbietet.
|
||||
Optional steht der **Selbstbindungs-Modus „Rebreak Magic"** zur Verfügung (Lock-Architektur, die App und Filter ohne Vertrauensperson nicht mehr deinstallierbar macht) — ein Schutz-Layer, den kein anderer Wettbewerber in Deutschland anbietet.
|
||||
|
||||
**Marktpotenzial (konservativ).** Zielmarkt Deutschland: ca. 1,3 Mio. problematische/pathologische Spielende + ca. 367.000 OASIS-Gesperrte + Angehörige (Faktor ~3 pro Betroffenem). **Erreichbarer Markt (SOM) Jahr 3: 30.000 zahlende Nutzer** ≙ ca. 0,8 % Penetration der Kernzielgruppe. Sekundär: **B2B-Lizenzierung** an Suchtberatungs-Träger (~1.400 Fachstellen DE) und mittelfristig **DiGA-Listung (BfArM)** mit Erstattung durch gesetzliche Krankenkassen (Größenordnung ~200 € / Quartal / Nutzer).
|
||||
|
||||
**Stand heute.** App **live in geschlossener Beta**. Kernfeatures (DNS-Block, IMAP-IDLE-Mail-Schutz, Lyra, Streak-Tracker, Multi-Device, RebReakBinder Build 19) sind ausgerollt und in Praxistest. Outreach an Suchtfachstellen Niedersachsen (LSG-Nds, NLS, STEP, Lukas-Werk, MHH) hat begonnen. **Pricing: Pro 3,99 €/Monat · Legend 7,99 €/Monat** (zzgl. 14-Tage-Trial; **kein Free-Tier**). Stripe-Web-Checkout (kein In-App-Purchase wegen Apple/Google-Glücksspiel-Policies).
|
||||
**Stand heute.** App **live in geschlossener Beta**. Kernfeatures (DNS-Block, IMAP-IDLE-Mail-Schutz, Lyra, Streak-Tracker, Multi-Device, Rebreak Magic Build 19) sind ausgerollt und in Praxistest. Outreach an Suchtfachstellen Niedersachsen (LSG-Nds, NLS, STEP, Lukas-Werk, MHH) hat begonnen. **Pricing: Pro 3,99 €/Monat · Legend 7,99 €/Monat** (zzgl. 14-Tage-Trial; **kein Free-Tier**). Stripe-Web-Checkout (kein In-App-Purchase wegen Apple/Google-Glücksspiel-Policies).
|
||||
|
||||
**Finanzierungsbedarf.** **75.000 € NBank-Gründungskredit** zur Finanzierung von DiGA-Wirksamkeitsstudie, IT-Sicherheit/ISMS-Aufbau, Marketing/B2B-Outreach, Gründer-Lohn-Puffer und externer Beratung (BfArM, Datenschutz). Damit erreicht Rebreak innerhalb von 24 Monaten eine **belastbare Marktposition als deutscher Schutz-Tech-Anbieter mit eingereichtem BfArM-Antrag und ersten institutionellen Kooperationen** (LOI Lukas-Werk Q3/2026 angestrebt).
|
||||
|
||||
@ -106,7 +106,7 @@ Rebreak ist eine **native Multi-Plattform-App** (iOS, Android, macOS) mit server
|
||||
| 1. Geräteschutz | DNS-/URL-Filter + plattformspezifische Schutzmechaniken | ja |
|
||||
| 2. Mail-Schutz | IMAP-IDLE-Daemon, Echtzeit-Filterung von Casino-Werbemails | ja |
|
||||
| 3. Begleitung | KI-Coach „Lyra" (Crisis-Mode + Coach-Mode), Streak, Community | ja |
|
||||
| 4. Selbstbindung | RebReakBinder (optionaler Lock-Modus für iOS) | ja (Build 19) |
|
||||
| 4. Selbstbindung | Rebreak Magic (optionaler Lock-Modus für iOS) | ja (Build 19) |
|
||||
|
||||
## 3.2 Geräteschutz (Layer 1)
|
||||
|
||||
@ -153,7 +153,7 @@ Rebreak ist eine **native Multi-Plattform-App** (iOS, Android, macOS) mit server
|
||||
- **Community-Bereich** — moderierte Posts, Reaktion-System, keine privaten DMs (bewusste Friktion zur Vermeidung von Wett-Verabredungen).
|
||||
- **Onboarding** — Selbsteinschätzung-Fragebogen (angelehnt an SOGS-Items), Setup-Assistent für Mail-Konten, optionale Trustee-Verbindung.
|
||||
|
||||
## 3.6 RebReakBinder — Selbstbindungs-Modus (optional)
|
||||
## 3.6 Rebreak Magic — Selbstbindungs-Modus (optional)
|
||||
|
||||
- macOS-Begleit-App (Build 19, seit 29.05.2026), die das **Self-Bind-MDM-Setup** auf wenige Klicks reduziert.
|
||||
- Ergebnis: iPhone ist supervised, Rebreak-App + DNS-Filter **können nicht mehr via Settings entfernt werden**.
|
||||
@ -229,7 +229,7 @@ Rebreak ist bewusst **plattformübergreifend nativ**, nicht hybrid. Grund: der S
|
||||
### Lücke 4 — Geräte-Bypass
|
||||
|
||||
- Auch wer freiwillig DNS-Filter setzt, kann sie in 30 Sekunden wieder löschen — meist im Moment des stärksten Impulses.
|
||||
- **Rebreak schließt diese Lücke** durch optionalen RebReakBinder (Selbstbindung mit Trustee-Recovery).
|
||||
- **Rebreak schließt diese Lücke** durch optionalen Rebreak Magic (Selbstbindung mit Trustee-Recovery).
|
||||
|
||||
## 4.3 Marktgröße (Top-Down + Bottom-Up)
|
||||
|
||||
@ -280,7 +280,7 @@ Rebreak ist bewusst **plattformübergreifend nativ**, nicht hybrid. Grund: der S
|
||||
|---|---|
|
||||
| Beziehung zur betroffenen Person | Partnerin / Mutter / Schwester |
|
||||
| Hauptbedürfnis | Kontrolle ohne Konfrontation, technische Hilfe ohne Detektivarbeit |
|
||||
| Rolle in Rebreak | Trustee (RebReakBinder-Recovery), passive Mit-Nutzung der Streak-Sicht, evtl. eigener Account für Selbsthilfe |
|
||||
| Rolle in Rebreak | Trustee (Rebreak Magic-Recovery), passive Mit-Nutzung der Streak-Sicht, evtl. eigener Account für Selbsthilfe |
|
||||
|
||||
## 5.3 Tertiäre Persona — B2B-Multiplikator „Fachstellenleiter Dr. K." (Phase 2)
|
||||
|
||||
@ -299,7 +299,7 @@ Rebreak ist bewusst **plattformübergreifend nativ**, nicht hybrid. Grund: der S
|
||||
|
||||
| Anbieter | Herkunft | Plattformen | DNS-/URL-Block | Mail-Schutz | KI-Coach | DE-Fokus | Lock-Modus | Preis (Monat) |
|
||||
|---|---|---|---|---|---|---|---|---|
|
||||
| **Rebreak** | DE | iOS, Android, **macOS** | ✅ (~330k Domains) | ✅ **IMAP-IDLE Echtzeit** | ✅ Lyra (DE) | ✅ | ✅ RebReakBinder | 3,99 / 7,99 € |
|
||||
| **Rebreak** | DE | iOS, Android, **macOS** | ✅ (~330k Domains) | ✅ **IMAP-IDLE Echtzeit** | ✅ Lyra (DE) | ✅ | ✅ Rebreak Magic | 3,99 / 7,99 € |
|
||||
| Gamban | UK | iOS, Android, Win, Mac | ✅ | ❌ | ❌ | teilweise EN | nur passwortgeschützt | ~3,75 € (£ 2,99 ähnl.) |
|
||||
| BetBlocker | UK (Charity) | iOS, Android, Win, Mac | ✅ | ❌ | ❌ | nein | timer-basiert | kostenlos |
|
||||
| GamBlock | AU | Win, Mac, Android | ✅ | ❌ | ❌ | nein | stark | ~5–9 € |
|
||||
@ -314,7 +314,7 @@ Rebreak ist bewusst **plattformübergreifend nativ**, nicht hybrid. Grund: der S
|
||||
| **Einziger Anbieter mit IMAP-IDLE-Mail-Schutz** in DE | Schließt Trigger-Kanal Werbemail vollständig | Technisch aufwändig (Server-Infra, OAuth-Integration); 12–18 Monate Vorsprung |
|
||||
| **macOS-nativer DNS-Schutz** kombiniert mit iOS/Android | Wettbewerber decken meist nur Mobile oder nur Desktop | Mittel (Apple-Tech-Investment nötig) |
|
||||
| **Deutscher Sitz, DSGVO-konform, deutschsprachiger KI-Coach** | Akzeptanz bei Fachstellen, Krankenkassen, BfArM | Hoch — internationale Player können das nicht „nachrüsten" |
|
||||
| **Selbstbindungs-Modus (RebReakBinder)** mit Trustee | Stärkster verfügbarer Anti-Rückfall-Mechanismus auf iOS am Markt | Mittel — Apple-Policy-Risiko vorhanden, Architektur empirisch validiert |
|
||||
| **Selbstbindungs-Modus (Rebreak Magic)** mit Trustee | Stärkster verfügbarer Anti-Rückfall-Mechanismus auf iOS am Markt | Mittel — Apple-Policy-Risiko vorhanden, Architektur empirisch validiert |
|
||||
| **Lyra — KI-Begleiter in DE Sprache** | Brückenfunktion zwischen Beratungsterminen | Mittel — andere können nachziehen, aber Persona/Vokabular sind Asset |
|
||||
| **Stripe-Web-Checkout** | Vermeidet Apple/Google-Cut **und** Glücksspiel-Store-Policies | Hoch — strukturell |
|
||||
| **B2B-Anschlussfähigkeit Fachstellen DE** | Vertriebskanal jenseits ASO | Hoch (Beziehungs-Asset) |
|
||||
@ -418,7 +418,7 @@ Drei Kern-Botschaften:
|
||||
|
||||
1. **„Schutz, wo OASIS endet."** — sachlich, faktisch, kein Pathos.
|
||||
2. **„Lyra ist da, wenn die Beraterin gerade nicht da ist."** — Komplementär-Position, keine Therapie-Behauptung.
|
||||
3. **„Du entscheidest. Auch wenn du später anders entscheidest."** — Selbstbindungs-Logik (RebReakBinder) ohne Bevormundung.
|
||||
3. **„Du entscheidest. Auch wenn du später anders entscheidest."** — Selbstbindungs-Logik (Rebreak Magic) ohne Bevormundung.
|
||||
|
||||
## 8.5 PR-Anker 2026/27
|
||||
|
||||
@ -516,7 +516,7 @@ Antrag │ FAGS-2.Welle │ Pen-Test bestanden │ Start delphi/MHH │ er
|
||||
|
||||
### Q2/2026 (jetzt, vor Förderung)
|
||||
- NBank-Antragsunterlagen final.
|
||||
- Beta-Stabilisierung Build 19 (RebReakBinder).
|
||||
- Beta-Stabilisierung Build 19 (Rebreak Magic).
|
||||
- Outreach-Welle 1 Niedersachsen (LSG, NLS, MHH, STEP).
|
||||
- Eigenmittel-Runway: [PLATZHALTER: Monate].
|
||||
|
||||
@ -664,12 +664,12 @@ Die NBank-Förderung deckt damit explizit die **Wachstums- und Compliance-Posten
|
||||
|---|---|---|---|---|
|
||||
| 1 | DiGA-Antrag wird abgelehnt | Mittel (BfArM lehnt ~40 % erstmaliger Anträge teilweise ab) | Hoch | **B2B-First-Pivot:** Lizenzierung an Fachstellen-Träger als Haupt-Erlöskanal; DiGA-Pfad als langfristige Option neu aufsetzen. Studienergebnisse bleiben verwertbar (B2B-Argument, PR, wissenschaftliche Glaubwürdigkeit). |
|
||||
| 2 | Apple / Google App-Store-Policy-Änderung | Mittel | Hoch | **Web-First-Failover:** macOS-App + PWA-Variante bereits in Architektur vorgesehen; Mail-Schutz funktioniert plattformunabhängig; Stripe-Web-Checkout schon heute Standard (keine IAP-Abhängigkeit). |
|
||||
| 3 | OASIS-Reform 2026/27 deckt Offshore mit ab | Niedrig–Mittel | Mittel | OASIS-Reform würde Jahre brauchen, regulatorisch komplex (kein Hoheitsrecht über Offshore-Server). Mail-Schutz, Lyra, RebReakBinder bleiben einzigartig. Rebreak positioniert sich offen als **OASIS-Ergänzung**, nicht -Konkurrenz — Reform wäre eher Rückenwind. |
|
||||
| 3 | OASIS-Reform 2026/27 deckt Offshore mit ab | Niedrig–Mittel | Mittel | OASIS-Reform würde Jahre brauchen, regulatorisch komplex (kein Hoheitsrecht über Offshore-Server). Mail-Schutz, Lyra, Rebreak Magic bleiben einzigartig. Rebreak positioniert sich offen als **OASIS-Ergänzung**, nicht -Konkurrenz — Reform wäre eher Rückenwind. |
|
||||
| 4 | Solo-Founder-Bus-Factor | Mittel | Hoch | **Hire-Plan Q1/2027** (CTO/Full-Stack); zwischenzeitlich: Tech-Stack vollständig dokumentiert, Code-Reviews extern, Notfall-Mandat („Bus-Factor-Treuhänder") mit klar definiertem Zugang zu kritischer Infrastruktur. |
|
||||
| 5 | DSGVO-Vorfall im Mail-Schutz | Niedrig | Sehr hoch | DPIA vor Skalierung (Anwaltsbudget); Mail-Inhalte nicht persistiert (in-memory only); regelmäßiger Pen-Test (12 k€-Posten); Audit-Logs revisionssicher. |
|
||||
| 6 | Großer internationaler Player lokalisiert auf DE | Niedrig | Mittel | Trust-Vorsprung durch Fachstellen-LOIs, DE-Sitz, DiGA-Pfad. Internationale Player haben Schwierigkeiten, deutschsprachige Fachstellen-Strukturen zu durchdringen. |
|
||||
| 7 | Marketing-Wirkung bleibt hinter Plan | Mittel | Mittel | B2B-Multiplikator-Kanal hat geringere Stückkosten als Performance-Ads; Conversion-Erwartungen sind konservativ angesetzt (s. Kapitel 4); Pricing leicht reduzierbar bei Bedarf (kein Markenschaden, da von Beginn an niedriges Niveau). |
|
||||
| 8 | RebReakBinder-Architektur wird von Apple sanktioniert | Niedrig–Mittel | Mittel | RebReakBinder ist **opt-in**, basiert auf dokumentierten Apple-Konfigurations-Mechanismen, keine Jailbreaks/Exploits. Fallback: klassisches Lock-Modus-Profil via Safari (vor RebReakBinder-Build 19 etablierter Weg). |
|
||||
| 8 | Rebreak Magic-Architektur wird von Apple sanktioniert | Niedrig–Mittel | Mittel | Rebreak Magic ist **opt-in**, basiert auf dokumentierten Apple-Konfigurations-Mechanismen, keine Jailbreaks/Exploits. Fallback: klassisches Lock-Modus-Profil via Safari (vor Rebreak Magic-Build 19 etablierter Weg). |
|
||||
| 9 | KI-Anbieter-Lock-in (Groq/Anthropic) | Niedrig | Niedrig–Mittel | Lyra-Persona ist anbieter-unabhängig spezifiziert (Single Source of Truth); Wechsel zu alternativem LLM in ~2 Wochen Engineering machbar. |
|
||||
| 10 | Wirksamkeitsstudie zeigt keinen signifikanten Effekt | Mittel | Hoch | Studiendesign so wählen, dass Sekundärendpunkte (Nutzungsdauer, Streak-Längen, Selbstwirksamkeitsscores) belastbar erhoben werden — auch ohne Primärendpunkt-Signifikanz lässt sich Versorgungs-Nutzen argumentieren. Vor Studienstart **Pre-Registration** und gut definierte Endpunkte. |
|
||||
|
||||
@ -724,11 +724,11 @@ Reale LOI-Originale werden im Nachgang separat eingereicht, sobald unterschriebe
|
||||
|
||||
## E. Screenshots der App
|
||||
|
||||
[PLATZHALTER: Onboarding-Screen, Streak-Tab, Lyra-Coach-Chat, Mail-Schutz-Übersicht, Schutz-Status-Screen, RebReakBinder-Setup-Screen — 6 Screenshots, idealerweise iOS und macOS.]
|
||||
[PLATZHALTER: Onboarding-Screen, Streak-Tab, Lyra-Coach-Chat, Mail-Schutz-Übersicht, Schutz-Status-Screen, Rebreak Magic-Setup-Screen — 6 Screenshots, idealerweise iOS und macOS.]
|
||||
|
||||
## F. Lyra-Persona / Produkt-Spezifikation (Auszug)
|
||||
|
||||
Auf Anforderung wird die vollständige Lyra-Persona-Spezifikation und die technische Architektur-Übersicht (RebReakBinder, IMAP-IDLE-Daemon, NEFilter-Setup) als separates Anlagen-Dokument bereitgestellt.
|
||||
Auf Anforderung wird die vollständige Lyra-Persona-Spezifikation und die technische Architektur-Übersicht (Rebreak Magic, IMAP-IDLE-Daemon, NEFilter-Setup) als separates Anlagen-Dokument bereitgestellt.
|
||||
|
||||
## G. Kontakt
|
||||
|
||||
|
||||
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).
|
||||
- Echtzeit-Mail-Schutz (IMAP-IDLE), der Casino-Werbemails löscht, bevor die Push-Benachrichtigung auslöst — meines Wissens das einzige Tool im deutschen Markt mit diesem Feature.
|
||||
- KI-Begleiter „Lyra" für die akuten 24/7-Momente zwischen den Beratungsterminen, **ausdrücklich nicht als Ersatz für Fachberatung**, sondern als Brücke zwischen den Sitzungen.
|
||||
- Optionaler Selbstbindungs-Modus für macOS (RebReakBinder), den Betroffene gemeinsam mit einer Vertrauensperson aktivieren.
|
||||
- Optionaler Selbstbindungs-Modus für macOS (Rebreak Magic), den Betroffene gemeinsam mit einer Vertrauensperson aktivieren.
|
||||
|
||||
Die App ist in geschlossener Beta und wird derzeit von einer kleinen Gruppe Betroffener im Alltag getestet. Bevor ich breiter ausrolle, möchte ich Rebreak frühzeitig mit erfahrenen Fachstellen abstimmen — gerade in Niedersachsen, wo das Lukas-Werk als LSG-Träger eine besondere Rolle einnimmt.
|
||||
|
||||
@ -65,7 +65,7 @@ Ich schreibe Ihnen offen: Ich bin selbst betroffen. Rebreak ist aus dem konkrete
|
||||
|
||||
- **„Trotz OASIS-Sperre spielen können"** → Rebreak blockiert geräteweit ~330.000 bekannte Glücksspiel-Domains, auch nicht-lizenzierte Offshore-Anbieter, die OASIS strukturell nicht erreicht.
|
||||
- **„Werbung trotz Sperre"** → Rebreak hat einen Echtzeit-Mail-Schutz (IMAP-IDLE), der Casino-Werbemails löscht, bevor die Push-Benachrichtigung am Endgerät anschlägt — meines Wissens das einzige deutschsprachige Tool mit dieser Funktion.
|
||||
- **„Über das 1.000-€-Limit hinaus spielen können"** → adressiert Rebreak indirekt über Geräte-Schutz + den optionalen Selbstbindungs-Modus „RebReakBinder" auf macOS, der die App nur mit einer Vertrauensperson lösbar macht.
|
||||
- **„Über das 1.000-€-Limit hinaus spielen können"** → adressiert Rebreak indirekt über Geräte-Schutz + den optionalen Selbstbindungs-Modus „Rebreak Magic" auf macOS, der die App nur mit einer Vertrauensperson lösbar macht.
|
||||
|
||||
Die App ist aktuell in geschlossener Beta auf iOS, Android und macOS und wird von einer kleinen Gruppe Betroffener im Alltag getestet. Lyra, der integrierte KI-Begleiter, **versteht sich ausdrücklich nicht als Ersatz für Fachberatung**, sondern als 24/7-Brücke zwischen den Beratungsterminen — und verweist in akuten Lagen aktiv an die etablierten Strukturen (BZgA-Hotline, Telefonseelsorge, lokale Fachstellen).
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user