diff --git a/data/network.json.example b/data/network.json.example index 0f32628..69c9004 100644 --- a/data/network.json.example +++ b/data/network.json.example @@ -53,5 +53,12 @@ } ] } + ], + "subnets": [ + { + "cidr": "192.168.1.0/24", + "name": "LAN", + "vlanId": 1 + } ] } diff --git a/src/lib/components/NetworkDiagram.svelte b/src/lib/components/NetworkDiagram.svelte index b2e5573..e04fa1a 100644 --- a/src/lib/components/NetworkDiagram.svelte +++ b/src/lib/components/NetworkDiagram.svelte @@ -20,6 +20,7 @@ withReconciledConnections } from '$lib/data/networkEditor'; import loadNetworkData from '$lib/data/loadNetworkData'; + import { buildIpamReport, suggestNextFreeIp, type IpamReport } from '$lib/data/ipam'; import { validateNetworkData, type ValidationIssue } from '$lib/data/networkSchema'; import transformNetworkDataToGraph from '$lib/graph/transformNetworkData'; import { computeSearchMatches } from '$lib/graph/searchHighlight'; @@ -82,6 +83,7 @@ let searchQuery = ''; let searchMatchCount = 0; let lastTransformResult: GraphTransformResult | null = null; + let showIpamPanel = false; let machineVmIndex: GraphTransformResult['machineVmIndex'] = {}; let collapsedHosts: Record = {}; let exportFileName = 'network-diagram'; @@ -135,6 +137,19 @@ }); $: visibleIconDefinitions = filteredIconDefinitions.slice(0, iconResultLimit); + $: ipamReport = buildIpamReport(networkData); + $: ipamWarnings = ipamReport.duplicates.map( + (duplicate) => `Duplicate IP ${duplicate.ip}: assigned to ${duplicate.owners.join(' and ')}.` + ); + + function duplicateOwnersForIp(report: IpamReport, ip: string, selfLabel: string): string[] { + const entry = report.duplicates.find((duplicate) => duplicate.ip === ip.trim()); + if (!entry) { + return []; + } + return entry.owners.filter((owner) => owner !== selfLabel); + } + $: selectedMachine = selectedTarget?.type === 'machine' ? networkData.machines[selectedTarget.index] : null; $: selectedDevice = @@ -143,6 +158,20 @@ selectedTarget?.type === 'vm' ? networkData.machines[selectedTarget.machineIndex]?.software.vms[selectedTarget.vmIndex] : null; + $: selectedMachineIpConflicts = selectedMachine + ? duplicateOwnersForIp(ipamReport, selectedMachine.ipAddress, selectedMachine.machineName) + : []; + $: selectedDeviceIpConflicts = selectedDevice + ? duplicateOwnersForIp(ipamReport, selectedDevice.ipAddress, selectedDevice.name) + : []; + $: selectedVmIpConflicts = + selectedVm && selectedTarget?.type === 'vm' + ? duplicateOwnersForIp( + ipamReport, + selectedVm.ipAddress, + `${selectedVm.name} (VM on ${networkData.machines[selectedTarget.machineIndex]?.machineName ?? ''})` + ) + : []; $: selectedMachineHostId = selectedMachine ? toMachineNodeId(selectedMachine.machineName) : null; $: selectedMachineVmCount = selectedMachineHostId && machineVmIndex[selectedMachineHostId] @@ -1380,6 +1409,7 @@ ...createEmptyMachine(), machineName: nextUniqueName('New Machine', existingNames) }; + newMachineDraft.ipAddress = suggestNextFreeIp(networkData) ?? newMachineDraft.ipAddress; addModalKind = 'machine'; } @@ -1389,9 +1419,47 @@ ...createEmptyDevice(), name: nextUniqueName('New Device', existingNames) }; + newDeviceDraft.ipAddress = suggestNextFreeIp(networkData) ?? newDeviceDraft.ipAddress; addModalKind = 'device'; } + function suggestIpForAddModal() { + const suggested = suggestNextFreeIp(networkData); + if (!suggested) { + return; + } + if (addModalKind === 'machine') { + newMachineDraft.ipAddress = suggested; + } else if (addModalKind === 'device') { + newDeviceDraft.ipAddress = suggested; + } + } + + function addDeclaredSubnet() { + const firstInferred = ipamReport.subnets.find((subnet) => !subnet.declared); + const existing = new Set((networkData.subnets ?? []).map((subnet) => subnet.cidr)); + let cidr = firstInferred?.cidr ?? '192.168.1.0/24'; + if (existing.has(cidr)) { + cidr = '192.168.1.0/24'; + } + if (existing.has(cidr)) { + return; + } + mutateDraft((draft) => { + draft.subnets ??= []; + draft.subnets.push({ cidr }); + }); + } + + function removeDeclaredSubnet(index: number) { + mutateDraft((draft) => { + draft.subnets?.splice(index, 1); + if (draft.subnets && draft.subnets.length === 0) { + delete draft.subnets; + } + }); + } + function submitAddEntity() { if (addModalKind === 'machine') { mutateDraft((draft) => { @@ -1728,6 +1796,13 @@ /> +