fix: cluster edges render on left/right handles from approve flow

Cluster edges created via the pending -> approve path rendered on the top
handle instead of left/right, because the edge and its endpoints lost their
handle information on the way to the canvas.

- Approve resolver (scan.py) now returns each edge's type + source/target
  handle. Handle IDs are the bare slot-0 side names ('right'/'left'), the
  canonical stored form React Flow resolves to the correct side; a '-t' target
  id fails to resolve and falls back to the top handle.
- Frontend injectAutoEdges no longer hardcodes iot/bottom/top-t. It injects
  each edge with its real type + handles and bumps the referenced nodes'
  left/right handle counts (which default to 0) so the cluster endpoints exist.
  Logic extracted to a pure, tested util (applyAutoEdges).
- clusterEdges direct-import path uses the bare 'left' target to match.

Tests: new autoEdges unit tests; updated backend handle assertions.

ha-relevant: maybe
This commit is contained in:
Pouzor
2026-07-06 11:13:32 +02:00
parent 9670d0a86a
commit 05ef746f22
8 changed files with 176 additions and 24 deletions
+18 -5
View File
@@ -642,16 +642,20 @@ async def _resolve_pending_links_for_ieee(
if edge_design_id is None:
first = (await db.execute(select(Design).order_by(Design.created_at).limit(1))).scalar()
edge_design_id = first.id if first else None
# Edge shape by link source:
# Edge shape by link source. Handle IDs are the *bare* slot-0 side names
# (the canonical stored form — the save path normalizes '<side>-t' → the
# bare source id, and React Flow resolves the bare id to that side). A
# '-t' target id does not resolve here and RF falls back to the top
# handle, so never emit one.
# proxmox → 'virtual' host→guest, vertical (bottom → top)
# proxmox_cluster → 'cluster' host↔host, horizontal (right → left)
# anything else → 'iot' mesh link, vertical
if link.discovery_source == "proxmox":
edge_type, src_handle, tgt_handle = "virtual", "bottom", "top-t"
edge_type, src_handle, tgt_handle = "virtual", "bottom", "top"
elif link.discovery_source == "proxmox_cluster":
edge_type, src_handle, tgt_handle = "cluster", "right", "left-t"
edge_type, src_handle, tgt_handle = "cluster", "right", "left"
else:
edge_type, src_handle, tgt_handle = "iot", "bottom", "top-t"
edge_type, src_handle, tgt_handle = "iot", "bottom", "top"
edge = Edge(
source=src_id,
target=tgt_id,
@@ -663,7 +667,16 @@ async def _resolve_pending_links_for_ieee(
db.add(edge)
await db.flush()
existing_pairs.add((src_id, tgt_id))
created.append({"id": edge.id, "source": src_id, "target": tgt_id})
# Return the edge's type + handles so the client injects it faithfully
# (a cluster edge must keep its right→left handles, not the iot default).
created.append({
"id": edge.id,
"source": src_id,
"target": tgt_id,
"type": edge_type,
"source_handle": src_handle,
"target_handle": tgt_handle,
})
await db.delete(link)
return created
+3 -1
View File
@@ -265,7 +265,9 @@ async def test_cluster_link_resolves_to_cluster_edge(db_session) -> None:
edge = (await db_session.execute(select(Edge))).scalars().one()
assert edge.type == "cluster"
assert edge.source_handle == "right"
assert edge.target_handle == "left-t"
# Bare side name (canonical) so React Flow resolves it to the left side;
# a '-t' target would fall back to the top handle.
assert edge.target_handle == "left"
@pytest.mark.asyncio
+2 -1
View File
@@ -1281,7 +1281,8 @@ async def test_approve_zigbee_creates_edge_when_other_endpoint_is_node(
assert edges[0].source == coord.id
assert edges[0].target == data["node_id"]
assert edges[0].source_handle == "bottom"
assert edges[0].target_handle == "top-t"
# Bare side name (canonical stored form); renders at the top like before.
assert edges[0].target_handle == "top"
assert edges[0].type == "iot"