P8(2): inbound node reassignment (move) with backend re-homing

An inbound's 部署到 node can now be changed after creation; the row,
clients, client_traffics and subscription output stay untouched (same
inbound id throughout).

Contract: the update payload's nodeId counts only when the key was
explicitly sent — the controller probes the raw JSON body and sets
Inbound.NodeIDProvided; the wire adapter now always emits nodeId (null =
local) so clearing the picker is an explicit move-to-local, while
callers that omit nodeId keep the stored assignment (legacy behaviour).

UpdateInbound resolves the target before the port-conflict check (so
conflicts are scoped to the target node), rejects moves to a missing
node, re-derives auto tags under the target node's prefix, validates
the tag against the target node's namespace, and marks BOTH nodes
dirty: the source node's next reconcile sweeps the old tag (no silent
ghost — the flag persists until a reconcile succeeds, so an offline
side just defers), the target's pushes the inbound. Agent-bound nodes
converge via GetXrayConfigForAgent on the agent's next config poll.
A local side has no reconcile loop, so it is applied directly
post-commit (Del/Add on the local runtime, restart fallback).

Frontend: the deploy-to Select is no longer disabled in edit mode
(protocol stays locked); candidate rules unchanged (online nodes only).

Tests: panel->panel move with reconcile sweep/push assertions,
auto-tag prefix regeneration, agent-config flip in both directions,
omitted-nodeId legacy contract, same-node no-op, missing-node
rejection, node->local move.
This commit is contained in:
Muse
2026-10-21 13:49:57 +00:00
parent 261a1f6787
commit 7284fa6cf0
7 changed files with 572 additions and 13 deletions
+7
View File
@@ -87,6 +87,13 @@ type Inbound struct {
// the master's externally reachable endpoint instead of the child's
// loopback listen. Not persisted.
FallbackParent *FallbackParentInfo `json:"fallbackParent,omitempty" gorm:"-"`
// NodeIDProvided reports whether an update payload explicitly carried a
// nodeId key, as opposed to omitting it. The inbound controller sets it
// from the raw request body; UpdateInbound only treats the payload's
// NodeID as a (re)assignment target when it is set — an omitted nodeId
// keeps the stored assignment. Not persisted, not serialized.
NodeIDProvided bool `json:"-" gorm:"-"`
}
// FallbackParentInfo carries everything the frontend needs to rewrite a