Files
RouterOS/RouterOSAssistant/Core/Models/PortConflict.swift
T
KayandClaude Sonnet 5 b503bac82c Experte-Tab: Port-Konflikt-Pruefung wie im Einrichten-Assistenten
Per explizitem Nutzerwunsch die "Port freimachen?"-Abfrage samt allen
Warnungen aus dem LAN-Schritt des Wizards auch auf den Experte-Tab
angewendet, ausgeloest durch echten Live-Fall (ether4 wurde manuell
ueber Expert als eigenes Netz angelegt, blieb dabei unbemerkt Bridge-
Mitglied - zwei DHCP-Server im selben Broadcast-Domain).

PortConflictWarningView aus LanStepView.swift in Features/Shared/
extrahiert (jetzt von Wizard UND Experte-Tab genutzt), neuer
immediateApply-Parameter fuer die kontextabhaengige Abschluss-Meldung
(Wizard: erst bei "Jetzt anwenden"; Experte: sofort bei "Anlegen"/
"Speichern"). Neue PortConflict.resolutionCommandsIncludingBridgeDetach()
- der Experte-Tab hat anders als der Wizard keinen separaten,
automatischen Bridge-Detach-Schritt, muss die Bridge-Entfernung also
selbst mit auflisten.

ExpertViewModel bekommt dieselbe Race-sichere Generation-Zaehler-Logik
wie SetupViewModel (bugs.md #1), angewendet auf das .interfacePick-Feld
des jeweils offenen Schemas. Speichern-Button gesperrt bis Konflikt
bestaetigt oder Port gewechselt.

6 neue Regressionstests. Build + alle 111 Unit-Tests gruen.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-17 22:21:36 +02:00

84 lines
4.3 KiB
Swift

import Foundation
/// Pre-existing configuration on a physical port that the Setup wizard's LAN step would silently
/// override if the user picks that port for a new LAN/DHCP network — surfaced instead so the user
/// can either choose a different, actually-free port or explicitly (twice-confirmed) opt in to
/// clearing it. `DhcpServerCommandBuilder` already unconditionally detaches a port from any bridge
/// as a safety net (see its own doc comment) — this check runs *before* that, to make the same
/// fact visible up front instead of only implicitly, and additionally covers cases that builder
/// doesn't touch at all (an existing IP address, or the port already being used as a WAN dial-up).
struct PortConflict: Equatable {
enum Reason: Equatable {
case bridgeMember(bridgeName: String)
case hasAddresses([String])
case dhcpClient
case pppoeClient
var description: String {
switch self {
case .bridgeMember(let bridgeName):
return "Ist Mitglied der Bridge \"\(bridgeName)\""
case .hasAddresses(let addresses):
return "Trägt bereits die IP-Adresse\(addresses.count == 1 ? "" : "n") \(addresses.joined(separator: ", "))"
case .dhcpClient:
return "Ist als WAN-DHCP-Client konfiguriert (bezieht selbst eine Adresse aus dem Internet)"
case .pppoeClient:
return "Wird von einer PPPoE-Einwahl verwendet"
}
}
}
let interfaceName: String
let reasons: [Reason]
/// Commands that clear each found conflict so the port is actually free before
/// `DhcpServerCommandBuilder`'s own commands run. Bridge membership is deliberately excluded
/// here — `DhcpServerCommandBuilder` already removes it unconditionally regardless of whether
/// this check ran or was acknowledged, so repeating it would just be a harmless duplicate
/// `.remove` at best; excluding it keeps this list to exactly what wouldn't otherwise happen.
func resolutionCommands() -> [RouterOSCommand] {
reasons.flatMap { reason -> [RouterOSCommand] in
switch reason {
case .bridgeMember:
return []
case .hasAddresses:
return [RouterOSCommand.remove(
menuPath: "/ip address", restPath: "ip/address",
matchField: "interface", matchValue: interfaceName,
summary: "Bestehende IP-Adresse(n) auf \(interfaceName) entfernen"
)]
case .dhcpClient:
return [RouterOSCommand.remove(
menuPath: "/ip dhcp-client", restPath: "ip/dhcp-client",
matchField: "interface", matchValue: interfaceName,
summary: "WAN-DHCP-Client auf \(interfaceName) entfernen"
)]
case .pppoeClient:
return [RouterOSCommand.remove(
menuPath: "/interface pppoe-client", restPath: "interface/pppoe-client",
matchField: "interface", matchValue: interfaceName,
summary: "PPPoE-Einwahl auf \(interfaceName) entfernen"
)]
}
}
}
/// `resolutionCommands()` alone for the Setup-Wizard context, where `DhcpServerCommandBuilder`
/// already unconditionally detaches the port from any bridge as its own separate safety net —
/// see that method's doc comment. Contexts without that separate detach (the Experte tab's
/// generic "Port freimachen?" flow, applied to whatever menu the user is actually editing, not
/// specifically the LAN/DHCP command set) need the bridge-membership removal included here
/// instead, or acknowledging a bridge-member conflict there would silently do nothing for
/// that specific reason.
func resolutionCommandsIncludingBridgeDetach() -> [RouterOSCommand] {
let bridgeDetach: [RouterOSCommand] = reasons.contains(where: { if case .bridgeMember = $0 { return true } else { return false } })
? [RouterOSCommand.remove(
menuPath: "/interface bridge port", restPath: "interface/bridge/port",
matchField: "interface", matchValue: interfaceName,
summary: "\(interfaceName) aus Bridge lösen"
)]
: []
return bridgeDetach + resolutionCommands()
}
}