Merge pull request #3 from jcreek/2-improve-the-diagram-display

2 improve the diagram display
This commit is contained in:
Josh Creek
2026-02-22 14:45:41 +00:00
committed by GitHub
18 changed files with 4083 additions and 858 deletions
+9
View File
@@ -0,0 +1,9 @@
{
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "always"
},
"eslint.useFlatConfig": true,
"eslint.validate": ["javascript", "typescript", "svelte"]
}
+7 -7
View File
@@ -11,7 +11,7 @@
**Fully self-hostable via Docker**
**JSON-based network structure** (editable in UI or manually)
**Interactive network visualisation**
**Persistent storage of network configurations**
**Simple file-based configuration**
**Lightweight Svelte**
Use it to **document your home lab, office network, or cloud infrastructure** with an easy-to-use web interface.
@@ -25,20 +25,20 @@ Use it to **document your home lab, office network, or cloud infrastructure** wi
Pull and run the latest image:
```bash
docker run -d -p 8080:3000 -v /path/to/data:/data jcreek23/open-network-diagram
docker run -d -p 8080:3000 jcreek23/open-network-diagram
```
- **`-p 8080:3000`** → Maps the app to `http://localhost:8080`
- **`-v /path/to/data:/data`** → Mounts a folder for persistent `network.json` storage
### **2️⃣ Open the Web UI**
Visit **`http://localhost:8080`** to view and edit your network diagram.
Visit **`http://localhost:8080`** to view your network diagram.
### **3️⃣ Modify Your Network (JSON-Based)**
- The app reads **`network.json`** from the **mounted `/data` directory**.
- Use the **web UI** to edit and save network changes.
- The app reads **`/data/network.json`** from static assets.
- In this repo, the default file is **`static/data/network.json`**.
- Update that file to change the diagram data.
---
@@ -91,7 +91,7 @@ docker build -t open-network-diagram .
### **Run Locally**
```bash
docker run -p 8080:3000 -v /path/to/data:/data open-network-diagram
docker run -p 8080:3000 open-network-diagram
```
### **CI/CD (GitHub Actions)**
+63 -11
View File
@@ -1,37 +1,89 @@
import prettier from 'eslint-config-prettier';
import js from '@eslint/js';
import { includeIgnoreFile } from '@eslint/compat';
import { FlatCompat } from '@eslint/eslintrc';
import js from '@eslint/js';
import prettier from 'eslint-config-prettier';
import importPlugin from 'eslint-plugin-import';
import svelte from 'eslint-plugin-svelte';
import globals from 'globals';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import ts from 'typescript-eslint';
import tseslint from 'typescript-eslint';
import svelteConfig from './svelte.config.js';
const gitignorePath = fileURLToPath(new URL('./.gitignore', import.meta.url));
const baseDirectory = path.dirname(fileURLToPath(import.meta.url));
const compat = new FlatCompat({
baseDirectory,
recommendedConfig: js.configs.recommended
});
export default ts.config(
const airbnbBaseConfig = compat.extends('airbnb-base').map((config) => ({
...config,
files: ['**/*.{js,cjs,mjs,ts,cts,mts}']
}));
export default tseslint.config(
includeIgnoreFile(gitignorePath),
js.configs.recommended,
...ts.configs.recommended,
...svelte.configs.recommended,
{
ignores: ['eslint.config.js', 'svelte.config.js', 'vite.config.ts']
},
{
plugins: {
import: importPlugin
}
},
...airbnbBaseConfig,
...tseslint.configs.recommended,
...svelte.configs['flat/recommended'],
prettier,
...svelte.configs.prettier,
...svelte.configs['flat/prettier'],
{
languageOptions: {
globals: { ...globals.browser, ...globals.node }
},
rules: { 'no-undef': 'off' }
settings: {
'import/resolver': {
node: {
extensions: ['.js', '.mjs', '.cjs', '.ts', '.d.ts', '.svelte', '.json']
}
},
'import/extensions': ['.js', '.ts', '.mjs', '.svelte']
},
rules: {
curly: ['error', 'all'],
'brace-style': ['error', '1tbs', { allowSingleLine: false }],
'no-continue': 'off',
'no-restricted-syntax': 'off',
'no-inner-declarations': 'off',
'no-unused-vars': 'off',
'import/no-extraneous-dependencies': 'off',
'@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }],
'import/extensions': [
'error',
'ignorePackages',
{
js: 'never',
mjs: 'never',
cjs: 'never',
ts: 'never'
}
]
}
},
{
files: ['**/*.svelte', '**/*.svelte.ts', '**/*.svelte.js'],
ignores: ['eslint.config.js', 'svelte.config.js'],
languageOptions: {
parserOptions: {
projectService: true,
extraFileExtensions: ['.svelte'],
parser: ts.parser,
parser: tseslint.parser,
svelteConfig
}
},
rules: {
// Svelte components commonly export mutable props and top-level declarations.
'import/prefer-default-export': 'off',
'import/no-mutable-exports': 'off'
}
}
);
+10 -6
View File
@@ -15,7 +15,8 @@
},
"devDependencies": {
"@eslint/compat": "^1.2.5",
"@eslint/js": "^9.18.0",
"@eslint/eslintrc": "^3.3.1",
"@eslint/js": "^8.57.1",
"@sveltejs/adapter-auto": "^4.0.0",
"@sveltejs/adapter-netlify": "^5.0.0",
"@sveltejs/kit": "^2.16.0",
@@ -23,17 +24,19 @@
"@tailwindcss/typography": "^0.5.15",
"@tailwindcss/vite": "^4.0.0",
"@types/cytoscape-dagre": "^2.3.3",
"eslint": "^9.18.0",
"eslint-config-prettier": "^10.0.1",
"eslint": "^8.57.1",
"eslint-config-airbnb-base": "^15.0.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-import": "^2.32.0",
"eslint-plugin-svelte": "^3.0.0",
"globals": "^16.0.0",
"prettier": "^3.4.2",
"prettier": "^3.8.1",
"prettier-plugin-svelte": "^3.3.3",
"prettier-plugin-tailwindcss": "^0.6.11",
"svelte": "^5.0.0",
"svelte-check": "^4.0.0",
"tailwindcss": "^4.0.0",
"typescript": "^5.0.0",
"typescript": "^5.9.3",
"typescript-eslint": "^8.20.0",
"vite": "^6.2.5"
},
@@ -44,6 +47,7 @@
},
"packageManager": "pnpm@9.15.4+sha512.b2dc20e2fc72b3e18848459b37359a32064663e5627a51e4c74b2c29dd8e8e0491483c3abb40789cfd578bf362fb6ba8261b05f0387d76792ed6e23ea3b1b6a0",
"dependencies": {
"leader-line": "^1.0.8"
"cytoscape": "^3.31.2",
"cytoscape-dagre": "^2.5.0"
}
}
+1512 -187
View File
File diff suppressed because it is too large Load Diff
-1
View File
@@ -5,7 +5,6 @@
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
%sveltekit.head%
<script src="/node_modules/leader-line/leader-line.min.js"></script>
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
-46
View File
@@ -1,46 +0,0 @@
<script lang="ts">
import type { NetworkDevice } from '../../lib/types';
export let device: NetworkDevice;
</script>
<div class="device-card">
<h3>{device.name}</h3>
<p><strong>Type:</strong> {device.type}</p>
<p><strong>IP:</strong> {device.ipAddress}</p>
{#if device.notes}
<p><em>{device.notes}</em></p>
{/if}
{#if device.ports && device.ports.length}
<h4>Ports:</h4>
<div class="ports-container">
{#each device.ports as port}
<div class="port-item" data-port-key={device.name + '-' + port.portName}>
<p>{port.portName} ({port.speedGbps ?? 1}GbE)</p>
{#if port.connectedTo}
<p>Connected to: {port.connectedTo}</p>
{/if}
</div>
{/each}
</div>
{/if}
</div>
<style>
.device-card {
border: 1px dashed #aaa;
padding: 1rem;
border-radius: 5px;
max-width: 300px;
}
.ports-container {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.port-item {
background-color: #ffe7d6;
padding: 0.5rem;
border-radius: 4px;
}
</style>
-59
View File
@@ -1,59 +0,0 @@
<script lang="ts">
import type { Machine } from '../../lib/types';
import VMCard from './VMCard.svelte';
export let machine: Machine;
</script>
<div class="machine-card">
<h2>{machine.machineName}</h2>
<p><strong>Role:</strong> {machine.role}</p>
<p><strong>IP Address:</strong> {machine.ipAddress}</p>
<p><strong>OS:</strong> {machine.operatingSystem}</p>
{#if machine.ports && machine.ports.length}
<h4>Ports:</h4>
<div class="ports-container">
{#each machine.ports as port}
<div class="port-item" data-port-key="{machine.machineName}-{port.portName}">
<p>{port.portName} ({port.speedGbps ?? 1}GbE)</p>
{#if port.connectedTo}
<p>Connected to: {port.connectedTo}</p>
{/if}
</div>
{/each}
</div>
{/if}
<!-- VMs -->
{#if machine.software?.vms?.length}
<details>
<summary>Show VMs (Total: {machine.software.vms.length})</summary>
<div class="vm-list">
{#each machine.software.vms as vm}
<VMCard {vm} />
{/each}
</div>
</details>
{/if}
</div>
<style>
.machine-card {
border: 2px solid #ddd;
padding: 1rem;
border-radius: 8px;
max-width: 300px;
background-color: #f9f9f9;
}
.ports-container {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.port-item {
background-color: #e7f1f9;
padding: 0.5rem;
border-radius: 4px;
}
</style>
+120
View File
@@ -0,0 +1,120 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte';
export let isOpen = false;
export let title = '';
export let maxWidth = '680px';
const dispatch = createEventDispatcher<{ close: void }>();
function closeModal() {
dispatch('close');
}
function onBackdropClick(event: MouseEvent) {
if (event.target === event.currentTarget) {
closeModal();
}
}
function onWindowKeydown(event: KeyboardEvent) {
if (event.key === 'Escape' && isOpen) {
closeModal();
}
}
</script>
<svelte:window on:keydown={onWindowKeydown} />
{#if isOpen}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div class="modal-backdrop" on:click={onBackdropClick}>
<div class="modal-container" style={`max-width: ${maxWidth}`}>
<header class="modal-header">
{#if title}
<h2>{title}</h2>
{/if}
<button type="button" class="close-button" on:click={closeModal} aria-label="Close">×</button>
</header>
<div class="modal-body">
<slot />
</div>
</div>
</div>
{/if}
<style>
.modal-backdrop {
position: fixed;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
padding: 1rem;
background: rgba(15, 23, 42, 0.58);
z-index: 1000;
}
.modal-container {
width: 100%;
max-height: 90vh;
display: flex;
flex-direction: column;
background: #f8fafc;
border: 1px solid #cbd5e1;
border-radius: 12px;
box-shadow: 0 24px 48px rgba(15, 23, 42, 0.24);
}
.modal-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.5rem;
padding: 0.9rem 1rem;
border-bottom: 1px solid #e2e8f0;
}
.modal-header h2 {
margin: 0;
font-size: 1.05rem;
color: #0f172a;
}
.close-button {
border: 1px solid transparent;
background: transparent;
color: #475569;
font-size: 1.7rem;
line-height: 1;
width: 2rem;
height: 2rem;
border-radius: 8px;
cursor: pointer;
}
.close-button:hover {
background: #e2e8f0;
color: #0f172a;
}
.modal-body {
padding: 1rem;
overflow: auto;
}
@media (max-width: 720px) {
.modal-backdrop {
padding: 0;
}
.modal-container {
height: 100vh;
max-height: 100vh;
border-radius: 0;
border-left: 0;
border-right: 0;
}
}
</style>
File diff suppressed because it is too large Load Diff
-19
View File
@@ -1,19 +0,0 @@
<script lang="ts">
import type { VMInfo } from '../../stores/networkStore';
export let vm: VMInfo;
</script>
<div class="vm-card">
<p><strong>{vm.name}</strong></p>
<p>Role: {vm.role}</p>
<p>IP: {vm.ipAddress}</p>
</div>
<style>
.vm-card {
padding: 0.5rem;
background-color: #eaeaea;
border: 1px solid #ccc;
border-radius: 5px;
}
</style>
+37
View File
@@ -0,0 +1,37 @@
import type { NetworkData } from '../types';
function isNetworkData(value: unknown): value is NetworkData {
if (!value || typeof value !== 'object') {
return false;
}
const candidate = value as Partial<NetworkData>;
return Array.isArray(candidate.machines) && Array.isArray(candidate.devices);
}
export default async function loadNetworkData(jsonPath: string): Promise<NetworkData> {
const response = await fetch(jsonPath, {
headers: {
Accept: 'application/json'
}
});
if (!response.ok) {
throw new Error(`Failed to load ${jsonPath} (${response.status} ${response.statusText})`);
}
let rawData: unknown;
try {
rawData = await response.json();
} catch (error) {
throw new Error(`Failed to parse JSON at ${jsonPath}`, {
cause: error
});
}
if (!isNetworkData(rawData)) {
throw new Error(`JSON at ${jsonPath} is not valid network data`);
}
return rawData;
}
+298
View File
@@ -0,0 +1,298 @@
import type { ConnectedPortRef, NetworkData, Port } from '../types';
import type { GraphEdgeElement, GraphNodeElement, GraphTransformResult } from './types';
interface EndpointOwner {
kind: 'machine' | 'device';
nodeId: string;
name: string;
ports: Map<string, Port>;
}
function toOwnerKey(kind: EndpointOwner['kind'], name: string): string {
return `${kind}:${name}`;
}
function toMachineNodeId(name: string): string {
return `machine:${encodeURIComponent(name)}`;
}
function toDeviceNodeId(name: string): string {
return `device:${encodeURIComponent(name)}`;
}
function toVmNodeId(hostName: string, vmName: string): string {
return `vm:${encodeURIComponent(hostName)}:${encodeURIComponent(vmName)}`;
}
function parseConnectedTo(connection: Port['connectedTo']): ConnectedPortRef | null {
if (!connection?.device || !connection?.port) {
return null;
}
return {
device: connection.device.trim(),
port: connection.port.trim()
};
}
function hasReciprocalLink(
targetPort: Port | undefined,
expectedDeviceName: string,
expectedPortName: string
): boolean {
const reverse = parseConnectedTo(targetPort?.connectedTo);
if (!reverse) {
return false;
}
return reverse.device === expectedDeviceName && reverse.port === expectedPortName;
}
function connectionKey(a: string, b: string): string {
return a < b ? `${a}|${b}` : `${b}|${a}`;
}
function edgeSpeed(a: number | undefined, b: number | undefined): number | undefined {
if (typeof a === 'number' && typeof b === 'number') {
return Math.min(a, b);
}
if (typeof a === 'number') {
return a;
}
if (typeof b === 'number') {
return b;
}
return undefined;
}
export default function transformNetworkDataToGraph(data: NetworkData): GraphTransformResult {
const nodes: GraphNodeElement[] = [];
const edges: GraphEdgeElement[] = [];
const warnings: string[] = [];
const machineVmIndex: Record<
string,
{
vmNodeIds: string[];
hostingEdgeIds: string[];
vmCount: number;
}
> = {};
const ownersByKey = new Map<string, EndpointOwner>();
const seenWarnings = new Set<string>();
const warn = (message: string) => {
if (!seenWarnings.has(message)) {
seenWarnings.add(message);
warnings.push(message);
}
};
const addOwner = (owner: EndpointOwner) => {
const ownerKey = toOwnerKey(owner.kind, owner.name);
if (ownersByKey.has(ownerKey)) {
warn(
`Duplicate ${owner.kind} name "${owner.name}" found. First occurrence will be used for link resolution.`
);
return;
}
ownersByKey.set(ownerKey, owner);
};
let hostingEdgeCounter = 0;
for (const machine of data.machines) {
const machineNodeId = toMachineNodeId(machine.machineName);
const machinePortsList = [...(machine.ports ?? [])];
const sourceMachineVms = machine.software?.vms ?? [];
const machineVms = sourceMachineVms.map((vm) => ({
name: vm.name,
ip: vm.ipAddress,
role: vm.role
}));
nodes.push({
data: {
id: machineNodeId,
label: machine.machineName,
rawName: machine.machineName,
kind: 'machine',
nodeWidth: 200,
nodeHeight: 110,
details: {
type: 'machine',
name: machine.machineName,
ip: machine.ipAddress,
role: machine.role,
os: machine.operatingSystem,
cpu: machine.hardware?.cpu ?? 'Unknown',
ram: machine.hardware?.ram ?? 'Unknown',
gpu: machine.hardware?.gpu ?? undefined,
ports: machinePortsList,
vmCount: machineVms.length,
vms: machineVms
}
}
});
machineVmIndex[machineNodeId] = {
vmNodeIds: [],
hostingEdgeIds: [],
vmCount: sourceMachineVms.length
};
for (const vm of sourceMachineVms) {
const vmNodeId = toVmNodeId(machine.machineName, vm.name);
nodes.push({
data: {
id: vmNodeId,
label: vm.name,
rawName: vm.name,
kind: 'vm',
hostMachineId: machineNodeId,
nodeWidth: 140,
nodeHeight: 66,
details: {
type: 'vm',
name: vm.name,
ip: vm.ipAddress,
role: vm.role,
hostName: machine.machineName
}
}
});
hostingEdgeCounter += 1;
const hostingEdgeId = `hosting:${hostingEdgeCounter}`;
edges.push({
data: {
id: hostingEdgeId,
source: machineNodeId,
target: vmNodeId,
kind: 'hosting',
label: 'hosts'
}
});
machineVmIndex[machineNodeId].vmNodeIds.push(vmNodeId);
machineVmIndex[machineNodeId].hostingEdgeIds.push(hostingEdgeId);
}
const machinePorts = new Map<string, Port>();
for (const port of machine.ports ?? []) {
machinePorts.set(port.portName, port);
}
addOwner({
kind: 'machine',
nodeId: machineNodeId,
name: machine.machineName,
ports: machinePorts
});
}
for (const device of data.devices) {
const deviceNodeId = toDeviceNodeId(device.name);
const devicePorts = [...(device.ports ?? [])];
nodes.push({
data: {
id: deviceNodeId,
label: device.name,
rawName: device.name,
kind: 'device',
nodeWidth: 176,
nodeHeight: 116,
details: {
type: 'device',
name: device.name,
ip: device.ipAddress,
deviceType: device.type,
notes: device.notes,
ports: devicePorts
}
}
});
const devicePortMap = new Map<string, Port>();
for (const port of device.ports ?? []) {
devicePortMap.set(port.portName, port);
}
addOwner({
kind: 'device',
nodeId: deviceNodeId,
name: device.name,
ports: devicePortMap
});
}
const seenPhysicalConnections = new Set<string>();
let physicalEdgeCounter = 0;
for (const owner of ownersByKey.values()) {
for (const port of owner.ports.values()) {
const connectedTo = parseConnectedTo(port.connectedTo);
if (!connectedTo) {
continue;
}
const machineTargetOwner = ownersByKey.get(toOwnerKey('machine', connectedTo.device));
const deviceTargetOwner = ownersByKey.get(toOwnerKey('device', connectedTo.device));
if (machineTargetOwner && deviceTargetOwner) {
warn(
`Link from "${owner.name}:${port.portName}" references ambiguous device "${connectedTo.device}" (matches both machine and device names).`
);
continue;
}
const targetOwner = machineTargetOwner ?? deviceTargetOwner;
if (!targetOwner) {
warn(
`Link from "${owner.name}:${port.portName}" references unknown device "${connectedTo.device}".`
);
continue;
}
const targetPort = targetOwner.ports.get(connectedTo.port);
if (!targetPort) {
warn(
`Link from "${owner.name}:${port.portName}" references unknown port "${connectedTo.device}:${connectedTo.port}".`
);
continue;
}
const a = `${owner.nodeId}:${port.portName}`;
const b = `${targetOwner.nodeId}:${connectedTo.port}`;
const dedupeKey = connectionKey(a, b);
if (seenPhysicalConnections.has(dedupeKey)) {
continue;
}
seenPhysicalConnections.add(dedupeKey);
const reciprocal = hasReciprocalLink(targetPort, owner.name, port.portName);
if (!reciprocal) {
warn(
`Non-reciprocal link: "${owner.name}:${port.portName}" points to "${connectedTo.device}:${connectedTo.port}" but reverse link is missing.`
);
}
physicalEdgeCounter += 1;
const speedGbps = edgeSpeed(port.speedGbps, targetPort.speedGbps);
edges.push({
data: {
id: `physical:${physicalEdgeCounter}`,
source: owner.nodeId,
target: targetOwner.nodeId,
kind: 'physical',
label: speedGbps ? `${speedGbps}GbE` : undefined,
sourcePort: port.portName,
targetPort: connectedTo.port,
speedGbps,
reciprocal
}
});
}
}
return { nodes, edges, warnings, machineVmIndex };
}
+93
View File
@@ -0,0 +1,93 @@
export type GraphNodeKind = 'machine' | 'device' | 'vm';
export type GraphEdgeKind = 'physical' | 'hosting';
export interface PortDetails {
portName: string;
speedGbps?: number;
connectedTo?: {
device: string;
port: string;
};
}
export interface MachineDetails {
type: 'machine';
name: string;
ip: string;
role: string;
os: string;
cpu: string;
ram: string;
gpu?: string;
ports: PortDetails[];
vmCount: number;
vms: Array<{
name: string;
ip: string;
role: string;
}>;
}
export interface DeviceDetails {
type: 'device';
name: string;
ip: string;
deviceType: string;
notes?: string;
ports: PortDetails[];
}
export interface VmDetails {
type: 'vm';
name: string;
ip: string;
role: string;
hostName: string;
}
export type GraphNodeDetails = MachineDetails | DeviceDetails | VmDetails;
export interface GraphNodeData {
id: string;
label: string;
kind: GraphNodeKind;
rawName?: string;
hostMachineId?: string;
nodeWidth?: number;
nodeHeight?: number;
details?: GraphNodeDetails;
}
export interface GraphEdgeData {
id: string;
source: string;
target: string;
kind: GraphEdgeKind;
label?: string;
sourcePort?: string;
targetPort?: string;
speedGbps?: number;
reciprocal?: boolean;
}
export interface GraphNodeElement {
data: GraphNodeData;
}
export interface GraphEdgeElement {
data: GraphEdgeData;
}
export interface GraphTransformResult {
nodes: GraphNodeElement[];
edges: GraphEdgeElement[];
warnings: string[];
machineVmIndex: Record<
string,
{
vmNodeIds: string[];
hostingEdgeIds: string[];
vmCount: number;
}
>;
}
+6 -1
View File
@@ -1,7 +1,12 @@
export interface ConnectedPortRef {
device: string;
port: string;
}
export interface Port {
portName: string;
speedGbps?: number; // default to 1 if not provided
connectedTo?: string; // e.g. "Router-LAN2"
connectedTo?: ConnectedPortRef;
}
export interface Hardware {
-62
View File
@@ -1,62 +0,0 @@
// Simple grid-based A* pathfinding for orthogonal routing
// Returns a list of [x, y] points from start to end, or [] if no path found
export type Point = [number, number];
interface Node {
x: number;
y: number;
g: number;
h: number;
f: number;
parent?: Node;
}
export function astar(grid: number[][], start: Point, end: Point): Point[] {
const height = grid.length;
const width = grid[0].length;
const open: Node[] = [];
const closed: boolean[][] = Array.from({ length: height }, () => Array(width).fill(false));
function heuristic([x, y]: Point): number {
// Manhattan distance
return Math.abs(x - end[0]) + Math.abs(y - end[1]);
}
open.push({ x: start[0], y: start[1], g: 0, h: heuristic(start), f: heuristic(start) });
while (open.length > 0) {
// Get node with lowest f
open.sort((a, b) => a.f - b.f);
const current = open.shift()!;
if (current.x === end[0] && current.y === end[1]) {
// Reconstruct path
const path: Point[] = [];
let node: Node | undefined = current;
while (node) {
path.push([node.x, node.y]);
node = node.parent;
}
return path.reverse();
}
closed[current.y][current.x] = true;
// Explore neighbors (orthogonal only)
for (const [dx, dy] of [[1,0], [-1,0], [0,1], [0,-1]]) {
const nx = current.x + dx;
const ny = current.y + dy;
if (nx < 0 || ny < 0 || nx >= width || ny >= height) continue;
if (grid[ny][nx] === 1 || closed[ny][nx]) continue; // Obstacle or closed
const g = current.g + 1;
const h = heuristic([nx, ny]);
const existing = open.find(n => n.x === nx && n.y === ny);
if (!existing) {
open.push({ x: nx, y: ny, g, h, f: g + h, parent: current });
} else if (g < existing.g) {
existing.g = g;
existing.f = g + h;
existing.parent = current;
}
}
}
return [];
}
-270
View File
@@ -1,270 +0,0 @@
import { writable } from 'svelte/store';
import type { NetworkData } from '../lib/types';
// Create a writable store for the data
export const networkStore = writable<NetworkData>({
machines: [],
devices: []
});
// A function to load JSON from an API or local file if needed
export async function loadNetworkData(jsonPath: string) {
// For a real app, fetch the file or do an import:
// const response = await fetch(jsonPath);
// const data = await response.json();
// networkStore.set(data);
// For now, set a static example with 'ports' and sample connections:
const exampleData: NetworkData = {
machines: [
{
machineName: 'ProxRouter',
ipAddress: '10.0.0.3',
role: 'Hypervisor',
operatingSystem: 'Proxmox',
ports: [
{
portName: 'eth0',
speedGbps: 10,
// Connect to the router's LAN2
connectedTo: 'Router-LAN2'
},
{
portName: 'eth1',
speedGbps: 1
// Not connected
}
],
software: {
vms: [
{ name: 'OpnSense', role: 'Router', ipAddress: '10.0.0.4' },
{ name: 'PiVPN', role: 'VPN Server', ipAddress: '10.0.0.5' },
{ name: 'PiHole', role: 'DNS Ad-blocker', ipAddress: '10.0.0.6' },
{ name: 'Dashy', role: 'Dashboard', ipAddress: '10.0.0.12' },
{ name: 'DockerHost', role: 'Docker/Reverse Proxy', ipAddress: '10.0.0.23' }
]
},
hardware: {
cpu: 'Intel N100',
ram: '8GB',
networkPorts: 4,
networkPortSpeedGbps: 1,
gpu: null
}
},
{
machineName: 'Asustor NAS',
ipAddress: '10.0.0.9',
role: 'NAS',
operatingSystem: 'Asustor ADM',
ports: [
{
portName: 'eth0',
speedGbps: 1
// No connection
}
],
software: { vms: [] },
hardware: {
cpu: 'Realtek RTD1296 Quad Core 1.4GHz',
ram: '2GB',
networkPorts: 1,
networkPortSpeedGbps: 1,
gpu: null
}
},
{
machineName: 'Home Assistant Green',
ipAddress: '10.0.0.13',
role: 'Smart Home Controller',
operatingSystem: 'Home Assistant OS',
ports: [
{
portName: 'eth0',
speedGbps: 1
// Not connected
}
],
software: { vms: [] },
hardware: {
cpu: 'Home Assistant Custom SoC',
ram: 'Unknown',
networkPorts: 1,
networkPortSpeedGbps: 1,
gpu: null
}
},
{
machineName: 'Plex Server',
ipAddress: '10.0.0.11',
role: 'Media Server',
operatingSystem: 'Ubuntu Server',
ports: [
{
portName: 'eth0',
speedGbps: 1
// Not connected
}
],
software: { vms: [] },
hardware: {
cpu: 'Intel N100',
ram: 'Unknown',
networkPorts: 1,
networkPortSpeedGbps: 1,
gpu: null
}
},
{
machineName: 'Win11 N100',
ipAddress: '10.0.0.8',
role: 'Media Downloader',
operatingSystem: 'Windows 11',
ports: [
{
portName: 'eth0',
speedGbps: 1
// Not connected
}
],
software: { vms: [] },
hardware: {
cpu: 'Intel N100',
ram: 'Unknown',
networkPorts: 1,
networkPortSpeedGbps: 1,
gpu: null
}
},
{
machineName: 'Win11 Backblaze NAS',
ipAddress: '10.0.0.7',
role: 'Backup NAS + Hypervisor',
operatingSystem: 'TrueNAS Scale',
ports: [
{
portName: 'eth0',
speedGbps: 1
// Not connected
}
],
software: {
vms: [
{ name: 'Win11-Backblaze', role: 'Backblaze Backup', ipAddress: '10.0.0.24' },
{ name: 'UbuntuDockerHost', role: 'Docker Host', ipAddress: '10.0.0.14' },
{ name: 'MakeMKV', role: 'Blu-ray Ripper', ipAddress: '10.0.0.14' },
{ name: 'Handbrake', role: 'Video Transcoder', ipAddress: '10.0.0.15' },
{ name: 'NextCloud', role: 'Cloud Storage', ipAddress: 'TBD' }
]
},
hardware: {
cpu: 'AMD Ryzen 5 4600G',
ram: '16GB',
networkPorts: 1,
networkPortSpeedGbps: 1,
gpu: 'EVGA GeForce GTX 1050 Ti'
}
},
{
machineName: 'AI Server',
ipAddress: '10.0.0.20',
role: 'AI Dev/Inference',
operatingSystem: 'Pop!_OS',
ports: [
{
portName: 'eth0',
speedGbps: 10
// Not connected
}
],
software: {
vms: [
{ name: 'Ollama', role: 'LLM Inference', ipAddress: '10.0.0.21' },
{ name: 'Bot Training', role: 'AI Training', ipAddress: '10.0.0.22' }
]
},
hardware: {
cpu: 'AMD Ryzen 5 3600',
ram: '32GB',
networkPorts: 1,
networkPortSpeedGbps: 1,
gpu: 'Gigabyte GeForce GTX 1080'
}
}
],
devices: [
{
name: 'Router',
ipAddress: '10.0.0.1',
type: 'Gateway',
notes: 'Main internet-facing router',
ports: [
{
portName: 'LAN1',
speedGbps: 1,
// Connect to Omada Controller
connectedTo: 'Omada Controller-eth0'
},
{
portName: 'LAN2',
speedGbps: 10,
// Connected to ProxRouter
connectedTo: 'ProxRouter-eth0'
},
{
portName: 'LAN3',
speedGbps: 1,
// Connect to HDHomeRun
connectedTo: 'HDHomeRun-eth0'
}
]
},
{
name: 'HDHomeRun',
ipAddress: '10.0.0.2',
type: 'TV Tuner',
ports: [
{
portName: 'eth0',
speedGbps: 1,
connectedTo: 'Router-LAN3'
}
]
},
{
name: 'Omada Controller',
ipAddress: '10.0.0.4',
type: 'Network Controller',
notes: 'Access via port 8043',
ports: [
{
portName: 'eth0',
speedGbps: 1,
connectedTo: 'Router-LAN1'
}
]
},
{
name: '3DS',
ipAddress: '10.0.0.17',
type: 'Handheld Console'
// No ports => Wi-Fi only
},
{
name: '2DS',
ipAddress: '10.0.0.18',
type: 'Handheld Console'
// No ports => Wi-Fi only
},
{
name: 'Nintendo Switch',
ipAddress: '10.0.0.19',
type: 'Gaming Console',
notes: 'IP to be confirmed'
// No ports => Wi-Fi only
}
]
};
networkStore.set(exampleData);
}
+494
View File
@@ -0,0 +1,494 @@
{
"machines": [
{
"machineName": "ProxRouter",
"ipAddress": "10.0.0.3",
"role": "Hypervisor",
"operatingSystem": "Proxmox",
"ports": [
{
"portName": "eth0",
"speedGbps": 1,
"connectedTo": {
"device": "Gigabit Switch",
"port": "1"
}
},
{
"portName": "eth1",
"speedGbps": 1
},
{
"portName": "eth2",
"speedGbps": 1,
"connectedTo": {
"device": "Gigabit Switch",
"port": "2"
}
},
{
"portName": "eth3",
"speedGbps": 1,
"connectedTo": {
"device": "WAN Uplink",
"port": "wan"
}
}
],
"software": {
"vms": [
{
"name": "OpnSense",
"role": "Router/Firewall/Gateway (DHCP 10.0.0.100-254)",
"ipAddress": "10.0.0.1"
},
{
"name": "TPLink Omada Controller",
"role": "Network Controller (:8043)",
"ipAddress": "10.0.0.4"
},
{ "name": "PiVPN (WireGuard)", "role": "VPN Server", "ipAddress": "10.0.0.5" },
{
"name": "PiHole",
"role": "DNS Ad-blocker (installed, not in active use)",
"ipAddress": "10.0.0.6"
},
{ "name": "Dashy", "role": "Dashboard", "ipAddress": "10.0.0.12" },
{
"name": "ProxRouter-Docker",
"role": "Docker host (Nginx reverse proxy, RustDesk, TwinGate)",
"ipAddress": "10.0.0.23"
}
]
},
"hardware": {
"cpu": "Intel N100",
"ram": "8GB",
"networkPorts": 4,
"networkPortSpeedGbps": 1
}
},
{
"machineName": "Asustor NAS",
"ipAddress": "10.0.0.9",
"role": "NAS",
"operatingSystem": "Asustor ADM",
"ports": [
{
"portName": "eth0",
"speedGbps": 1,
"connectedTo": {
"device": "Gigabit Switch",
"port": "3"
}
}
],
"software": { "vms": [] },
"hardware": {
"cpu": "Realtek RTD1296 Quad Core 1.4GHz",
"ram": "2GB",
"networkPorts": 1,
"networkPortSpeedGbps": 1
}
},
{
"machineName": "Home Assistant Green",
"ipAddress": "10.0.0.13",
"role": "Smart Home Controller",
"operatingSystem": "Home Assistant OS",
"ports": [
{
"portName": "eth0",
"speedGbps": 1,
"connectedTo": {
"device": "Gigabit Switch",
"port": "4"
}
}
],
"software": { "vms": [] },
"hardware": {
"cpu": "Home Assistant Custom SoC",
"ram": "Unknown",
"networkPorts": 1,
"networkPortSpeedGbps": 1
}
},
{
"machineName": "Plex Server",
"ipAddress": "10.0.0.11",
"role": "Media Server",
"operatingSystem": "Ubuntu Server",
"ports": [
{
"portName": "eth0",
"speedGbps": 1,
"connectedTo": {
"device": "Gigabit Switch",
"port": "5"
}
}
],
"software": { "vms": [] },
"hardware": {
"cpu": "Intel N100",
"ram": "Unknown",
"networkPorts": 1,
"networkPortSpeedGbps": 1
}
},
{
"machineName": "Win11 N100",
"ipAddress": "10.0.0.8",
"role": "Media Downloader",
"operatingSystem": "Windows 11",
"ports": [
{
"portName": "eth0",
"speedGbps": 1,
"connectedTo": {
"device": "Gigabit Switch",
"port": "6"
}
}
],
"software": { "vms": [] },
"hardware": {
"cpu": "Intel N100",
"ram": "Unknown",
"networkPorts": 1,
"networkPortSpeedGbps": 1
}
},
{
"machineName": "Win11 Backblaze NAS",
"ipAddress": "10.0.0.7",
"role": "Backup NAS + Hypervisor",
"operatingSystem": "TrueNAS Scale",
"ports": [
{
"portName": "eth0",
"speedGbps": 1,
"connectedTo": {
"device": "Gigabit Switch",
"port": "7"
}
}
],
"software": {
"vms": [
{ "name": "Win11NAS", "role": "Backblaze Backup VM", "ipAddress": "10.0.0.24" },
{ "name": "MakeMKV", "role": "Blu-ray Ripper", "ipAddress": "10.0.0.14" },
{ "name": "Handbrake", "role": "Video Transcoder", "ipAddress": "10.0.0.15" }
]
},
"hardware": {
"cpu": "AMD Ryzen 5 4600G",
"ram": "16GB",
"networkPorts": 1,
"networkPortSpeedGbps": 1,
"gpu": "EVGA GeForce GTX 1050 Ti"
}
},
{
"machineName": "TrueNAS Host",
"ipAddress": "10.0.0.10",
"role": "Storage + VM Host",
"operatingSystem": "TrueNAS Scale",
"ports": [
{
"portName": "eth0",
"speedGbps": 1,
"connectedTo": {
"device": "Gigabit Switch",
"port": "8"
}
}
],
"software": {
"vms": [
{
"name": "TrueNAS-Docker",
"role": "Docker host on TrueNAS",
"ipAddress": "10.0.0.25"
}
]
},
"hardware": {
"cpu": "Unknown",
"ram": "Unknown",
"networkPorts": 1,
"networkPortSpeedGbps": 1
}
},
{
"machineName": "AI Server",
"ipAddress": "10.0.0.20",
"role": "AI Dev/Inference",
"operatingSystem": "Pop!_OS",
"ports": [
{
"portName": "eth0",
"speedGbps": 1,
"connectedTo": {
"device": "Gigabit Switch",
"port": "9"
}
}
],
"software": {
"vms": [
{ "name": "Ollama", "role": "LLM Inference", "ipAddress": "10.0.0.21" },
{ "name": "Bot Training", "role": "AI Training", "ipAddress": "10.0.0.22" }
]
},
"hardware": {
"cpu": "AMD Ryzen 5 3600",
"ram": "32GB",
"networkPorts": 1,
"networkPortSpeedGbps": 1,
"gpu": "Gigabyte GeForce GTX 1080"
}
},
{
"machineName": "Immich Mini PC",
"ipAddress": "10.0.0.30",
"role": "Photo Server",
"operatingSystem": "Linux",
"ports": [
{
"portName": "eth0",
"speedGbps": 1,
"connectedTo": {
"device": "Gigabit Switch",
"port": "10"
}
}
],
"software": { "vms": [] },
"hardware": {
"cpu": "Unknown",
"ram": "Unknown",
"networkPorts": 1,
"networkPortSpeedGbps": 1
}
}
],
"devices": [
{
"name": "Gigabit Switch",
"ipAddress": "unknown",
"type": "Network Switch",
"notes": "Main 24-port switch. Wireless access point uplinks through this switch.",
"ports": [
{
"portName": "1",
"speedGbps": 1,
"connectedTo": {
"device": "ProxRouter",
"port": "eth0"
}
},
{
"portName": "2",
"speedGbps": 1,
"connectedTo": {
"device": "ProxRouter",
"port": "eth2"
}
},
{
"portName": "3",
"speedGbps": 1,
"connectedTo": {
"device": "Asustor NAS",
"port": "eth0"
}
},
{
"portName": "4",
"speedGbps": 1,
"connectedTo": {
"device": "Home Assistant Green",
"port": "eth0"
}
},
{
"portName": "5",
"speedGbps": 1,
"connectedTo": {
"device": "Plex Server",
"port": "eth0"
}
},
{
"portName": "6",
"speedGbps": 1,
"connectedTo": {
"device": "Win11 N100",
"port": "eth0"
}
},
{
"portName": "7",
"speedGbps": 1,
"connectedTo": {
"device": "Win11 Backblaze NAS",
"port": "eth0"
}
},
{
"portName": "8",
"speedGbps": 1,
"connectedTo": {
"device": "TrueNAS Host",
"port": "eth0"
}
},
{
"portName": "9",
"speedGbps": 1,
"connectedTo": {
"device": "AI Server",
"port": "eth0"
}
},
{
"portName": "10",
"speedGbps": 1,
"connectedTo": {
"device": "Immich Mini PC",
"port": "eth0"
}
},
{
"portName": "11",
"speedGbps": 1,
"connectedTo": {
"device": "HDHomeRun",
"port": "eth0"
}
},
{
"portName": "12",
"speedGbps": 1
},
{
"portName": "13",
"speedGbps": 1
},
{
"portName": "14",
"speedGbps": 1
},
{
"portName": "15",
"speedGbps": 1
},
{
"portName": "16",
"speedGbps": 1
},
{
"portName": "17",
"speedGbps": 1
},
{
"portName": "18",
"speedGbps": 1
},
{
"portName": "19",
"speedGbps": 1
},
{
"portName": "20",
"speedGbps": 1
},
{
"portName": "21",
"speedGbps": 1
},
{
"portName": "22",
"speedGbps": 1
},
{
"portName": "23",
"speedGbps": 1
},
{
"portName": "24",
"speedGbps": 1
}
]
},
{
"name": "WAN Uplink",
"ipAddress": "unknown",
"type": "ISP/WAN",
"notes": "Upstream internet link (modem/ONT/ISP path).",
"ports": [
{
"portName": "wan",
"speedGbps": 1,
"connectedTo": {
"device": "ProxRouter",
"port": "eth3"
}
}
]
},
{
"name": "HDHomeRun",
"ipAddress": "10.0.0.2",
"type": "TV Tuner",
"ports": [
{
"portName": "eth0",
"speedGbps": 1,
"connectedTo": {
"device": "Gigabit Switch",
"port": "11"
}
}
]
},
{
"name": "3DS",
"ipAddress": "10.0.0.17",
"type": "Handheld Console",
"notes": "Wi-Fi only."
},
{
"name": "2DS",
"ipAddress": "10.0.0.18",
"type": "Handheld Console",
"notes": "Wi-Fi only."
},
{
"name": "Nintendo Switch",
"ipAddress": "10.0.0.19",
"type": "Gaming Console",
"notes": "Wi-Fi only."
},
{
"name": "NanoKVM Lite",
"ipAddress": "10.0.0.26",
"type": "KVM Device",
"notes": "Port link not yet modeled."
},
{
"name": "UPS Pi",
"ipAddress": "10.0.0.27",
"type": "Power Monitoring Device",
"notes": "Port link not yet modeled."
},
{
"name": "Waveshare",
"ipAddress": "10.0.0.28",
"type": "Peripheral Device",
"notes": "Port link not yet modeled."
}
]
}