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:
aaaamoooo
2026-10-20 20:48:58 +00:00
parent d576219a27
commit 80f1370f1b
3 changed files with 248 additions and 22 deletions
+74 -4
View File
@@ -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