From 5107f59d94df28170687a09b1e136d557cc4faae Mon Sep 17 00:00:00 2001 From: sagitchu Date: Tue, 21 Apr 2026 16:53:47 +0800 Subject: [PATCH] fix: tolerate offline nodes when controlling forward services and editing tunnels - controlForwardServices: skip offline nodes instead of failing entire operation, so forward pause/resume/delete works when some entry nodes are offline - onNodeOnline: always sync forward state on node reconnect (not just post-upgrade), so forwards that changed status while a node was offline get synced - add ListForwardIDsByNode repo method to sync all forwards (including paused) - tunnel edit UI: allow deselecting already-selected offline nodes in entry/chain/exit selectors, matching the backend's existing tolerance --- .../internal/http/handler/control_plane.go | 3 + go-backend/internal/http/handler/upgrade.go | 6 +- .../internal/store/repo/repository_control.go | 16 ++++++ vite-frontend/src/pages/tunnel.tsx | 57 +++++++++++++++---- 4 files changed, 66 insertions(+), 16 deletions(-) diff --git a/go-backend/internal/http/handler/control_plane.go b/go-backend/internal/http/handler/control_plane.go index ba875c6..d3f62ec 100644 --- a/go-backend/internal/http/handler/control_plane.go +++ b/go-backend/internal/http/handler/control_plane.go @@ -474,6 +474,9 @@ func (h *Handler) controlForwardServices(forward *forwardRecord, commandType str nodeHandled, lastNotFoundErr, err := h.controlForwardServicesOnNode(fp.NodeID, bases, commandType) if err != nil { + if isNodeOfflineOrTimeoutError(err) { + continue + } return err } diff --git a/go-backend/internal/http/handler/upgrade.go b/go-backend/internal/http/handler/upgrade.go index 26ef384..ebde530 100644 --- a/go-backend/internal/http/handler/upgrade.go +++ b/go-backend/internal/http/handler/upgrade.go @@ -389,9 +389,7 @@ func (h *Handler) consumeNodePendingUpgradeRedeploy(nodeID int64) bool { } func (h *Handler) onNodeOnline(nodeID int64) { - if !h.consumeNodePendingUpgradeRedeploy(nodeID) { - return - } + h.consumeNodePendingUpgradeRedeploy(nodeID) h.redeployNodeRuntimeAfterUpgrade(nodeID) } @@ -401,7 +399,7 @@ func (h *Handler) redeployNodeRuntimeAfterUpgrade(nodeID int64) { fmt.Printf("post-upgrade redeploy: list tunnels for node %d failed: %v\n", nodeID, err) return } - forwardIDs, err := h.repo.ListActiveForwardIDsByNode(nodeID) + forwardIDs, err := h.repo.ListForwardIDsByNode(nodeID) if err != nil { fmt.Printf("post-upgrade redeploy: list forwards for node %d failed: %v\n", nodeID, err) return diff --git a/go-backend/internal/store/repo/repository_control.go b/go-backend/internal/store/repo/repository_control.go index e1c0882..bff4fbb 100644 --- a/go-backend/internal/store/repo/repository_control.go +++ b/go-backend/internal/store/repo/repository_control.go @@ -99,6 +99,22 @@ func (r *Repository) ListActiveForwardIDsByNode(nodeID int64) ([]int64, error) { return ids, nil } +func (r *Repository) ListForwardIDsByNode(nodeID int64) ([]int64, error) { + if r == nil || r.db == nil { + return nil, errors.New("repository not initialized") + } + var ids []int64 + err := r.db.Model(&model.ForwardPort{}). + Where("forward_port.node_id = ?", nodeID). + Select("DISTINCT forward_port.forward_id"). + Order("forward_port.forward_id ASC"). + Pluck("forward_port.forward_id", &ids).Error + if err != nil { + return nil, err + } + return ids, nil +} + func (r *Repository) ListForwardPorts(forwardID int64) ([]model.ForwardPortRecord, error) { if r == nil || r.db == nil { return nil, errors.New("repository not initialized") diff --git a/vite-frontend/src/pages/tunnel.tsx b/vite-frontend/src/pages/tunnel.tsx index 6c08ec6..efe4e77 100644 --- a/vite-frontend/src/pages/tunnel.tsx +++ b/vite-frontend/src/pages/tunnel.tsx @@ -2184,16 +2184,27 @@ export default function TunnelPage() {

入口配置

- + node.status !== 1 && + !( + isEdit && + form.inNodeId.some( + (ct) => ct.nodeId === node.id, + ) + ), + ) + .map((node) => node.id.toString()), + ...(form.outNodeId || []).map((ct) => + ct.nodeId.toString(), + ), + ...getSelectedChainNodeIds().map((id) => + id.toString(), + ), + ]} errorMessage={errors.inNodeId} isInvalid={!!errors.inNodeId} label="入口节点" @@ -2365,7 +2376,18 @@ export default function TunnelPage() { }} disabledKeys={[ ...nodes - .filter((node) => node.status !== 1) + .filter( + (node) => + node.status !== 1 && + !( + isEdit && + groupNodes.some( + (ct) => + ct.nodeId === node.id && + ct.nodeId !== -1, + ) + ), + ) .map((node) => node.id.toString()), ...form.inNodeId.map((ct) => ct.nodeId.toString(), @@ -2626,7 +2648,18 @@ export default function TunnelPage() { }} disabledKeys={[ ...nodes - .filter((node) => node.status !== 1) + .filter( + (node) => + node.status !== 1 && + !( + isEdit && + (form.outNodeId || []).some( + (ct) => + ct.nodeId === node.id && + ct.nodeId !== -1, + ) + ), + ) .map((node) => node.id.toString()), ...form.inNodeId.map((ct) => ct.nodeId.toString(),