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
This commit is contained in:
sagitchu
2026-04-21 16:53:47 +08:00
parent 431613cb6a
commit 5107f59d94
4 changed files with 66 additions and 16 deletions
@@ -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
}
+2 -4
View File
@@ -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
@@ -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")
+45 -12
View File
@@ -2184,16 +2184,27 @@ export default function TunnelPage() {
<h3 className="text-lg font-semibold">入口配置</h3>
<div className="space-y-2">
<Select
disabledKeys={[
...nodes
.filter((node) => node.status !== 1)
.map((node) => node.id.toString()),
...(form.outNodeId || []).map((ct) =>
ct.nodeId.toString(),
),
...getSelectedChainNodeIds().map((id) => id.toString()),
]}
<Select
disabledKeys={[
...nodes
.filter(
(node) =>
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(),