fix: replace Proxmox cluster handles with configurable side points

The Proxmox node had two always-on cluster-left/cluster-right handles that
rendered independently of the new per-side connection-point config — so a
configured side stacked on top of the forced handle (e.g. 0 config still showed
1; 2 config left the forced one in the middle).

- ProxmoxGroupNode: remove the always-on cluster handles in both container and
  non-container modes; cluster links now use the normal per-side points
- add migrateClusterHandles(nodes, edges): on load, remap any edge still bound
  to cluster-left/right onto the left/right slot-0 point and set that node's
  side count to at least 1, so existing links survive (edge 'cluster' type/style
  kept). Applied on API load, standalone load, LiveView, and YAML import
- App: new cluster links are made by choosing the Cluster edge type in the edge
  modal (drop the cluster-handle auto-default)

ha-relevant: yes
This commit is contained in:
Pouzor
2026-07-04 01:35:28 +02:00
parent 6302c43e06
commit 1479c777d0
8 changed files with 130 additions and 59 deletions
@@ -6,6 +6,7 @@ import {
serializeEdge,
deserializeApiNode,
deserializeApiEdge,
migrateClusterHandles,
type ApiNode,
type ApiEdge,
} from '@/utils/canvasSerializer'
@@ -483,3 +484,40 @@ describe('serializeNode — text node roundtrip', () => {
expect(restored.data.label).toBe('Hello world')
})
})
// ── migrateClusterHandles (#243) ──────────────────────────────────────────────
describe('migrateClusterHandles', () => {
it('remaps cluster-right/left edge handles to right/left slot-0', () => {
const nodes = [makeRfNode({ id: 'p1', type: 'proxmox' }), makeRfNode({ id: 'p2', type: 'proxmox' })]
const edges = [makeRfEdge({ id: 'c1', source: 'p1', target: 'p2', type: 'cluster', sourceHandle: 'cluster-right', targetHandle: 'cluster-left', data: { type: 'cluster' } })]
const out = migrateClusterHandles(nodes, edges)
const e = out.edges.find((x) => x.id === 'c1')!
expect(e.sourceHandle).toBe('right')
expect(e.targetHandle).toBe('left')
expect(e.type).toBe('cluster') // style/type preserved
})
it('bumps the connected side to 1 on each proxmox node', () => {
const nodes = [makeRfNode({ id: 'p1', type: 'proxmox' }), makeRfNode({ id: 'p2', type: 'proxmox' })]
const edges = [makeRfEdge({ id: 'c1', source: 'p1', target: 'p2', sourceHandle: 'cluster-right', targetHandle: 'cluster-left' })]
const out = migrateClusterHandles(nodes, edges)
expect(out.nodes.find((n) => n.id === 'p1')!.data.right_handles).toBe(1)
expect(out.nodes.find((n) => n.id === 'p2')!.data.left_handles).toBe(1)
})
it('does not lower an already-higher side count', () => {
const nodes = [makeRfNode({ id: 'p1', type: 'proxmox', data: { label: 'x', type: 'proxmox', status: 'online', services: [], right_handles: 3 } })]
const edges = [makeRfEdge({ id: 'c1', source: 'p1', target: 'p2', sourceHandle: 'cluster-right' })]
const out = migrateClusterHandles(nodes, edges)
expect(out.nodes[0].data.right_handles).toBe(3)
})
it('leaves non-cluster edges and their nodes untouched (identity when nothing to do)', () => {
const nodes = [makeRfNode({ id: 'p1' })]
const edges = [makeRfEdge({ id: 'e1', sourceHandle: 'bottom', targetHandle: 'top' })]
const out = migrateClusterHandles(nodes, edges)
expect(out.nodes).toBe(nodes)
expect(out.edges[0].sourceHandle).toBe('bottom')
})
})