Files
RouterOS/RouterOSAssistant/Features/Wizard/Steps/Connect/ConnectView.swift
T
KayandClaude Sonnet 5 c77cd72cbd Verbinden-Tab: Trennen-Button ergänzt
ConnectionService.disconnect() existierte schon (bisher nur intern
nach Werksreset genutzt), aber ohne UI-Zugang im Verbinden-Tab selbst
-- Nutzer musste die App neu starten, um eine Verbindung sauber zu
beenden. Jetzt als Button direkt neben dem "Verbunden"-Status.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HReLXMbmPvtQ23p1iWiJNW
2026-09-13 00:44:18 +02:00

204 lines
8.1 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(
"Unbekannter SSH-Schlüssel",
isPresented: sshHostKeyAlertBinding,
presenting: sshHostKeyFingerprint
) { fingerprint in
Button("Vertrauen und verbinden") {
viewModel.trustSSHHostKeyAndRetry(fingerprint: fingerprint)
}
Button("Abbrechen", role: .cancel) {}
} message: { fingerprint in
Text("Der Router hat sich mit einem unbekannten SSH-Schlüssel gemeldet.\nFingerabdruck: \(fingerprint)\n\nNur bestätigen, wenn dies dein eigenes Gerät im lokalen Netzwerk ist. Falls du diesen Router schon einmal verbunden hattest und sich der Fingerabdruck geändert hat, könnte das auf ein manipuliertes Netzwerk hindeuten — im Zweifel nicht bestätigen.")
}
.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 }
)
}
private var sshHostKeyFingerprint: String? {
if case .needsSSHHostKeyConfirmation(let fingerprint) = connectionService.state {
return fingerprint
}
return nil
}
private var sshHostKeyAlertBinding: Binding<Bool> {
Binding(
get: { sshHostKeyFingerprint != nil },
set: { _ in }
)
}
@ViewBuilder
private var statusSection: some View {
switch connectionService.state {
case .idle, .needsCertificateConfirmation, .needsSSHHostKeyConfirmation:
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("Trennen", role: .destructive) {
Task { await connectionService.disconnect() }
}
.help("Beendet die Verbindung zum Router. Zugangsdaten bleiben erhalten (falls gemerkt).")
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())
}