chahinebrini d54bd06727 feat(magic): post-login Device-Hub als zentraler Einstieg + Limit 3->5
Redesign:
- Nach Login landet User direkt im neuen DeviceHubView statt
  Auto-Mac-Registrierung. Hub zeigt: User-Email, X/5-Slot-Counter,
  Liste aller registrierten Geraete + 'Geraet hinzufuegen' mit
  iPhone/iPad vs Mac Wahl.
- Mac wird NUR registriert wenn User aktiv 'Mac' im Hub waehlt
  (frueher: auto on app-start, frass Slot).
- iOS-Pfad: Hub -> Welcome/Preflight/Supervise/Enroll/Configure
  -> Done -> 'Zurueck zur Geraete-Uebersicht'.
- Mac-Pfad: Hub -> MacRegistrationView (Register+DNS-Install)
  -> 'Fertig -> Hub'.
- Wizard-Header hat jetzt Grid-Icon 'Zur Geraete-Uebersicht' als
  Escape-Hatch jederzeit.
- Per-Device-Loeschung im Hub: Trash-Icon -> Confirm-Dialog
  ('Auf X muss Freigabe bestaetigt werden, 24h Cooldown') ->
  request-release-Endpoint (existing infra).
- Device-Limit 3 -> 5 in backend (Staging-Testing + Legend-Wert
  fuer spaeter).
- StepIndicator/Step-Counter: macRegistration zaehlt nicht im
  iOS-Flow.
2026-06-03 10:39:51 +02:00

238 lines
7.5 KiB
Swift

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 {
var step: WizardStep = .macRegistration
var device: DeviceState?
var supervisionLog: [String] = []
var supervisionRunning: Bool = false
var supervisionError: String?
var enrollmentLog: [String] = []
var enrollmentRunning: Bool = false
var enrollmentError: String?
var configureLog: [String] = []
var configureRunning: Bool = false
var configureError: String?
var showAdvancedLogs: Bool = false
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
// Auth + Magic State
var authSession: AuthSession?
var showingLogin: Bool = false
var showingHub: Bool = false
var showingManageBindings: Bool = false
var magicRegistration: MagicRegistration?
var registrationError: String?
init() {
// Load existing session from keychain
authSession = AuthService.shared.currentSession()
showingLogin = (authSession == nil)
// Nach Login direkt zum Hub statt Mac-Auto-Registrierung
showingHub = (authSession != nil)
}
func advance() {
if let next = WizardStep(rawValue: step.rawValue + 1) {
step = next
}
}
func goTo(_ s: WizardStep) {
step = s
}
// MARK: - Mac Registration
/// Registriert den aktuellen Mac im Backend.
/// Wirft MagicError.limitReached falls Device-Limit erreicht.
func registerMac() async throws {
registrationError = nil
do {
let macInfo = try MacDeviceDetector.detect()
let registration = try await MagicAPIClient.shared.register(
deviceId: macInfo.deviceId,
hostname: macInfo.hostname,
model: macInfo.model,
osVersion: macInfo.osVersion
)
magicRegistration = registration
} catch let error as MagicError {
// Bei limit_reached öffne ManageBindingsView
if case .limitReached(_) = error {
showingManageBindings = true
}
registrationError = error.localizedDescription
throw error
} catch {
registrationError = error.localizedDescription
throw error
}
}
func handleLogin(session: AuthSession) {
authSession = session
showingLogin = false
showingHub = true
}
func handleLogout() async {
await AuthService.shared.signOut()
authSession = nil
showingLogin = true
showingHub = false
reset()
}
// MARK: - Hub Navigation
/// User wählt 'iOS-Gerät hinzufügen' im Hub.
func startIOSFlow() {
showingHub = false
step = .welcome
}
/// User wählt 'Mac schützen' im Hub.
func startMacFlow() {
showingHub = false
step = .macRegistration
}
/// Zurück zur Geräte-Übersicht.
func returnToHub() {
reset()
showingHub = true
}
func reset() {
step = .macRegistration
device = nil
supervisionLog = []
enrollmentLog = []
configureLog = []
supervisionError = nil
enrollmentError = nil
configureError = nil
showAdvancedLogs = false
cooldownEndsAt = nil
resetStatus = nil
magicRegistration = nil
registrationError = 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
}
}
}
}
}