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>
84 lines
4.3 KiB
Swift
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()
|
|
}
|
|
}
|