diff --git a/frontend/src/features/analysis/ui/GraphView.vue b/frontend/src/features/analysis/ui/GraphView.vue index 331b32c..713f09e 100644 --- a/frontend/src/features/analysis/ui/GraphView.vue +++ b/frontend/src/features/analysis/ui/GraphView.vue @@ -238,6 +238,14 @@ async function renderGraph(): Promise { ) parentMap.value = computedParentMap + // Guard against dangling edges (source/target not in the node set). Both + // backends can emit them in edge cases — e.g. ON_PAGE edges whose page_no + // doesn't match any returned Page node, or DERIVED_FROM edges crossing + // document boundaries in Neo4j. Cytoscape-dagre auto-creates a skeleton + // graphlib entry for the missing endpoint with undefined data, then crashes + // in its ordering phase when it tries to set `.order` on that entry. + const nodeIds = new Set(payload.value.nodes.map((n) => n.id)) + const elements = [ ...payload.value.nodes.map((n) => ({ data: { @@ -256,14 +264,16 @@ async function renderGraph(): Promise { raw: n, }, })), - ...payload.value.edges.map((e) => ({ - data: { - id: e.id, - source: e.source, - target: e.target, - type: e.type, - }, - })), + ...payload.value.edges + .filter((e) => nodeIds.has(e.source) && nodeIds.has(e.target)) + .map((e) => ({ + data: { + id: e.id, + source: e.source, + target: e.target, + type: e.type, + }, + })), ] cy.value = cytoscape({