forked from kay/RouterOS
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
This commit is contained in:
@@ -12,3 +12,8 @@ xcuserdata/
|
|||||||
# Firmware images (large binaries, not app source)
|
# Firmware images (large binaries, not app source)
|
||||||
*.npk
|
*.npk
|
||||||
*.cpgz
|
*.cpgz
|
||||||
|
|
||||||
|
# Router config backups (real device data, not app source) — created here when
|
||||||
|
# testing the app's "choose backup folder" feature with this directory selected.
|
||||||
|
/Backups/
|
||||||
|
*.rsc
|
||||||
|
|||||||
@@ -70,6 +70,12 @@ final class SSHTransport: RouterOSTransport {
|
|||||||
_ = try await run(command.cliLine)
|
_ = try await run(command.cliLine)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Restores RouterOS' own vendor-default configuration and reboots the device. See
|
||||||
|
/// FactoryResetService for why this bypasses the RouterOSCommand add/set model entirely.
|
||||||
|
func resetToFactoryDefaults() async throws {
|
||||||
|
_ = try await run("/system reset-configuration no-defaults=no skip-backup=no")
|
||||||
|
}
|
||||||
|
|
||||||
/// Runs a command via `executeCommandStream` (not the simpler `executeCommand`), because
|
/// Runs a command via `executeCommandStream` (not the simpler `executeCommand`), because
|
||||||
/// `executeCommand` discards whatever output it already collected the moment the command
|
/// `executeCommand` discards whatever output it already collected the moment the command
|
||||||
/// exits non-zero — exactly the RouterOS error text we need. Collecting the stream ourselves
|
/// exits non-zero — exactly the RouterOS error text we need. Collecting the stream ourselves
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
/// Restores the router's own vendor-default configuration via RouterOS' built-in
|
||||||
|
/// `/system reset-configuration` — not a restore of one of our own backup files, the actual
|
||||||
|
/// factory default the device shipped with. Always runs over a dedicated SSH connection, same
|
||||||
|
/// reasoning as BackupService: this is a one-shot system action, not a menu item to add/set,
|
||||||
|
/// so it doesn't fit the REST-mirrors-menu-items model RouterOSCommand assumes, and there's no
|
||||||
|
/// verified REST equivalent to fall back on.
|
||||||
|
final class FactoryResetService {
|
||||||
|
/// `no-defaults=no` restores the vendor default config (not a blank slate); RouterOS'
|
||||||
|
/// own `skip-backup=no` default already saves an automatic backup on the router itself
|
||||||
|
/// right before resetting, on top of whatever backup this app already made.
|
||||||
|
func resetToVendorDefaults(for credentials: RouterOSCredentials) async throws {
|
||||||
|
let transport = SSHTransport(credentials: credentials)
|
||||||
|
try await transport.connect()
|
||||||
|
// The router reboots immediately after this command; the connection dying mid-command
|
||||||
|
// instead of returning a clean response is the expected outcome here, not a failure.
|
||||||
|
try? await transport.resetToFactoryDefaults()
|
||||||
|
await transport.disconnect()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ struct BackupListView: View {
|
|||||||
@ObservedObject var connectionService: ConnectionService
|
@ObservedObject var connectionService: ConnectionService
|
||||||
@StateObject private var viewModel = BackupViewModel()
|
@StateObject private var viewModel = BackupViewModel()
|
||||||
@State private var showFolderPicker = false
|
@State private var showFolderPicker = false
|
||||||
|
@State private var showFactoryResetConfirmation = false
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationStack {
|
NavigationStack {
|
||||||
@@ -42,6 +43,38 @@ struct BackupListView: View {
|
|||||||
Button("Standard verwenden") { viewModel.resetBackupDirectoryToDefault() }
|
Button("Standard verwenden") { viewModel.resetBackupDirectoryToDefault() }
|
||||||
.help("Setzt den Speicherort zurück auf den App-eigenen Standardordner.")
|
.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)
|
.padding(8)
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
@@ -55,6 +88,25 @@ struct BackupListView: View {
|
|||||||
viewModel.setBackupDirectory(url)
|
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")
|
.navigationTitle("Sicherungen")
|
||||||
.toolbar {
|
.toolbar {
|
||||||
ToolbarItem {
|
ToolbarItem {
|
||||||
@@ -85,6 +137,18 @@ struct BackupListView: View {
|
|||||||
} message: { message in
|
} message: { message in
|
||||||
Text(message)
|
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() }
|
.onAppear { viewModel.load() }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,12 @@ final class BackupViewModel: ObservableObject {
|
|||||||
@Published private(set) var backupDirectoryPath: String = BackupService.customDirectoryURL?.path
|
@Published private(set) var backupDirectoryPath: String = BackupService.customDirectoryURL?.path
|
||||||
?? BackupService.defaultDirectoryURL.path
|
?? BackupService.defaultDirectoryURL.path
|
||||||
|
|
||||||
|
@Published private(set) var isResettingToFactoryDefaults = false
|
||||||
|
@Published var resetError: String?
|
||||||
|
@Published private(set) var didSendFactoryReset = false
|
||||||
|
|
||||||
private let backupService = BackupService()
|
private let backupService = BackupService()
|
||||||
|
private let factoryResetService = FactoryResetService()
|
||||||
|
|
||||||
func load() {
|
func load() {
|
||||||
backups = backupService.listBackups()
|
backups = backupService.listBackups()
|
||||||
@@ -39,4 +44,23 @@ final class BackupViewModel: ObservableObject {
|
|||||||
isCreatingBackup = false
|
isCreatingBackup = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Backs up first (best-effort — a failed backup shouldn't block the reset itself, since
|
||||||
|
/// RouterOS makes its own backup as part of the reset anyway), then restores the router's
|
||||||
|
/// vendor-default configuration and reboots it.
|
||||||
|
func resetToFactoryDefaults(for credentials: RouterOSCredentials) {
|
||||||
|
isResettingToFactoryDefaults = true
|
||||||
|
resetError = nil
|
||||||
|
didSendFactoryReset = false
|
||||||
|
Task {
|
||||||
|
do {
|
||||||
|
_ = try? await backupService.createBackup(for: credentials)
|
||||||
|
try await factoryResetService.resetToVendorDefaults(for: credentials)
|
||||||
|
didSendFactoryReset = true
|
||||||
|
} catch {
|
||||||
|
resetError = error.localizedDescription
|
||||||
|
}
|
||||||
|
isResettingToFactoryDefaults = false
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user