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)
|
||||
*.npk
|
||||
*.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)
|
||||
}
|
||||
|
||||
/// 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
|
||||
/// `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
|
||||
|
||||
@@ -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
|
||||
@StateObject private var viewModel = BackupViewModel()
|
||||
@State private var showFolderPicker = false
|
||||
@State private var showFactoryResetConfirmation = false
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
@@ -42,6 +43,38 @@ struct BackupListView: View {
|
||||
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)
|
||||
@@ -55,6 +88,25 @@ struct BackupListView: View {
|
||||
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 {
|
||||
@@ -85,6 +137,18 @@ struct BackupListView: View {
|
||||
} 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() }
|
||||
}
|
||||
|
||||
@@ -8,7 +8,12 @@ final class BackupViewModel: ObservableObject {
|
||||
@Published private(set) var backupDirectoryPath: String = BackupService.customDirectoryURL?.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 factoryResetService = FactoryResetService()
|
||||
|
||||
func load() {
|
||||
backups = backupService.listBackups()
|
||||
@@ -39,4 +44,23 @@ final class BackupViewModel: ObservableObject {
|
||||
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