fix(panel-go): random ports roll once — sticky across edits (round-2 #7)
NormalizeNode re-selected ports whenever the request merely CONTAINED port/serverId keys; the browser form always submits them (port=null in random mode), so every save re-rolled both entry and exit ports. Now an assigned port only moves on an explicit addressing change: mode switch to random, a different fixed port value, or a server move where the port is taken (fixed mode surfaces the 409). random->fixed pins the current port. Regression: portlock_test.go (UI-shape payload saved twice, rename-only, mode switches, fixed change/conflict, server move free/taken). Golden re-captured: diff confined to nodePatchUiStyle/nodePatchJobs/ nodesAfterPatch/backup port leaves returning to the seed ports.
This commit is contained in:
+74
-4
@@ -320,8 +320,21 @@ func NormalizeNode(body map[string]any, current *Node, data *Data) (*Node, error
|
||||
_, portModeInBody := body["portMode"]
|
||||
_, portInBody := body["port"]
|
||||
portModeChanged := portModeInBody && portMode != currentPortMode
|
||||
_, serverIDInBody := body["serverId"]
|
||||
if portModeChanged || portInBody || serverIDInBody || currentPort == 0 {
|
||||
// An already-assigned port is sticky. The browser form always submits
|
||||
// portMode/port/serverId keys (port is null in random mode), so key
|
||||
// presence alone must never trigger a re-selection — a random port is
|
||||
// rolled exactly once, at creation. On edit the port only moves for an
|
||||
// explicit addressing change: switching the mode to random, supplying a
|
||||
// different fixed port, or moving to a server where the port is taken.
|
||||
portValueChanged := false
|
||||
if portInBody && !jsFalsy(body["port"]) {
|
||||
if f, valid := anyToJSNumber(body["port"]); valid && int(f) != currentPort {
|
||||
portValueChanged = true
|
||||
}
|
||||
}
|
||||
serverChanged := current != nil && serverID != current.ServerID
|
||||
switch {
|
||||
case current == nil || currentPort == 0:
|
||||
var requested any
|
||||
if portInBody {
|
||||
requested = body["port"]
|
||||
@@ -333,6 +346,32 @@ func NormalizeNode(body map[string]any, current *Node, data *Data) (*Node, error
|
||||
return nil, err
|
||||
}
|
||||
next.Port = port
|
||||
case portModeChanged && portMode == "random":
|
||||
// Explicit switch into random mode: roll a fresh port.
|
||||
port, err := SelectPort(data, server, portMode, nil, currentID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
next.Port = port
|
||||
case portValueChanged && portMode == "fixed":
|
||||
port, err := SelectPort(data, server, portMode, body["port"], currentID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
next.Port = port
|
||||
case serverChanged && NodePortInUse(data, serverID, currentPort, currentID):
|
||||
// The port is taken on the target server: fixed mode surfaces the
|
||||
// conflict, random mode rolls a replacement. (When it is free the
|
||||
// switch falls through and the port is kept verbatim.)
|
||||
var requested any
|
||||
if portMode == "fixed" {
|
||||
requested = json.Number(fmt.Sprint(currentPort))
|
||||
}
|
||||
port, err := SelectPort(data, server, portMode, requested, currentID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
next.Port = port
|
||||
}
|
||||
if !direct {
|
||||
exitPortMode := strVal(next.ExitPortMode)
|
||||
@@ -364,8 +403,17 @@ func NormalizeNode(body map[string]any, current *Node, data *Data) (*Node, error
|
||||
_, exitPortModeInBody := body["exitPortMode"]
|
||||
_, exitPortInBody := body["exitPort"]
|
||||
exitPortModeChanged := exitPortModeInBody && exitPortMode != currentExitPortMode
|
||||
_, exitServerIDInBody := body["exitServerId"]
|
||||
if exitPortModeChanged || exitPortInBody || exitServerIDInBody || currentExitPort == nil {
|
||||
// Same stickiness rule as the entry port: an assigned exit port is
|
||||
// only re-selected on creation or an explicit addressing change.
|
||||
exitPortValueChanged := false
|
||||
if exitPortInBody && !jsFalsy(body["exitPort"]) && currentExitPort != nil {
|
||||
if f, valid := anyToJSNumber(body["exitPort"]); valid && int(f) != *currentExitPort {
|
||||
exitPortValueChanged = true
|
||||
}
|
||||
}
|
||||
exitServerChanged := current != nil && strVal(exitServerID) != strVal(current.ExitServerID)
|
||||
switch {
|
||||
case current == nil || currentExitPort == nil:
|
||||
var requested any
|
||||
if exitPortInBody {
|
||||
requested = body["exitPort"]
|
||||
@@ -377,6 +425,28 @@ func NormalizeNode(body map[string]any, current *Node, data *Data) (*Node, error
|
||||
return nil, err
|
||||
}
|
||||
next.ExitPort = &port
|
||||
case exitPortModeChanged && exitPortMode == "random":
|
||||
port, err := SelectPort(data, exitServer, exitPortMode, nil, currentID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
next.ExitPort = &port
|
||||
case exitPortValueChanged && exitPortMode == "fixed":
|
||||
port, err := SelectPort(data, exitServer, exitPortMode, body["exitPort"], currentID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
next.ExitPort = &port
|
||||
case exitServerChanged && NodePortInUse(data, strVal(exitServerID), *currentExitPort, currentID):
|
||||
var requested any
|
||||
if exitPortMode == "fixed" {
|
||||
requested = json.Number(fmt.Sprint(*currentExitPort))
|
||||
}
|
||||
port, err := SelectPort(data, exitServer, exitPortMode, requested, currentID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
next.ExitPort = &port
|
||||
}
|
||||
} else {
|
||||
next.ExitPortMode = nil
|
||||
|
||||
Reference in New Issue
Block a user