Files
RouterOS/RouterOSAssistant/Features/Backup/BackupListView.swift
T
KayandClaude Sonnet 5 a93a61c84f Werkseinstellungen wiederherstellen (Gefahrenzone im Sicherungen-Tab)
RouterOS bringt eine eigene Standardkonfiguration mit, wiederherstellbar
über /system reset-configuration no-defaults=no -- restauriert die vom
Hersteller ausgelieferte Konfiguration (nicht eine leere), RouterOS legt
dabei selbst zusätzlich ein Backup an (skip-backup=no, Standard).

FactoryResetService läuft wie BackupService immer über eine eigene
SSH-Verbindung (kein verifiziertes REST-Äquivalent, passt nicht ins
add/set-Modell von RouterOSCommand). Vor dem Zurücksetzen erstellt die
App zusätzlich selbst ein Backup (best-effort). Verbindung zum Router
bricht durch den Reboot erwartungsgemäß ab -- wird nicht als Fehler
behandelt, ConnectionService trennt sich danach selbst.

UI: rot markierte "Gefahrenzone" im Sicherungen-Tab, destruktiver
Bestätigungsdialog vor dem Ausführen, klarer Hinweistext was verloren
geht.

Außerdem .gitignore um /Backups/ und *.rsc ergänzt -- beim Testen des
wählbaren Backup-Ordners landete ein echter Router-Export im
Projektordner, gehört nicht ins Repo.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HReLXMbmPvtQ23p1iWiJNW
2026-09-12 20:52:23 +02:00

160 lines
7.1 KiB
Swift

import SwiftUI
import UniformTypeIdentifiers
struct BackupListView: View {
@ObservedObject var connectionService: ConnectionService
@StateObject private var viewModel = BackupViewModel()
@State private var showFolderPicker = false
@State private var showFactoryResetConfirmation = false
var body: some View {
NavigationStack {
Group {
if viewModel.backups.isEmpty {
ContentUnavailableView(
"Keine Sicherungen",
systemImage: "clock.arrow.circlepath",
description: Text("Erstelle eine Sicherung, bevor du Änderungen am Router vornimmst.")
)
} else {
List(viewModel.backups) { backup in
VStack(alignment: .leading) {
Text(backup.host).bold()
Text(backup.createdAt.formatted(date: .abbreviated, time: .standard))
.font(.caption)
.foregroundStyle(.secondary)
}
}
}
}
.safeAreaInset(edge: .bottom) {
VStack(alignment: .leading, spacing: 4) {
Text("Speicherort")
.font(.caption)
.foregroundStyle(.secondary)
Text(viewModel.backupDirectoryPath)
.font(.caption)
.lineLimit(1)
.truncationMode(.middle)
.help(viewModel.backupDirectoryPath)
HStack {
Button("Ordner wählen…") { showFolderPicker = true }
.help("Wähle einen eigenen Ordner für neue Sicherungen, z.B. auf einer externen Festplatte oder in iCloud Drive.")
Button("Standard verwenden") { viewModel.resetBackupDirectoryToDefault() }
.help("Setzt den Speicherort zurück auf den App-eigenen Standardordner.")
}
Divider()
.padding(.vertical, 4)
Text("Gefahrenzone")
.font(.caption.bold())
.foregroundStyle(.red)
Text("Setzt den Router komplett auf die vom Hersteller mitgelieferte Standardkonfiguration zurück — alle bisherigen Änderungen (Internet, Heimnetz, VLANs, WLAN, Firewall) gehen verloren. Der Router startet danach neu.")
.font(.caption)
.foregroundStyle(.secondary)
Button(role: .destructive) {
showFactoryResetConfirmation = true
} label: {
if viewModel.isResettingToFactoryDefaults {
HStack {
ProgressView()
Text("Setze zurück…")
}
} else {
Label("Werkseinstellungen wiederherstellen", systemImage: "exclamationmark.triangle")
}
}
.disabled(connectionService.credentials == nil || viewModel.isResettingToFactoryDefaults)
.help("Stellt die vom Hersteller vorinstallierte Standardkonfiguration des Routers wieder her. Nur für den Notfall, falls etwas schiefgelaufen ist.")
if viewModel.didSendFactoryReset {
Label(
"Befehl gesendet — der Router startet jetzt neu (kann 1-2 Minuten dauern). Verbinde dich danach im Tab \"Verbinden\" erneut.",
systemImage: "arrow.clockwise"
)
.font(.caption)
}
}
.padding(8)
.frame(maxWidth: .infinity, alignment: .leading)
.background(.bar)
}
.fileImporter(
isPresented: $showFolderPicker,
allowedContentTypes: [.folder]
) { result in
if case .success(let url) = result {
viewModel.setBackupDirectory(url)
}
}
.confirmationDialog(
"Werkseinstellungen wirklich wiederherstellen?",
isPresented: $showFactoryResetConfirmation,
titleVisibility: .visible
) {
Button("Zurücksetzen", role: .destructive) {
if let credentials = connectionService.credentials {
viewModel.resetToFactoryDefaults(for: credentials)
}
}
Button("Abbrechen", role: .cancel) {}
} message: {
Text("Dies löscht ALLE bisherigen Einstellungen (Internet, Heimnetz, VLANs, WLAN, Firewall) und setzt den Router auf die Werks-Standardkonfiguration zurück. Der Router startet neu, diese App verliert danach die Verbindung. Vorher wird zusätzlich automatisch eine Sicherung erstellt.")
}
.onChange(of: viewModel.didSendFactoryReset) { _, didSend in
if didSend {
Task { await connectionService.disconnect() }
}
}
.navigationTitle("Sicherungen")
.toolbar {
ToolbarItem {
Button {
if let credentials = connectionService.credentials {
viewModel.createBackup(for: credentials)
}
} label: {
if viewModel.isCreatingBackup {
ProgressView()
} else {
Label("Jetzt sichern", systemImage: "square.and.arrow.down")
}
}
.disabled(connectionService.credentials == nil || viewModel.isCreatingBackup)
.help(connectionService.credentials == nil ? "Zuerst im Tab 'Verbinden' mit dem Router verbinden." : "Sicherung jetzt erstellen")
}
}
.alert(
"Sicherung fehlgeschlagen",
isPresented: Binding(
get: { viewModel.errorMessage != nil },
set: { _ in viewModel.errorMessage = nil }
),
presenting: viewModel.errorMessage
) { _ in
Button("OK") {}
} message: { message in
Text(message)
}
.alert(
"Zurücksetzen fehlgeschlagen",
isPresented: Binding(
get: { viewModel.resetError != nil },
set: { _ in viewModel.resetError = nil }
),
presenting: viewModel.resetError
) { _ in
Button("OK") {}
} message: { message in
Text(message)
}
}
.onAppear { viewModel.load() }
}
}
#Preview {
BackupListView(connectionService: ConnectionService())
}