feat(web/mazenet): cross-LAN port drag now creates a real bridge
Port-to-port edges previously lived only in the editor's local state
— the backend's edge model is decky<->LAN membership, so the deploy
validator still saw cross-LAN pairs as orphans. Drawing a line from
dmz-gateway to a decky in subnet-d6b2 did nothing that a later
DMZ_ORPHAN check could see.
Now onAddEdge inspects endpoints: same-LAN stays visual (no bridge
to create), cross-LAN calls attachEdge with the source decky and
the target LAN, multi-homing the decky so the validator's LAN
adjacency scan threads through it. The viz edge stores the returned
backendEdgeId; removeEdge detaches that membership before dropping
the local edge. Observed entities (attacker-pool) are read-only and
never bridge.
A toast ("BRIDGED <decky> -> <lan>") surfaces the backend-persistent
side of the gesture so the user knows it's not just a cosmetic line.
This commit is contained in:
@@ -231,13 +231,56 @@ const MazeNET: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}, [editor, flashErr, nets, nodes, topologyId]);
|
}, [editor, flashErr, nets, nodes, topologyId]);
|
||||||
|
|
||||||
/* Port→port edges stay UI-only (backend edges are decky↔LAN). */
|
/* Port→port edges:
|
||||||
const onAddEdge = useCallback((fromId: string, toId: string) => {
|
* - Same-LAN: visual-only (no bridge to create).
|
||||||
const id = `viz-${fromId}-${toId}-${Date.now()}`;
|
* - Cross-LAN: promote the source decky to multi-home into the
|
||||||
setEdges((prev) => prev.some((e) => (e.from === fromId && e.to === toId) || (e.from === toId && e.to === fromId))
|
* target LAN via attachEdge. The resulting viz edge carries a
|
||||||
? prev
|
* backendEdgeId so removeEdge can detach it later. Observed
|
||||||
: [...prev, { id, from: fromId, to: toId, traffic: 'active' as const }]);
|
* entities (attacker-pool) are read-only and never bridge. */
|
||||||
}, []);
|
const onAddEdge = useCallback(async (fromId: string, toId: string) => {
|
||||||
|
const fromNode = nodes.find((n) => n.id === fromId);
|
||||||
|
const toNode = nodes.find((n) => n.id === toId);
|
||||||
|
if (!fromNode || !toNode) return;
|
||||||
|
if (fromNode.kind === 'observed' || toNode.kind === 'observed') return;
|
||||||
|
|
||||||
|
const dup = edges.some((e) =>
|
||||||
|
(e.from === fromId && e.to === toId) || (e.from === toId && e.to === fromId),
|
||||||
|
);
|
||||||
|
if (dup) return;
|
||||||
|
|
||||||
|
const sameLan = fromNode.netId === toNode.netId;
|
||||||
|
if (sameLan || !topologyId) {
|
||||||
|
const id = `viz-${fromId}-${toId}-${Date.now()}`;
|
||||||
|
setEdges((prev) => [...prev, { id, from: fromId, to: toId, traffic: 'active' as const }]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetNet = nets.find((n) => n.id === toNode.netId);
|
||||||
|
if (!targetNet) return;
|
||||||
|
const fromName = fromNode.kind === 'decky' ? fromNode.name : '';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await editor.attachEdge(
|
||||||
|
topologyId,
|
||||||
|
{ decky_uuid: fromId, lan_id: toNode.netId, is_bridge: true },
|
||||||
|
fromName,
|
||||||
|
targetNet.label,
|
||||||
|
);
|
||||||
|
const backendEdgeId = res.kind === 'applied' ? res.data.id : `enqueued:${res.mutationId}`;
|
||||||
|
const id = `viz-${fromId}-${toId}-${Date.now()}`;
|
||||||
|
setEdges((prev) => [
|
||||||
|
...prev,
|
||||||
|
{ id, from: fromId, to: toId, traffic: 'active' as const, backendEdgeId },
|
||||||
|
]);
|
||||||
|
pushToast({
|
||||||
|
text: `BRIDGED ${fromName.toUpperCase()} → ${targetNet.label.toUpperCase()}`,
|
||||||
|
tone: 'violet',
|
||||||
|
icon: 'terminal',
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
flashErr(err, 'bridge failed');
|
||||||
|
}
|
||||||
|
}, [edges, editor, flashErr, nets, nodes, pushToast, topologyId]);
|
||||||
|
|
||||||
const interaction = useMazeInteraction({
|
const interaction = useMazeInteraction({
|
||||||
nets, nodes, setNets, setNodes, canvasRef,
|
nets, nodes, setNets, setNodes, canvasRef,
|
||||||
@@ -284,10 +327,33 @@ const MazeNET: React.FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const removeEdge = (id: string) => {
|
const removeEdge = async (id: string) => {
|
||||||
/* Viz-only edges: backend has no edge to delete here. */
|
const edge = edges.find((e) => e.id === id);
|
||||||
setEdges((p) => p.filter((e) => e.id !== id));
|
if (!edge) return;
|
||||||
setSelection(null);
|
|
||||||
|
/* Viz-only edges (same-LAN, pre-bridge era, or attach still in
|
||||||
|
* flight without a backing id) just drop from local state. */
|
||||||
|
if (!edge.backendEdgeId || !topologyId) {
|
||||||
|
setEdges((p) => p.filter((e) => e.id !== id));
|
||||||
|
setSelection(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Cross-LAN bridge: detach the membership edge before removing
|
||||||
|
* the viz edge. Look the names up from the endpoints so the live
|
||||||
|
* mutation path has what it needs. */
|
||||||
|
const fromNode = nodes.find((n) => n.id === edge.from);
|
||||||
|
const toNode = nodes.find((n) => n.id === edge.to);
|
||||||
|
const targetNet = toNode ? nets.find((n) => n.id === toNode.netId) : undefined;
|
||||||
|
const fromName = fromNode?.kind === 'decky' ? fromNode.name : '';
|
||||||
|
const lanName = targetNet?.label ?? '';
|
||||||
|
try {
|
||||||
|
await editor.detachEdge(topologyId, edge.backendEdgeId, fromName, lanName);
|
||||||
|
setEdges((p) => p.filter((e) => e.id !== id));
|
||||||
|
setSelection(null);
|
||||||
|
} catch (err) {
|
||||||
|
flashErr(err, 'unbridge failed');
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const duplicateNode = async (id: string) => {
|
const duplicateNode = async (id: string) => {
|
||||||
|
|||||||
@@ -45,5 +45,9 @@ export interface Edge {
|
|||||||
to: string;
|
to: string;
|
||||||
traffic: 'hot' | 'active' | 'idle';
|
traffic: 'hot' | 'active' | 'idle';
|
||||||
label?: string;
|
label?: string;
|
||||||
|
/** Backend membership-edge id when this visual edge mirrors a
|
||||||
|
* cross-LAN bridge attachment. Same-LAN edges stay visual-only
|
||||||
|
* and leave this undefined. Set at attach, consumed at detach. */
|
||||||
|
backendEdgeId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user