"Jetzt sichern"-Button erscheint sobald verbunden, noch bevor man in den Einrichten-Tab wechselt -- Nutzer-Wunsch, um vor Änderungen ohne Tab-Wechsel sichern zu können. Zeigt zusätzlich "Zuletzt gesichert: ..." für den aktuell verbundenen Host, falls schon eine Sicherung existiert (gefiltert auf den aktuellen Host, damit es bei mehreren Routern nicht verwirrt). Nutzt dieselbe BackupViewModel/BackupService-Logik wie der Sicherungen-Tab, eigene Instanz. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HReLXMbmPvtQ23p1iWiJNW
173 lines
6.7 KiB
Swift
173 lines
6.7 KiB
Swift
import SwiftUI
|
|
|
|
struct ConnectView: View {
|
|
@ObservedObject private var connectionService: ConnectionService
|
|
@StateObject private var viewModel: ConnectViewModel
|
|
@StateObject private var backupViewModel = BackupViewModel()
|
|
|
|
init(connectionService: ConnectionService) {
|
|
self.connectionService = connectionService
|
|
_viewModel = StateObject(wrappedValue: ConnectViewModel(connectionService: connectionService))
|
|
}
|
|
|
|
var body: some View {
|
|
NavigationSplitView {
|
|
Form {
|
|
Section("Verbindung") {
|
|
TextField("IP-Adresse oder Hostname", text: $viewModel.host)
|
|
.help("Die Adresse deines Routers im Netzwerk. Werkseinstellung bei Mikrotik ist meist 192.168.88.1.")
|
|
TextField("Benutzername", text: $viewModel.username)
|
|
.help("Der Admin-Benutzername deines Routers. Werkseinstellung ist meist \"admin\".")
|
|
SecureField("Passwort", text: $viewModel.password)
|
|
.help("Das Passwort für diesen Benutzer. Bei unverändertem Werkszustand oft leer.")
|
|
Toggle("Passwort merken", isOn: $viewModel.rememberPassword)
|
|
.help("Speichert das Passwort verschlüsselt in der macOS-Schlüsselbundverwaltung, damit du es nicht jedes Mal neu eingeben musst.")
|
|
}
|
|
|
|
Section {
|
|
Button("Verbinden") {
|
|
viewModel.connect()
|
|
}
|
|
.disabled(viewModel.host.isEmpty || viewModel.username.isEmpty)
|
|
}
|
|
|
|
statusSection
|
|
}
|
|
.formStyle(.grouped)
|
|
.frame(minWidth: 320)
|
|
} detail: {
|
|
deviceDetail
|
|
}
|
|
.onAppear {
|
|
viewModel.onAppear()
|
|
backupViewModel.load()
|
|
}
|
|
.alert(
|
|
"Unbekanntes Zertifikat",
|
|
isPresented: certificateAlertBinding,
|
|
presenting: certificateFingerprint
|
|
) { fingerprint in
|
|
Button("Vertrauen und verbinden") {
|
|
viewModel.trustAndRetry(fingerprint: fingerprint)
|
|
}
|
|
Button("Abbrechen", role: .cancel) {}
|
|
} message: { fingerprint in
|
|
Text("Der Router hat sich mit einem unbekannten Zertifikat gemeldet.\nFingerabdruck: \(fingerprint)\n\nNur bestätigen, wenn dies dein eigenes Gerät im lokalen Netzwerk ist.")
|
|
}
|
|
.alert(
|
|
"Sicherung fehlgeschlagen",
|
|
isPresented: Binding(
|
|
get: { backupViewModel.errorMessage != nil },
|
|
set: { _ in backupViewModel.errorMessage = nil }
|
|
),
|
|
presenting: backupViewModel.errorMessage
|
|
) { _ in
|
|
Button("OK") {}
|
|
} message: { message in
|
|
Text(message)
|
|
}
|
|
}
|
|
|
|
private var certificateFingerprint: String? {
|
|
if case .needsCertificateConfirmation(let fingerprint) = connectionService.state {
|
|
return fingerprint
|
|
}
|
|
return nil
|
|
}
|
|
|
|
private var certificateAlertBinding: Binding<Bool> {
|
|
Binding(
|
|
get: { certificateFingerprint != nil },
|
|
set: { _ in }
|
|
)
|
|
}
|
|
|
|
@ViewBuilder
|
|
private var statusSection: some View {
|
|
switch connectionService.state {
|
|
case .idle, .needsCertificateConfirmation:
|
|
EmptyView()
|
|
case .connecting:
|
|
Section {
|
|
HStack {
|
|
ProgressView()
|
|
Text("Verbinde…")
|
|
}
|
|
}
|
|
case .connected(let kind):
|
|
Section {
|
|
Label("Verbunden (\(kind == .rest ? "REST-API" : "SSH"))", systemImage: "checkmark.circle.fill")
|
|
.foregroundStyle(.green)
|
|
|
|
Button {
|
|
if let credentials = connectionService.credentials {
|
|
backupViewModel.createBackup(for: credentials)
|
|
}
|
|
} label: {
|
|
if backupViewModel.isCreatingBackup {
|
|
HStack {
|
|
ProgressView()
|
|
Text("Sichere…")
|
|
}
|
|
} else {
|
|
Label("Jetzt sichern", systemImage: "square.and.arrow.down")
|
|
}
|
|
}
|
|
.disabled(backupViewModel.isCreatingBackup)
|
|
.help("Sichert die aktuelle Router-Konfiguration — sinnvoll direkt nach dem Verbinden, bevor du im Einrichten-Tab etwas änderst.")
|
|
|
|
if let lastBackup = backupViewModel.backups.first(where: { $0.host == connectionService.credentials?.host }) {
|
|
Label(
|
|
"Zuletzt gesichert: \(lastBackup.createdAt.formatted(date: .abbreviated, time: .standard))",
|
|
systemImage: "checkmark"
|
|
)
|
|
.font(.caption)
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
}
|
|
case .failed(let message):
|
|
Section {
|
|
Label(message, systemImage: "exclamationmark.triangle.fill")
|
|
.foregroundStyle(.red)
|
|
}
|
|
}
|
|
}
|
|
|
|
@ViewBuilder
|
|
private var deviceDetail: some View {
|
|
if let info = connectionService.deviceInfo {
|
|
List {
|
|
Section("Gerät") {
|
|
LabeledContent("Modell", value: info.boardName)
|
|
LabeledContent("RouterOS-Version", value: info.routerOSVersion)
|
|
LabeledContent("Architektur", value: info.architecture)
|
|
LabeledContent("Laufzeit", value: info.uptime)
|
|
}
|
|
Section("Interfaces") {
|
|
ForEach(connectionService.interfaces) { interface in
|
|
HStack {
|
|
Image(systemName: interface.running ? "circle.fill" : "circle")
|
|
.foregroundStyle(interface.running ? .green : .secondary)
|
|
.font(.caption)
|
|
VStack(alignment: .leading) {
|
|
Text(interface.name).bold()
|
|
Text(interface.type).font(.caption).foregroundStyle(.secondary)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
ContentUnavailableView(
|
|
"Nicht verbunden",
|
|
systemImage: "network.slash",
|
|
description: Text("Verbinde dich mit deinem Router, um Geräteinformationen zu sehen.")
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
#Preview {
|
|
ConnectView(connectionService: ConnectionService())
|
|
}
|