chahinebrini d65ba84eb1 feat(binder): MDMClient, EnrollView improvements + supervise flow_backup
- MDMClient: error handling verbessert
- SuperviseRunner: robustere EOF-nach-Success Erkennung
- EnrollView: Enrollment-Status-Polling, Retry-Logik
- SuperviseView: UX-Verbesserungen
- ConfigureView: minor cleanup
- flow_backup.go: backup flow für supervise-magic

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-01 04:30:28 +02:00

160 lines
5.7 KiB
Swift

import SwiftUI
struct SuperviseView: View {
@Environment(WizardModel.self) private var model
@State private var task: Task<Void, Never>?
var body: some View {
VStack(alignment: .leading, spacing: 16) {
header
Text("Wir schreiben jetzt nur die Supervision-Metadaten auf dein iPhone und starten es neu. Apps, Daten und Logins bleiben erhalten. Das dauert ~60 Sekunden. **Trenne das USB-Kabel nicht.**")
.foregroundStyle(.secondary)
statusBox
if model.showAdvancedLogs {
logViewer
}
Button(model.showAdvancedLogs ? "Details ausblenden" : "Details anzeigen") {
model.showAdvancedLogs.toggle()
}
.buttonStyle(.borderless)
.foregroundStyle(.secondary)
Spacer()
navigationBar
}
.padding(40)
.onAppear { startIfNeeded() }
.onDisappear { task?.cancel() }
}
private var header: some View {
HStack {
Image(systemName: "lock.shield")
.font(.system(size: 30))
.foregroundStyle(.tint)
Text("Supervisieren")
.font(.title).bold()
}
}
@ViewBuilder
private var statusBox: some View {
if model.supervisionRunning {
HStack(spacing: 8) {
ProgressView().controlSize(.small)
Text("supervise-magic läuft …")
}
.padding(10)
.background(Color.blue.opacity(0.08))
.cornerRadius(6)
} else if let err = model.supervisionError {
HStack(alignment: .top, spacing: 8) {
Image(systemName: "xmark.octagon")
.foregroundStyle(.red)
Text(err).font(.callout)
}
.padding(10)
.background(Color.red.opacity(0.08))
.cornerRadius(6)
} else if !model.supervisionLog.isEmpty {
HStack {
Image(systemName: "checkmark.circle.fill").foregroundStyle(.green)
Text("Supervisieren abgeschlossen.")
}
.padding(10)
.background(Color.green.opacity(0.08))
.cornerRadius(6)
}
}
private var logViewer: some View {
ScrollViewReader { proxy in
ScrollView {
LazyVStack(alignment: .leading, spacing: 2) {
ForEach(Array(model.supervisionLog.enumerated()), id: \.offset) { idx, line in
Text(line)
.font(.system(.caption, design: .monospaced))
.foregroundStyle(line.contains("[stderr]") ? .orange : .secondary)
.id(idx)
}
}
.padding(8)
.frame(maxWidth: .infinity, alignment: .leading)
}
.background(Color.black.opacity(0.04))
.cornerRadius(6)
.frame(maxHeight: 220)
.onChange(of: model.supervisionLog.count) { _, newCount in
if newCount > 0 { proxy.scrollTo(newCount - 1, anchor: .bottom) }
}
}
}
private var navigationBar: some View {
HStack {
Button("Zurück") { model.goTo(.preflight) }
.buttonStyle(.bordered)
.disabled(model.supervisionRunning)
Spacer()
if model.supervisionError != nil {
Button("Neu versuchen") { startSupervise() }
.buttonStyle(.bordered)
}
Button("Weiter →") { model.advance() }
.buttonStyle(.borderedProminent)
.disabled(model.supervisionRunning || model.supervisionLog.isEmpty || model.supervisionError != nil)
}
}
private func startIfNeeded() {
// Skip wenn iPad bereits von ReBreak supervised direkt zum nächsten Step.
if model.device?.isSupervised == true,
(model.device?.supervisorOrgName ?? "") == "ReBreak" {
model.supervisionLog = ["✓ Bereits von ReBreak supervised — überspringe."]
model.supervisionError = nil
model.supervisionRunning = false
return
}
if model.supervisionLog.isEmpty && !model.supervisionRunning && model.supervisionError == nil {
startSupervise()
}
}
private func startSupervise() {
model.supervisionLog = []
model.supervisionError = nil
model.supervisionRunning = true
task?.cancel()
task = Task { @MainActor in
do {
// force=false: wenn Device schon supervised ist (z.B. nach
// partial-success + Retry), exitet CLI mit 0 + Hinweis statt
// den ganzen Backup-Restore-Flow nochmal durchzulaufen.
_ = try await SuperviseRunner.supervise(
organizationName: "ReBreak",
force: false,
verbose: model.showAdvancedLogs
) { line in
model.supervisionLog.append(line)
}
model.supervisionRunning = false
model.device?.isSupervised = true
model.device?.supervisorOrgName = "ReBreak"
// Nach re-supervise ist der MDM-Channel oft weg; Enroll-Step soll
// deshalb nicht fälschlich übersprungen werden.
model.device?.isEnrolled = false
model.device?.enrollmentStatus = nil
} catch {
model.supervisionError = error.localizedDescription
model.supervisionRunning = false
}
}
}
}