Files
mek-tech-consulting/arch-editor.ejs
T

954 lines
64 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="nl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title><%= diagram ? diagram.name : 'Nieuw Diagram' %> — Mek-Tech Architectuur</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/css/style.css">
<link rel="stylesheet" href="/css/neon.css">
<link rel="stylesheet" href="/css/dynamic.css">
<link rel="stylesheet" href="/css/arch-editor.css">
<link rel="icon" href="/img/favicon.svg" type="image/svg+xml">
<script src="https://unpkg.com/lucide@latest"></script>
</head>
<body class="arch-editor<%= (typeof embed !== 'undefined' && embed) ? ' embed-mode' : '' %><%= (typeof rack !== 'undefined' && rack && !(typeof embed !== 'undefined' && embed)) ? ' has-rack-ws-nav' : '' %>">
<% if (typeof rack !== 'undefined' && rack && !(typeof embed !== 'undefined' && embed)) { %>
<%- include('partials/rack-workspace-nav', { rack, flowDiagram, landscapeDiagram, workspaceTab: typeof workspaceTab !== 'undefined' ? workspaceTab : 'flow' }) %>
<% } %>
<header class="arch-topbar">
<a href="/architecture" class="brand">
<img src="/img/logo.svg" alt="Mek-Tech">
<span>Architectuur Designer<em>v2</em></span>
</a>
<div class="arch-topbar-actions">
<a href="/architecture"><i data-lucide="layers" style="width:14px;height:14px"></i> Diagrammen</a>
<a href="/"><i data-lucide="layout-dashboard" style="width:14px;height:14px"></i> Dashboard</a>
<a href="/auth/logout" style="color:#ff6b8a"><i data-lucide="log-out" style="width:14px;height:14px"></i></a>
</div>
</header>
<% if (typeof rack !== 'undefined' && rack) { %>
<div class="rack-context-bar" style="background:linear-gradient(90deg,rgba(0,212,255,.12),rgba(168,85,247,.08));border-bottom:1px solid rgba(0,212,255,.25);padding:.55rem 1rem;display:flex;align-items:center;justify-content:space-between;gap:1rem;flex-wrap:wrap">
<div style="font-size:.75rem;color:#fff">
<strong style="color:var(--accent)">Rack:</strong> <%= rack.name %> · <%= rack.client_name %>
<span style="color:rgba(255,255,255,.7);margin-left:.5rem"><%= (typeof rackDevices !== 'undefined' ? rackDevices.length : 0) %> devices gekoppeld</span>
</div>
<div style="display:flex;gap:.35rem;flex-wrap:wrap">
<a href="/racks/<%= rack.id %>" class="btn btn-pri" style="font-size:.65rem;padding:.35rem .75rem">← Terug naar Rack</a>
<a href="/racks/<%= rack.id %>/studio" class="btn btn-sec" style="font-size:.65rem;padding:.35rem .75rem">Studio</a>
<a href="/racks/<%= rack.id %>/report" class="btn btn-sec" style="font-size:.65rem;padding:.35rem .75rem">Rapport</a>
</div>
</div>
<% } %>
<div class="editor-layout">
<div class="editor-sidebar">
<div class="palette-header">
<h3>Componenten</h3>
<div class="palette-search"><input id="paletteSearch" placeholder="Zoek component..." oninput="filterPalette(this.value)"></div>
<div class="palette-cats" id="paletteCats">
<button class="pc-btn active" data-cat="" onclick="filterPaletteCat('')">Alles</button>
<button class="pc-btn" data-cat="Data Bronnen" onclick="filterPaletteCat(this.dataset.cat)">Bron</button>
<button class="pc-btn" data-cat="Ingest" onclick="filterPaletteCat(this.dataset.cat)">Pipeline</button>
<button class="pc-btn" data-cat="Opslag" onclick="filterPaletteCat(this.dataset.cat)">Storage</button>
<button class="pc-btn" data-cat="Microservices" onclick="filterPaletteCat(this.dataset.cat)">Apps</button>
<button class="pc-btn" data-cat="API" onclick="filterPaletteCat(this.dataset.cat)">API</button>
<button class="pc-btn" data-cat="Databases" onclick="filterPaletteCat(this.dataset.cat)">DB</button>
<button class="pc-btn" data-cat="CI/CD" onclick="filterPaletteCat(this.dataset.cat)">DevOps</button>
<button class="pc-btn" data-cat="Observability" onclick="filterPaletteCat(this.dataset.cat)">Observe</button>
</div>
<% if (typeof rack !== 'undefined' && rack) { %>
<div class="rack-flow-filters" style="margin-top:.55rem;padding-top:.55rem;border-top:1px solid var(--border)">
<div style="font-size:.58rem;color:var(--text-muted);text-transform:uppercase;letter-spacing:.04em;margin-bottom:.35rem;font-weight:700">Flow filters</div>
<div class="palette-cats" style="margin-bottom:.4rem">
<button type="button" class="pc-btn layer-btn active" data-layer="" onclick="filterPaletteLayer('')">Alle lagen</button>
<button type="button" class="pc-btn layer-btn" data-layer="data" onclick="filterPaletteLayer('data')">Data</button>
<button type="button" class="pc-btn layer-btn" data-layer="app" onclick="filterPaletteLayer('app')">App</button>
<button type="button" class="pc-btn layer-btn" data-layer="integration" onclick="filterPaletteLayer('integration')">Integratie</button>
<button type="button" class="pc-btn layer-btn" data-layer="infra" onclick="filterPaletteLayer('infra')">Infra</button>
</div>
<select id="flowProtocolDefault" style="width:100%;padding:.3rem .45rem;font-size:.68rem;background:var(--bg);border:1px solid var(--border);border-radius:6px;color:var(--text);margin-bottom:.4rem" onchange="setDefaultFlowProtocol(this.value)">
<option value="">Standaard protocol: geen</option>
<option value="sync">Sync (REST/RPC)</option>
<option value="async">Async (message)</option>
<option value="batch">Batch / ETL</option>
<option value="stream">Stream / realtime</option>
<option value="event">Event-driven</option>
</select>
<button type="button" class="btn btn-sec" style="width:100%;font-size:.65rem;padding:.35rem" onclick="importRackDevices()">
<i data-lucide="download" style="width:12px;height:12px"></i> Import rack devices (<%= typeof rackDevices !== 'undefined' ? rackDevices.length : 0 %>)
</button>
</div>
<% } %>
</div>
<div class="palette-scroll" id="paletteScroll">
<div class="cat-label">Data Bronnen</div>
<div class="palette-item" draggable="true" data-type="source-db"><span class="p-icon" style="background:#d2992233;color:#d29922">DB</span> Database</div>
<div class="palette-item" draggable="true" data-type="source-api"><span class="p-icon" style="background:#d2992233;color:#d29922">API</span> API Service</div>
<div class="palette-item" draggable="true" data-type="source-iot"><span class="p-icon" style="background:#d2992233;color:#d29922">IoT</span> IoT Sensor</div>
<div class="palette-item" draggable="true" data-type="source-stream"><span class="p-icon" style="background:#d2992233;color:#d29922">ST</span> Streaming</div>
<div class="palette-item" draggable="true" data-type="source-sap"><span class="p-icon" style="background:#0070d133;color:#0070d1">SAP</span> SAP HANA</div>
<div class="palette-item" draggable="true" data-type="source-oracle"><span class="p-icon" style="background:#f8000033;color:#f80000">OR</span> Oracle DB</div>
<div class="cat-label">Ingest & Pipeline</div>
<div class="palette-item" draggable="true" data-type="ingest-kafka"><span class="p-icon" style="background:#58a6ff33;color:#00d4ff">K</span> Kafka</div>
<div class="palette-item" draggable="true" data-type="ingest-airbyte"><span class="p-icon" style="background:#58a6ff33;color:#00d4ff">AB</span> Airbyte</div>
<div class="palette-item" draggable="true" data-type="ingest-flink"><span class="p-icon" style="background:#58a6ff33;color:#00d4ff">SF</span> Spark/Flink</div>
<div class="palette-item" draggable="true" data-type="ingest-cdc"><span class="p-icon" style="background:#58a6ff33;color:#00d4ff">CDC</span> Debezium/CDC</div>
<div class="palette-item" draggable="true" data-type="ingest-nifi"><span class="p-icon" style="background:#58a6ff33;color:#00d4ff">NF</span> NiFi</div>
<div class="palette-item" draggable="true" data-type="ingest-dms"><span class="p-icon" style="background:#58a6ff33;color:#00d4ff">DMS</span> AWS DMS</div>
<div class="cat-label">Opslag & Storage</div>
<div class="palette-item" draggable="true" data-type="storage-minio"><span class="p-icon" style="background:#3fb95033;color:#3fb950">M</span> MinIO / S3</div>
<div class="palette-item" draggable="true" data-type="storage-iceberg"><span class="p-icon" style="background:#3fb95033;color:#3fb950">IC</span> Iceberg</div>
<div class="palette-item" draggable="true" data-type="storage-warehouse"><span class="p-icon" style="background:#3fb95033;color:#3fb950">DW</span> Data Warehouse</div>
<div class="palette-item" draggable="true" data-type="storage-lake"><span class="p-icon" style="background:#3fb95033;color:#3fb950">LH</span> Data Lakehouse</div>
<div class="palette-item" draggable="true" data-type="storage-nimble"><span class="p-icon" style="background:#ff7b0033;color:#ff7b00">HN</span> HPE Nimble</div>
<div class="palette-item" draggable="true" data-type="storage-3par"><span class="p-icon" style="background:#ff7b0033;color:#ff7b00">3P</span> HPE 3PAR</div>
<div class="palette-item" draggable="true" data-type="storage-powerstore"><span class="p-icon" style="background:#0076a833;color:#0076a8">PS</span> Dell PowerStore</div>
<div class="palette-item" draggable="true" data-type="storage-powerscale"><span class="p-icon" style="background:#0076a833;color:#0076a8">PSC</span> Dell PowerScale</div>
<div class="palette-item" draggable="true" data-type="storage-netapp"><span class="p-icon" style="background:#00a1df33;color:#00a1df">NA</span> NetApp AFF/FAS</div>
<div class="palette-item" draggable="true" data-type="storage-pure"><span class="p-icon" style="background:#4a90d933;color:#4a90d9">PU</span> Pure Storage</div>
<div class="cat-label">Compute / Servers</div>
<div class="palette-item" draggable="true" data-type="hw-hpe-dl"><span class="p-icon" style="background:#ff7b0033;color:#ff7b00">HP</span> HPE ProLiant DL</div>
<div class="palette-item" draggable="true" data-type="hw-hpe-ml"><span class="p-icon" style="background:#ff7b0033;color:#ff7b00">HPM</span> HPE ProLiant ML</div>
<div class="palette-item" draggable="true" data-type="hw-hpe-synergy"><span class="p-icon" style="background:#ff7b0033;color:#ff7b00">SYN</span> HPE Synergy</div>
<div class="palette-item" draggable="true" data-type="hw-dell-pe"><span class="p-icon" style="background:#0076a833;color:#0076a8">DE</span> Dell PowerEdge</div>
<div class="palette-item" draggable="true" data-type="hw-dell-mx"><span class="p-icon" style="background:#0076a833;color:#0076a8">MX</span> Dell PowerEdge MX</div>
<div class="palette-item" draggable="true" data-type="hw-lenovo-ts"><span class="p-icon" style="background:#e41f3333;color:#e41f33">LE</span> Lenovo ThinkSystem</div>
<div class="palette-item" draggable="true" data-type="hw-lenovo-ta"><span class="p-icon" style="background:#e41f3333;color:#e41f33">TA</span> Lenovo ThinkAgile</div>
<div class="palette-item" draggable="true" data-type="hw-supermicro"><span class="p-icon" style="background:#e6a81733;color:#e6a817">SM</span> Supermicro SYS</div>
<div class="palette-item" draggable="true" data-type="hw-cisco-ucs"><span class="p-icon" style="background:#00529a33;color:#00529a">UCS</span> Cisco UCS</div>
<div class="palette-item" draggable="true" data-type="compute-vmware"><span class="p-icon" style="background:#a855f733;color:#a855f7">VM</span> VMware vSphere</div>
<div class="cat-label">Netwerk</div>
<div class="palette-item" draggable="true" data-type="net-catalyst"><span class="p-icon" style="background:#34d39933;color:#34d399">CAT</span> Cisco Catalyst</div>
<div class="palette-item" draggable="true" data-type="net-nexus"><span class="p-icon" style="background:#34d39933;color:#34d399">NX</span> Cisco Nexus</div>
<div class="palette-item" draggable="true" data-type="net-junper-ex"><span class="p-icon" style="background:#34d39933;color:#34d399">JEX</span> Juniper EX</div>
<div class="palette-item" draggable="true" data-type="net-junper-qfx"><span class="p-icon" style="background:#34d39933;color:#34d399">JQ</span> Juniper QFX</div>
<div class="palette-item" draggable="true" data-type="net-junper-srx"><span class="p-icon" style="background:#34d39933;color:#34d399">SRX</span> Juniper SRX</div>
<div class="palette-item" draggable="true" data-type="net-arista"><span class="p-icon" style="background:#34d39933;color:#34d399">AR</span> Arista EOS</div>
<div class="palette-item" draggable="true" data-type="net-fortinet"><span class="p-icon" style="background:#34d39933;color:#34d399">FG</span> Fortinet FortiGate</div>
<div class="palette-item" draggable="true" data-type="net-f5"><span class="p-icon" style="background:#34d39933;color:#34d399">F5</span> F5 BIG-IP</div>
<div class="palette-item" draggable="true" data-type="net-paloalto"><span class="p-icon" style="background:#34d39933;color:#34d399">PA</span> Palo Alto</div>
<div class="cat-label">Compute / Data Platform</div>
<div class="palette-item" draggable="true" data-type="compute-spark"><span class="p-icon" style="background:#a855f733;color:#a855f7">SP</span> Apache Spark</div>
<div class="palette-item" draggable="true" data-type="compute-trino"><span class="p-icon" style="background:#a855f733;color:#a855f7">TR</span> Trino / Presto</div>
<div class="palette-item" draggable="true" data-type="compute-k8s"><span class="p-icon" style="background:#a855f733;color:#a855f7">K8</span> Kubernetes</div>
<div class="palette-item" draggable="true" data-type="compute-dbt"><span class="p-icon" style="background:#a855f733;color:#a855f7">DBT</span> dbt</div>
<div class="palette-item" draggable="true" data-type="compute-hadoop"><span class="p-icon" style="background:#a855f733;color:#a855f7">HD</span> Hadoop Cluster</div>
<div class="palette-item" draggable="true" data-type="compute-sap-hana"><span class="p-icon" style="background:#0070d133;color:#0070d1">H</span> SAP HANA Platform</div>
<div class="cat-label">AI / ML</div>
<div class="palette-item" draggable="true" data-type="ai-ollama"><span class="p-icon" style="background:#fb718533;color:#fb7185">O</span> Ollama / vLLM</div>
<div class="palette-item" draggable="true" data-type="ai-mlflow"><span class="p-icon" style="background:#fb718533;color:#fb7185">MF</span> MLflow</div>
<div class="palette-item" draggable="true" data-type="ai-vector"><span class="p-icon" style="background:#fb718533;color:#fb7185">VS</span> Vector Store</div>
<div class="palette-item" draggable="true" data-type="ai-model"><span class="p-icon" style="background:#fb718533;color:#fb7185">MS</span> Model Serving</div>
<div class="palette-item" draggable="true" data-type="ai-nvidia"><span class="p-icon" style="background:#76b90033;color:#76b900">NV</span> NVIDIA DGX</div>
<div class="cat-label">Microservices & Apps</div>
<div class="palette-item" draggable="true" data-type="app-nodejs"><span class="p-icon" style="background:#22c55e33;color:#22c55e">NJS</span> Node.js App</div>
<div class="palette-item" draggable="true" data-type="app-python"><span class="p-icon" style="background:#22c55e33;color:#22c55e">PY</span> Python App</div>
<div class="palette-item" draggable="true" data-type="app-java"><span class="p-icon" style="background:#22c55e33;color:#22c55e">JV</span> Java / Spring Boot</div>
<div class="palette-item" draggable="true" data-type="app-go"><span class="p-icon" style="background:#22c55e33;color:#22c55e">GO</span> Golang App</div>
<div class="palette-item" draggable="true" data-type="app-dotnet"><span class="p-icon" style="background:#22c55e33;color:#22c55e">DN</span> .NET App</div>
<div class="palette-item" draggable="true" data-type="app-react"><span class="p-icon" style="background:#22c55e33;color:#22c55e">RE</span> React / Frontend</div>
<div class="palette-item" draggable="true" data-type="app-microservice"><span class="p-icon" style="background:#22c55e33;color:#22c55e">μS</span> Microservice</div>
<div class="palette-item" draggable="true" data-type="app-container"><span class="p-icon" style="background:#22c55e33;color:#22c55e">CT</span> Container / Docker</div>
<div class="palette-item" draggable="true" data-type="app-serverless"><span class="p-icon" style="background:#22c55e33;color:#22c55e">λ</span> Serverless Function</div>
<div class="cat-label">API & Gateway</div>
<div class="palette-item" draggable="true" data-type="api-gateway"><span class="p-icon" style="background:#38bdf833;color:#38bdf8">GW</span> API Gateway</div>
<div class="palette-item" draggable="true" data-type="api-rest"><span class="p-icon" style="background:#38bdf833;color:#38bdf8">REST</span> REST API</div>
<div class="palette-item" draggable="true" data-type="api-graphql"><span class="p-icon" style="background:#38bdf833;color:#38bdf8">GQL</span> GraphQL API</div>
<div class="palette-item" draggable="true" data-type="api-grpc"><span class="p-icon" style="background:#38bdf833;color:#38bdf8">gRPC</span> gRPC Service</div>
<div class="palette-item" draggable="true" data-type="api-ingress"><span class="p-icon" style="background:#38bdf833;color:#38bdf8">IN</span> K8s Ingress</div>
<div class="palette-item" draggable="true" data-type="api-reverse-proxy"><span class="p-icon" style="background:#38bdf833;color:#38bdf8">RP</span> Reverse Proxy</div>
<div class="palette-item" draggable="true" data-type="api-service-mesh"><span class="p-icon" style="background:#38bdf833;color:#38bdf8">SM</span> Service Mesh</div>
<div class="cat-label">Messaging & Queue</div>
<div class="palette-item" draggable="true" data-type="queue-kafka"><span class="p-icon" style="background:#38bdf833;color:#38bdf8">KQ</span> Kafka / Confluent</div>
<div class="palette-item" draggable="true" data-type="queue-rabbit"><span class="p-icon" style="background:#38bdf833;color:#38bdf8">RM</span> RabbitMQ</div>
<div class="palette-item" draggable="true" data-type="queue-nats"><span class="p-icon" style="background:#38bdf833;color:#38bdf8">N</span> NATS / Pulsar</div>
<div class="palette-item" draggable="true" data-type="queue-redis"><span class="p-icon" style="background:#38bdf833;color:#38bdf8">RD</span> Redis Pub/Sub</div>
<div class="palette-item" draggable="true" data-type="queue-mqtt"><span class="p-icon" style="background:#38bdf833;color:#38bdf8">MQ</span> MQTT Broker</div>
<div class="palette-item" draggable="true" data-type="queue-sqs"><span class="p-icon" style="background:#38bdf833;color:#38bdf8">SQS</span> AWS SQS / SNS</div>
<div class="cat-label">Databases & Cache</div>
<div class="palette-item" draggable="true" data-type="db-postgres"><span class="p-icon" style="background:#d2992233;color:#d29922">PG</span> PostgreSQL</div>
<div class="palette-item" draggable="true" data-type="db-mysql"><span class="p-icon" style="background:#d2992233;color:#d29922">MY</span> MySQL / MariaDB</div>
<div class="palette-item" draggable="true" data-type="db-mongodb"><span class="p-icon" style="background:#d2992233;color:#d29922">MD</span> MongoDB</div>
<div class="palette-item" draggable="true" data-type="db-redis"><span class="p-icon" style="background:#d2992233;color:#d29922">RE</span> Redis Cache</div>
<div class="palette-item" draggable="true" data-type="db-elastic"><span class="p-icon" style="background:#d2992233;color:#d29922">ES</span> Elasticsearch</div>
<div class="palette-item" draggable="true" data-type="db-cassandra"><span class="p-icon" style="background:#d2992233;color:#d29922">CA</span> Cassandra / Scylla</div>
<div class="palette-item" draggable="true" data-type="db-clickhouse"><span class="p-icon" style="background:#d2992233;color:#d29922">CH</span> ClickHouse</div>
<div class="palette-item" draggable="true" data-type="db-dynamodb"><span class="p-icon" style="background:#d2992233;color:#d29922">DY</span> DynamoDB / Cosmos</div>
<div class="cat-label">CI/CD & DevOps</div>
<div class="palette-item" draggable="true" data-type="devops-github"><span class="p-icon" style="background:#a855f733;color:#a855f7">GH</span> GitHub Actions</div>
<div class="palette-item" draggable="true" data-type="devops-gitlab"><span class="p-icon" style="background:#a855f733;color:#a855f7">GL</span> GitLab CI/CD</div>
<div class="palette-item" draggable="true" data-type="devops-jenkins"><span class="p-icon" style="background:#a855f733;color:#a855f7">JK</span> Jenkins</div>
<div class="palette-item" draggable="true" data-type="devops-argocd"><span class="p-icon" style="background:#a855f733;color:#a855f7">AR</span> ArgoCD</div>
<div class="palette-item" draggable="true" data-type="devops-terraform"><span class="p-icon" style="background:#a855f733;color:#a855f7">TF</span> Terraform</div>
<div class="palette-item" draggable="true" data-type="devops-docker"><span class="p-icon" style="background:#a855f733;color:#a855f7">DK</span> Docker Registry</div>
<div class="palette-item" draggable="true" data-type="devops-helm"><span class="p-icon" style="background:#a855f733;color:#a855f7">HL</span> Helm Charts</div>
<div class="cat-label">Observability</div>
<div class="palette-item" draggable="true" data-type="obs-prometheus"><span class="p-icon" style="background:#f472b633;color:#f472b6">PR</span> Prometheus</div>
<div class="palette-item" draggable="true" data-type="obs-grafana"><span class="p-icon" style="background:#f472b633;color:#f472b6">GF</span> Grafana</div>
<div class="palette-item" draggable="true" data-type="obs-loki"><span class="p-icon" style="background:#f472b633;color:#f472b6">LO</span> Loki / Logs</div>
<div class="palette-item" draggable="true" data-type="obs-tempo"><span class="p-icon" style="background:#f472b633;color:#f472b6">TP</span> Tempo / Tracing</div>
<div class="palette-item" draggable="true" data-type="obs-datadog"><span class="p-icon" style="background:#f472b633;color:#f472b6">DD</span> Datadog</div>
<div class="palette-item" draggable="true" data-type="obs-opentelemetry"><span class="p-icon" style="background:#f472b633;color:#f472b6">OT</span> OpenTelemetry</div>
<div class="palette-item" draggable="true" data-type="obs-elk"><span class="p-icon" style="background:#f472b633;color:#f472b6">ELK</span> ELK Stack</div>
<div class="cat-label">Security</div>
<div class="palette-item" draggable="true" data-type="security-iam"><span class="p-icon" style="background:#f472b633;color:#f472b6">IAM</span> IAM / Auth</div>
<div class="palette-item" draggable="true" data-type="security-vault"><span class="p-icon" style="background:#f472b633;color:#f472b6">V</span> HashiCorp Vault</div>
<div class="palette-item" draggable="true" data-type="security-grafana"><span class="p-icon" style="background:#f472b633;color:#f472b6">G</span> Grafana</div>
<div class="cat-label">Infrastructure</div>
<div class="palette-item" draggable="true" data-type="infra-rack"><span class="p-icon" style="background:#6e768133;color:#6e7681">RK</span> Server Rack</div>
<div class="palette-item" draggable="true" data-type="infra-apc-ups"><span class="p-icon" style="background:#6e768133;color:#6e7681">UPS</span> APC UPS</div>
<div class="palette-item" draggable="true" data-type="infra-pdu"><span class="p-icon" style="background:#6e768133;color:#6e7681">PDU</span> PDU / Stroom</div>
<div class="palette-item" draggable="true" data-type="infra-cooling"><span class="p-icon" style="background:#6e768133;color:#6e7681">AC</span> Koeling / AC</div>
<div class="palette-item" draggable="true" data-type="infra-patch-panel"><span class="p-icon" style="background:#6e768133;color:#6e7681">PP</span> Patch Panel</div>
</div>
<div class="node-count" id="nodeCount">0 componenten · 0 connecties</div>
</div>
<div class="editor-main">
<div class="editor-toolbar">
<div class="toolbar-group meta">
<input class="ed-name" id="diagramName" placeholder="Diagram naam..." value="<%= diagram ? diagram.name : '' %>">
<select id="diagramType">
<option value="data_architecture" <%= (diagram && diagram.diagram_type === 'data_architecture') || (!diagram && defaultDiagramType === 'data_architecture') ? 'selected' : '' %>>Data Architectuur</option>
<option value="network_topology" <%= (diagram && diagram.diagram_type === 'network_topology') || (!diagram && defaultDiagramType === 'network_topology') ? 'selected' : '' %>>Netwerk Topologie</option>
<option value="system_architecture" <%= (diagram && diagram.diagram_type === 'system_architecture') || (!diagram && defaultDiagramType === 'system_architecture') ? 'selected' : '' %>>Systeem Architectuur</option>
<option value="application_flow" <%= (diagram && diagram.diagram_type === 'application_flow') || (!diagram && defaultDiagramType === 'application_flow') ? 'selected' : '' %>>Applicatie Flow</option>
<option value="application_landscape" <%= (diagram && diagram.diagram_type === 'application_landscape') || (!diagram && defaultDiagramType === 'application_landscape') ? 'selected' : '' %>>Applicatie Landschap</option>
</select>
<select id="diagramClient">
<option value="">Cliënt...</option>
<% clients.forEach(function(c) { %>
<option value="<%= c.id %>" <%= (diagram && diagram.client_id == c.id) || clientId == c.id ? 'selected' : '' %>><%= c.name %></option>
<% }) %>
</select>
</div>
<div class="tb-sep"></div>
<div class="toolbar-group">
<button class="btn btn-sec btn-icon" onclick="undoAction()" title="Ongedaan maken">↩</button>
<button class="btn btn-sec btn-icon" onclick="redoAction()" title="Opnieuw">↪</button>
</div>
<div class="tb-sep"></div>
<div class="toolbar-group flow-toolbar" id="defaultFlowToolbar">
<span class="flow-label">Nieuwe lijnen:</span>
<button type="button" class="flow-chip" data-flow="off" onclick="setDefaultFlowMode('off')" title="Statische lijnen">Uit</button>
<button type="button" class="flow-chip" data-flow="flow" onclick="setDefaultFlowMode('flow')" title="Stromende stippellijn">Flow</button>
<button type="button" class="flow-chip" data-flow="pulse" onclick="setDefaultFlowMode('pulse')" title="Pulserende gloed">Puls</button>
<button type="button" class="flow-chip active" data-flow="both" onclick="setDefaultFlowMode('both')" title="Flow + puls">Beide</button>
</div>
<div class="tb-sep"></div>
<div class="toolbar-group">
<button class="btn btn-pri" onclick="saveDiagram()">Opslaan</button>
<button class="btn btn-sec" onclick="exportJSON()">JSON</button>
<button class="btn btn-sec btn-icon" onclick="fitToScreen()" title="Passend maken">⊞</button>
<button class="btn btn-sec btn-icon" onclick="toggleGrid()" title="Raster">⊟</button>
<button class="btn btn-sec btn-danger" onclick="clearCanvas()" title="Wissen">× Wis</button>
</div>
<div class="tb-sep"></div>
<a href="<%= (typeof rack !== 'undefined' && rack) ? '/racks/' + rack.id + '/report' : '/architecture/report/' + (diagram ? diagram.id : 0) %>" class="btn btn-sec" title="Rapport genereren">📄 Rapport</a>
<span class="status-msg" id="statusMsg"></span>
</div>
<div class="canvas-wrap" id="canvasWrap">
<div class="canvas-hint" id="canvasHint">
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><path d="M17.5 14v7M14 17.5h7"/></svg>
<p>Sleep componenten hierheen</p>
<small>Verbind nodes via de poorten aan de zijkanten</small>
</div>
<div class="viewport" id="viewport">
<svg class="connections" id="svgConnections">
<defs>
<marker id="arrowhead" markerWidth="10" markerHeight="7" refX="10" refY="3.5" orient="auto"><polygon points="0 0,10 3.5,0 7" fill="var(--accent)"/></marker>
<marker id="arrowhead-glow" markerWidth="14" markerHeight="10" refX="12" refY="5" orient="auto"><polygon points="0 0,12 5,0 10" fill="#00d4ff"/></marker>
</defs>
</svg>
<div id="canvas" style="position:absolute;top:0;left:0;width:100%;height:100%"></div>
</div>
<div class="zoom-indicator" id="zoomIndicator">100%</div>
</div>
</div>
<div class="props-panel" id="propsPanel">
<h3 id="propsTitle">Eigenschappen</h3>
<div class="props-body">
<div id="propsNode">
<div class="pg"><label>Label</label><input id="pLabel" onchange="updateProp('label',this.value)"></div>
<div class="pg"><label>Subtitel</label><input id="pSub" onchange="updateProp('sub',this.value)"></div>
<div class="pg"><label>IP Adres</label><input id="pIp" onchange="updateProp('ip',this.value)" placeholder="10.0.0.1/24"></div>
<div class="props-divider"></div>
<div class="pg"><label>Vendor</label><select id="pVendor" onchange="updateProp('vendor',this.value)"></select></div>
<div class="pg"><label>Model</label><input id="pModel" onchange="updateProp('model',this.value)" placeholder="ProLiant DL380 Gen11"></div>
<div class="pg"><label>Type</label><select id="pType" onchange="updateProp('type',this.value)"></select></div>
<div class="pg"><label>Kleur</label><select id="pColor" onchange="updateProp('css',this.value)">
<option value="node-source">Oranje (Bron)</option><option value="node-ingest">Blauw (Ingest)</option>
<option value="node-storage">Groen (Opslag)</option><option value="node-compute">Paars (Compute)</option>
<option value="node-ai">Rood (AI/ML)</option><option value="node-queue">Cyaan (Queue)</option>
<option value="node-network">Mint (Netwerk)</option><option value="node-security">Roze (Security)</option>
<option value="node-vendor">Oranje (Vendor)</option><option value="node-sap">Blauw (SAP)</option>
<option value="node-db">Rood (Database)</option><option value="node-hw">Oranje (Hardware)</option>
<option value="node-hybrid">Cyaan (Hybrid)</option>
</select></div>
<div class="pg"><label>Breedte</label><input id="pWidth" type="number" min="100" max="500" onchange="updateProp('w',+this.value)"></div>
<div class="props-divider"></div>
<div class="pg"><label>Specs / Config</label><textarea id="pSpecs" onchange="updateProp('specs',this.value)" placeholder="CPU: Xeon 16C&#10;RAM: 512GB&#10;Opslag: 4x 3.84TB NVMe&#10;Poorten: 4x 25GbE"></textarea></div>
<div class="pg"><label>Notities</label><textarea id="pNotes" onchange="updateProp('notes',this.value)" placeholder="Extra informatie..."></textarea></div>
<div class="props-divider"></div>
<button class="btn-delete-node" onclick="deleteSelected()">Verwijder node</button>
</div>
<div id="propsEdge" style="display:none">
<div class="pg"><label>Connectie</label><div id="pEdgeRoute" class="edge-route-text">—</div></div>
<div class="pg"><label>Flow protocol</label>
<select id="pEdgeProtocol" onchange="setEdgeProtocol(this.value)">
<option value="">—</option>
<option value="sync">Sync</option>
<option value="async">Async</option>
<option value="batch">Batch</option>
<option value="stream">Stream</option>
<option value="event">Event</option>
</select>
</div>
<div class="pg"><label>Animatie</label>
<div class="flow-chips" id="edgeFlowChips">
<button type="button" class="flow-chip" data-flow="off" onclick="setEdgeFlow('off')">Uit</button>
<button type="button" class="flow-chip" data-flow="flow" onclick="setEdgeFlow('flow')">Flow</button>
<button type="button" class="flow-chip" data-flow="pulse" onclick="setEdgeFlow('pulse')">Puls</button>
<button type="button" class="flow-chip" data-flow="both" onclick="setEdgeFlow('both')">Beide</button>
</div>
<p class="flow-hint">Klik een lijn op het canvas om de animatie per connectie aan te passen.</p>
</div>
<div class="props-divider"></div>
<button class="btn-delete-node" onclick="deleteSelectedEdge()">Verwijder connectie</button>
</div>
</div>
</div>
</div>
<div id="diagramMeta" data-id="<%= diagram ? diagram.id : '' %>" data-rack-id="<%= (typeof rack !== 'undefined' && rack) ? rack.id : (diagram && diagram.rack_id ? diagram.rack_id : '') %>" style="display:none"></div>
<script>
const DIAGRAM_ID = '<%= diagram ? diagram.id : '' %>';
const RACK_ID = '<%= (typeof rack !== "undefined" && rack) ? rack.id : (diagram && diagram.rack_id ? diagram.rack_id : "") %>';
const RACK_DEVICES = <%- JSON.stringify(typeof rackDevices !== 'undefined' ? rackDevices : []) %>;
let defaultFlowProtocol = '';
let _paletteLayer = '';
let nodes = [], edges = [], selectedNodeId = null, selectedEdgeIdx = null;
let defaultFlowMode = 'both';
let dragNodeId = null, dragOffX = 0, dragOffY = 0;
let isPanning = false, panStartX, panStartY, panOffX = 0, panOffY = 0;
let connectingFrom = null, tempLine = null, zoomLevel = 1, showGrid = true;
let nodeIdCounter = 0, undoStack = [], redoStack = [], isDirty = false;
const wrap = document.getElementById('canvasWrap'), viewport = document.getElementById('viewport');
const canvas = document.getElementById('canvas'), svg = document.getElementById('svgConnections');
const propsEl = document.getElementById('propsPanel');
const VENDORS = ['','HPE','Dell','Lenovo','Super micro','Cisco','Juniper','Arista','NetApp','Pure Storage','Fortinet','F5','Palo Alto','NVIDIA','APC','Supermicro','ORACLE','Custom'];
const NT = {
'source-db':{l:'Database',s:'OLTP / Bron',c:'node-source',w:140,h:44},
'source-api':{l:'API Service',s:'REST / gRPC',c:'node-source',w:140,h:44},
'source-iot':{l:'IoT Sensor',s:'Device Data',c:'node-source',w:140,h:44},
'source-stream':{l:'Streaming',s:'Real-time Data',c:'node-source',w:140,h:44},
'source-sap':{l:'SAP HANA',s:'Enterprise Data',c:'node-sap',w:140,h:44},
'source-oracle':{l:'Oracle DB',s:'Enterprise DB',c:'node-db',w:140,h:44},
'ingest-kafka':{l:'Kafka',s:'Event Streaming',c:'node-ingest',w:130,h:44},
'ingest-airbyte':{l:'Airbyte',s:'Data Integration',c:'node-ingest',w:130,h:44},
'ingest-flink':{l:'Spark / Flink',s:'Stream Processing',c:'node-ingest',w:140,h:44},
'ingest-cdc':{l:'Debezium / CDC',s:'Change Data Capture',c:'node-ingest',w:150,h:44},
'ingest-nifi':{l:'Apache NiFi',s:'Data Flow',c:'node-ingest',w:130,h:44},
'ingest-dms':{l:'AWS DMS',s:'Migration Service',c:'node-ingest',w:140,h:44},
'storage-minio':{l:'MinIO / S3',s:'Object Storage',c:'node-storage',w:140,h:44},
'storage-iceberg':{l:'Iceberg',s:'Table Format',c:'node-storage',w:130,h:44},
'storage-warehouse':{l:'Data Warehouse',s:'Analytics',c:'node-storage',w:150,h:44},
'storage-lake':{l:'Data Lakehouse',s:'Unified Analytics',c:'node-storage',w:155,h:44},
'storage-nimble':{l:'HPE Nimble',s:'All-Flash Storage',c:'node-vendor',w:150,h:44},
'storage-3par':{l:'HPE 3PAR',s:'Enterprise Storage',c:'node-vendor',w:140,h:44},
'storage-powerstore':{l:'Dell PowerStore',s:'All-Flash',c:'node-hw',w:155,h:44},
'storage-powerscale':{l:'Dell PowerScale',s:'Scale-Out NAS',c:'node-hw',w:160,h:44},
'storage-netapp':{l:'NetApp AFF/FAS',s:'Hybrid Cloud',c:'node-vendor',w:155,h:44},
'storage-pure':{l:'Pure Storage',s:'All-Flash Array',c:'node-vendor',w:155,h:44},
'hw-hpe-dl':{l:'HPE ProLiant DL',s:'Rack Server',c:'node-hw',w:160,h:48},
'hw-hpe-ml':{l:'HPE ProLiant ML',s:'Tower Server',c:'node-hw',w:160,h:48},
'hw-hpe-synergy':{l:'HPE Synergy',s:'Composable Infra',c:'node-hw',w:160,h:48},
'hw-dell-pe':{l:'Dell PowerEdge',s:'Rack Server',c:'node-hw',w:160,h:48},
'hw-dell-mx':{l:'Dell PowerEdge MX',s:'Modular Infra',c:'node-hw',w:170,h:48},
'hw-lenovo-ts':{l:'Lenovo ThinkSystem',s:'Enterprise Server',c:'node-hw',w:170,h:48},
'hw-lenovo-ta':{l:'Lenovo ThinkAgile',s:'HCI Platform',c:'node-hw',w:170,h:48},
'hw-supermicro':{l:'Supermicro SYS',s:'High-Density Server',c:'node-hw',w:160,h:48},
'hw-cisco-ucs':{l:'Cisco UCS',s:'Unified Computing',c:'node-hw',w:155,h:48},
'compute-vmware':{l:'VMware vSphere',s:'Virtualization',c:'node-compute',w:155,h:44},
'net-catalyst':{l:'Cisco Catalyst',s:'Access / Core Switch',c:'node-network',w:155,h:44},
'net-nexus':{l:'Cisco Nexus',s:'Datacenter Switch',c:'node-network',w:155,h:44},
'net-junper-ex':{l:'Juniper EX',s:'Ethernet Switch',c:'node-network',w:150,h:44},
'net-junper-qfx':{l:'Juniper QFX',s:'Spine / Leaf',c:'node-network',w:150,h:44},
'net-junper-srx':{l:'Juniper SRX',s:'Security Gateway',c:'node-network',w:150,h:44},
'net-arista':{l:'Arista EOS',s:'Datacenter Switch',c:'node-network',w:150,h:44},
'net-fortinet':{l:'Fortinet FortiGate',s:'Next-Gen Firewall',c:'node-network',w:165,h:44},
'net-f5':{l:'F5 BIG-IP',s:'Load Balancer / ADC',c:'node-network',w:160,h:44},
'net-paloalto':{l:'Palo Alto',s:'Next-Gen Firewall',c:'node-network',w:155,h:44},
'compute-spark':{l:'Apache Spark',s:'Distributed Compute',c:'node-compute',w:145,h:44},
'compute-trino':{l:'Trino / Presto',s:'SQL Query Engine',c:'node-compute',w:150,h:44},
'compute-k8s':{l:'Kubernetes',s:'Container Orchestr.',c:'node-compute',w:145,h:44},
'compute-dbt':{l:'dbt',s:'Data Transform',c:'node-compute',w:130,h:44},
'compute-hadoop':{l:'Hadoop Cluster',s:'Big Data Platform',c:'node-compute',w:155,h:44},
'compute-sap-hana':{l:'SAP HANA',s:'In-Memory Platform',c:'node-sap',w:155,h:44},
'ai-ollama':{l:'Ollama / vLLM',s:'LLM Serving',c:'node-ai',w:145,h:44},
'ai-mlflow':{l:'MLflow',s:'ML Lifecycle',c:'node-ai',w:140,h:44},
'ai-vector':{l:'Vector Store',s:'Embeddings',c:'node-ai',w:140,h:44},
'ai-model':{l:'Model Serving',s:'Inference',c:'node-ai',w:145,h:44},
'ai-nvidia':{l:'NVIDIA DGX',s:'AI Supercomputer',c:'node-ai',w:150,h:44},
'queue-kafka':{l:'Kafka / Confluent',s:'Message Queue',c:'node-queue',w:150,h:44},
'queue-rabbit':{l:'RabbitMQ',s:'Message Broker',c:'node-queue',w:140,h:44},
'queue-nats':{l:'NATS / Pulsar',s:'Event Bus',c:'node-queue',w:150,h:44},
'security-iam':{l:'IAM / Auth',s:'Identity & Access',c:'node-security',w:140,h:44},
'security-vault':{l:'HashiCorp Vault',s:'Secrets Mgmt',c:'node-security',w:155,h:44},
'security-grafana':{l:'Grafana',s:'Observability',c:'node-security',w:140,h:44},
'infra-rack':{l:'Server Rack',s:'42U Rack Enclosure',c:'node-vendor',w:160,h:52},
'infra-apc-ups':{l:'APC UPS',s:'Uninterruptible Power',c:'node-vendor',w:150,h:44},
'infra-pdu':{l:'PDU',s:'Power Distribution',c:'node-vendor',w:140,h:42},
'infra-cooling':{l:'Koeling / AC',s:'Rack Cooling',c:'node-vendor',w:140,h:42},
'infra-patch-panel':{l:'Patch Panel',s:'48-port Keystone',c:'node-vendor',w:150,h:40},
// Software & Apps
'app-nodejs':{l:'Node.js App',s:'JavaScript Runtime',c:'node-compute',w:145,h:44},
'app-python':{l:'Python App',s:'Python Runtime',c:'node-compute',w:145,h:44},
'app-java':{l:'Java / Spring Boot',s:'JVM Runtime',c:'node-compute',w:155,h:44},
'app-go':{l:'Golang App',s:'Go Runtime',c:'node-compute',w:140,h:44},
'app-dotnet':{l:'.NET App',s:'.NET Runtime',c:'node-compute',w:140,h:44},
'app-react':{l:'React / Frontend',s:'Web Frontend',c:'node-compute',w:150,h:44},
'app-microservice':{l:'Microservice',s:'Service Component',c:'node-compute',w:150,h:44},
'app-container':{l:'Container / Docker',s:'Container Runtime',c:'node-compute',w:160,h:44},
'app-serverless':{l:'Serverless Function',s:'FaaS / Lambda',c:'node-compute',w:160,h:44},
// API & Gateway
'api-gateway':{l:'API Gateway',s:'Traffic Management',c:'node-queue',w:145,h:44},
'api-rest':{l:'REST API',s:'HTTP / JSON',c:'node-queue',w:135,h:44},
'api-graphql':{l:'GraphQL API',s:'Query Language',c:'node-queue',w:145,h:44},
'api-grpc':{l:'gRPC Service',s:'RPC Framework',c:'node-queue',w:145,h:44},
'api-ingress':{l:'K8s Ingress',s:'External Access',c:'node-queue',w:140,h:44},
'api-reverse-proxy':{l:'Reverse Proxy',s:'Load Balancing',c:'node-queue',w:145,h:44},
'api-service-mesh':{l:'Service Mesh',s:'mTLS / Observability',c:'node-queue',w:155,h:44},
// More messaging
'queue-redis':{l:'Redis Pub/Sub',s:'In-Memory Queue',c:'node-queue',w:145,h:44},
'queue-mqtt':{l:'MQTT Broker',s:'IoT Protocol',c:'node-queue',w:140,h:44},
'queue-sqs':{l:'AWS SQS / SNS',s:'Cloud Queue',c:'node-queue',w:145,h:44},
// Databases & Cache
'db-postgres':{l:'PostgreSQL',s:'Relational DB',c:'node-db',w:145,h:44},
'db-mysql':{l:'MySQL / MariaDB',s:'Relational DB',c:'node-db',w:150,h:44},
'db-mongodb':{l:'MongoDB',s:'Document DB',c:'node-db',w:145,h:44},
'db-redis':{l:'Redis Cache',s:'In-Memory Cache',c:'node-db',w:140,h:44},
'db-elastic':{l:'Elasticsearch',s:'Search & Analytics',c:'node-db',w:155,h:44},
'db-cassandra':{l:'Cassandra / Scylla',s:'Wide-Column DB',c:'node-db',w:160,h:44},
'db-clickhouse':{l:'ClickHouse',s:'OLAP Database',c:'node-db',w:145,h:44},
'db-dynamodb':{l:'DynamoDB / Cosmos',s:'NoSQL Cloud DB',c:'node-db',w:160,h:44},
// CI/CD & DevOps
'devops-github':{l:'GitHub Actions',s:'CI/CD Pipeline',c:'node-queue',w:145,h:44},
'devops-gitlab':{l:'GitLab CI/CD',s:'CI/CD Pipeline',c:'node-queue',w:145,h:44},
'devops-jenkins':{l:'Jenkins',s:'Automation Server',c:'node-queue',w:135,h:44},
'devops-argocd':{l:'ArgoCD',s:'GitOps Deployment',c:'node-queue',w:150,h:44},
'devops-terraform':{l:'Terraform',s:'Infrastructure as Code',c:'node-queue',w:160,h:44},
'devops-docker':{l:'Docker Registry',s:'Container Registry',c:'node-queue',w:155,h:44},
'devops-helm':{l:'Helm Charts',s:'K8s Package Manager',c:'node-queue',w:155,h:44},
// Observability
'obs-prometheus':{l:'Prometheus',s:'Metrics Collection',c:'node-security',w:150,h:44},
'obs-grafana':{l:'Grafana',s:'Dashboard & Viz',c:'node-security',w:145,h:44},
'obs-loki':{l:'Loki / Logs',s:'Log Aggregation',c:'node-security',w:145,h:44},
'obs-tempo':{l:'Tempo / Tracing',s:'Distributed Tracing',c:'node-security',w:155,h:44},
'obs-datadog':{l:'Datadog',s:'Monitoring SaaS',c:'node-security',w:145,h:44},
'obs-opentelemetry':{l:'OpenTelemetry',s:'Telemetry Standard',c:'node-security',w:160,h:44},
'obs-elk':{l:'ELK Stack',s:'Log Analysis',c:'node-security',w:140,h:44}
};
function snap(v){return Math.round(v/20)*20}
function addNode(type,x,y,id,opt){
const d=NT[type];if(!d)return null;
const nid=id||(++nodeIdCounter);
const el=document.createElement('div');
el.className='canvas-node '+(opt&&opt.css?opt.css:d.c);
el.dataset.id=nid;
el.style.left=snap(x)+'px';el.style.top=snap(y)+'px';el.style.width=(opt&&opt.w?opt.w:d.w)+'px';
const label=(opt&&opt.label)||d.l;
const sub=(opt&&opt.sub)||d.s;
const ip=(opt&&opt.ip)||'';
const metaLine=ip?'<div class="node-meta">'+ip+'</div>':'';
el.innerHTML='<div class="node-remove" onclick="removeNode('+nid+')">×</div>'+
'<div class="node-port in" data-node="'+nid+'" data-port="in"></div>'+
'<div class="node-label">'+label+'</div><div class="node-sub">'+sub+'</div>'+metaLine+
'<div class="node-port out" data-node="'+nid+'" data-port="out"></div>';
el.addEventListener('mousedown',e=>{
if(e.target.closest('.node-remove')||e.target.closest('.node-port'))return;
e.stopPropagation();selectNode(nid);
const r=el.getBoundingClientRect();dragNodeId=nid;
dragOffX=e.clientX-r.left;dragOffY=e.clientY-r.top;
el.style.zIndex='20';el.style.cursor='grabbing';saveState();
});
el.querySelectorAll('.node-port').forEach(p=>{
p.addEventListener('mousedown',e=>{
e.stopPropagation();e.preventDefault();
if(p.dataset.port==='out'){
connectingFrom={nodeId:parseInt(p.dataset.node),el:p};
const r=p.getBoundingClientRect(),wr=viewport.getBoundingClientRect();
tempLine=document.createElementNS('http://www.w3.org/2000/svg','line');
tempLine.setAttribute('stroke','#00d4ff');tempLine.setAttribute('stroke-width','2.5');
tempLine.setAttribute('stroke-dasharray','8,6');tempLine.setAttribute('opacity','0.85');
if(defaultFlowMode==='flow'||defaultFlowMode==='both')tempLine.classList.add('conn-anim-flow');
if(defaultFlowMode==='pulse'||defaultFlowMode==='both')tempLine.classList.add('conn-anim-pulse');
tempLine.setAttribute('x1',(r.left-wr.left)/zoomLevel);tempLine.setAttribute('y1',(r.top-wr.top+5)/zoomLevel);
tempLine.setAttribute('x2',(e.clientX-wr.left)/zoomLevel);tempLine.setAttribute('y2',(e.clientY-wr.top)/zoomLevel);
svg.appendChild(tempLine);
}});
});
canvas.appendChild(el);
const nd={id:nid,type,x:snap(x),y:snap(y),label,sub,css:opt&&opt.css?opt.css:d.c,w:opt&&opt.w?opt.w:d.w,h:opt&&opt.h?opt.h:d.h,ip:ip||'',vendor:(opt&&opt.vendor)||'',model:(opt&&opt.model)||'',specs:(opt&&opt.specs)||'',notes:(opt&&opt.notes)||'',rack_device_id:opt&&opt.rack_device_id?opt.rack_device_id:null,layer:opt&&opt.layer?opt.layer:inferLayerFromType(type)};
nodes.push(nd);updateStats();return nid;
}
function inferLayerFromType(type){
if(!type)return 'other';
if(type.startsWith('source-')||type.startsWith('ingest-')||type.startsWith('storage-')||type.startsWith('db-'))return 'data';
if(type.startsWith('app-'))return 'app';
if(type.startsWith('api-')||type.startsWith('queue-'))return 'integration';
if(type.startsWith('hw-')||type.startsWith('net-')||type.startsWith('infra-'))return 'infra';
return 'other';
}
function removeNode(id){
const el=canvas.querySelector('.canvas-node[data-id="'+id+'"]');
if(el)el.remove();
nodes=nodes.filter(n=>n.id!==id);
edges=edges.filter(e=>e.from!==id&&e.to!==id);
renderEdges();
if(selectedNodeId===id){selectedNodeId=null;if(selectedEdgeIdx===null)propsEl.classList.remove('open');}
updateStats();saveState();
}
function normalizeEdge(e){return{from:e.from,to:e.to,flow:e.flow||'both',protocol:e.protocol||''};}
function setDefaultFlowProtocol(p){defaultFlowProtocol=p||'';}
function setEdgeProtocol(protocol){
if(selectedEdgeIdx===null)return;
edges[selectedEdgeIdx].protocol=protocol;
saveState();
}
function setDefaultFlowMode(mode){
defaultFlowMode=mode;
document.querySelectorAll('#defaultFlowToolbar .flow-chip').forEach(b=>b.classList.toggle('active',b.dataset.flow===mode));
}
function setEdgeFlow(mode){
if(selectedEdgeIdx===null)return;
edges[selectedEdgeIdx].flow=mode;
document.querySelectorAll('#edgeFlowChips .flow-chip').forEach(b=>b.classList.toggle('active',b.dataset.flow===mode));
renderEdges();
saveState();
}
function selectEdge(idx){
selectedEdgeIdx=idx;
selectedNodeId=null;
canvas.querySelectorAll('.canvas-node').forEach(n=>n.classList.remove('selected'));
svg.querySelectorAll('.conn-group').forEach(g=>g.classList.remove('selected'));
const g=svg.querySelector('.conn-group[data-idx="'+idx+'"]');
if(g)g.classList.add('selected');
showEdgeProps(idx);
}
function showEdgeProps(idx){
const e=edges[idx];if(!e)return;
const from=nodes.find(n=>n.id===e.from),to=nodes.find(n=>n.id===e.to);
document.getElementById('propsTitle').textContent='Connectie';
document.getElementById('propsNode').style.display='none';
document.getElementById('propsEdge').style.display='block';
document.getElementById('pEdgeRoute').textContent=(from?from.label:'?')+' → '+(to?to.label:'?');
document.getElementById('pEdgeProtocol').value=e.protocol||'';
document.querySelectorAll('#edgeFlowChips .flow-chip').forEach(b=>b.classList.toggle('active',b.dataset.flow===(e.flow||'both')));
propsEl.classList.add('open');
}
function deselectAll(){
selectedNodeId=null;
selectedEdgeIdx=null;
canvas.querySelectorAll('.canvas-node').forEach(n=>n.classList.remove('selected'));
svg.querySelectorAll('.conn-group').forEach(g=>g.classList.remove('selected'));
propsEl.classList.remove('open');
}
function deleteSelectedEdge(){
if(selectedEdgeIdx===null)return;
edges.splice(selectedEdgeIdx,1);
selectedEdgeIdx=null;
propsEl.classList.remove('open');
renderEdges();
updateStats();
saveState();
}
function nodeLabel(id){const n=nodes.find(nn=>nn.id===id);return n?n.label:'Node '+id;}
function selectNode(id){
selectedEdgeIdx=null;
svg.querySelectorAll('.conn-group').forEach(g=>g.classList.remove('selected'));
canvas.querySelectorAll('.canvas-node').forEach(n=>n.classList.remove('selected'));
const el=canvas.querySelector('.canvas-node[data-id="'+id+'"]');
if(el)el.classList.add('selected');
selectedNodeId=id;showProps(id);
}
function showProps(id){
const n=nodes.find(nn=>nn.id===id);if(!n)return;
document.getElementById('propsTitle').textContent='Eigenschappen';
document.getElementById('propsNode').style.display='block';
document.getElementById('propsEdge').style.display='none';
propsEl.classList.add('open');
document.getElementById('pLabel').value=n.label||'';
document.getElementById('pSub').value=n.sub||'';
document.getElementById('pIp').value=n.ip||'';
document.getElementById('pWidth').value=n.w||140;
document.getElementById('pColor').value=n.css||'node-source';
document.getElementById('pSpecs').value=n.specs||'';
document.getElementById('pNotes').value=n.notes||'';
const vSel=document.getElementById('pVendor');
vSel.innerHTML='';VENDORS.forEach(v=>{const o=document.createElement('option');o.value=v;o.textContent=v||'— Vendor —';if(v===(n.vendor||''))o.selected=true;vSel.appendChild(o);});
const mInp=document.getElementById('pModel');mInp.value=n.model||'';
const tSel=document.getElementById('pType');
tSel.innerHTML='';Object.keys(NT).forEach(k=>{const o=document.createElement('option');o.value=k;o.textContent=NT[k].l;if(k===n.type)o.selected=true;tSel.appendChild(o);});
}
function updateProp(field,value){
const n=nodes.find(nn=>nn.id===selectedNodeId);if(!n)return;
const el=canvas.querySelector('.canvas-node[data-id="'+selectedNodeId+'"]');if(!el)return;
if(field==='label'){n.label=value;el.querySelector('.node-label').textContent=value;if(selectedEdgeIdx!==null)showEdgeProps(selectedEdgeIdx);}
else if(field==='sub'){n.sub=value;el.querySelector('.node-sub').textContent=value;}
else if(field==='ip'){n.ip=value;let m=el.querySelector('.node-meta');if(m){m.textContent=value;}else if(value){const d=document.createElement('div');d.className='node-meta';d.textContent=value;el.querySelector('.node-sub').after(d);}}
else if(field==='w'){n.w=value;el.style.width=value+'px';}
else if(field==='css'){n.css=value;el.className='canvas-node '+value;}
else if(field==='vendor'){n.vendor=value;}
else if(field==='model'){n.model=value;}
else if(field==='specs'){n.specs=value;}
else if(field==='notes'){n.notes=value;}
else if(field==='type'){
const d=NT[value];if(d){n.type=value;n.css=d.c;n.w=d.w;n.h=d.h;
el.style.width=d.w+'px';el.className='canvas-node '+d.c;
el.querySelector('.node-label').textContent=d.l;el.querySelector('.node-sub').textContent=d.s;
document.getElementById('pLabel').value=d.l;document.getElementById('pSub').value=d.s;
document.getElementById('pWidth').value=d.w;document.getElementById('pColor').value=d.c;
}}renderEdges();saveState();
}
function deleteSelected(){
if(selectedEdgeIdx!==null){deleteSelectedEdge();return;}
if(selectedNodeId)removeNode(selectedNodeId);
}
function addEdge(from,to){
if(from===to)return;
if(edges.some(e=>e.from===from&&e.to===to))return;
edges.push(normalizeEdge({from,to,flow:defaultFlowMode,protocol:defaultFlowProtocol}));
renderEdges();
selectEdge(edges.length-1);
updateStats();
saveState();
}
function renderEdges(){
svg.querySelectorAll('.conn-group').forEach(g=>g.remove());
const wr=viewport.getBoundingClientRect();
edges.forEach((e,idx)=>{
e=normalizeEdge(e);edges[idx]=e;
const flow=e.flow;
const fEl=canvas.querySelector('.canvas-node[data-id="'+e.from+'"]');
const tEl=canvas.querySelector('.canvas-node[data-id="'+e.to+'"]');
if(!fEl||!tEl)return;
const fr=fEl.getBoundingClientRect(),tr=tEl.getBoundingClientRect();
const x1=(fr.right-wr.left-8)/zoomLevel,y1=(fr.top+fr.height/2-wr.top)/zoomLevel;
const x2=(tr.left-wr.left+8)/zoomLevel,y2=(tr.top+tr.height/2-wr.top)/zoomLevel;
const dx=Math.abs(x2-x1),cp=Math.max(50,dx*0.4);
const d='M '+x1+' '+y1+' C '+(x1+cp)+' '+y1+', '+(x2-cp)+' '+y2+', '+x2+' '+y2;
const g=document.createElementNS('http://www.w3.org/2000/svg','g');
g.classList.add('conn-group');
g.dataset.idx=idx;
if(selectedEdgeIdx===idx)g.classList.add('selected');
const glow=document.createElementNS('http://www.w3.org/2000/svg','path');
glow.setAttribute('d',d);glow.setAttribute('stroke','var(--accent)');glow.setAttribute('stroke-width','6');
glow.setAttribute('fill','none');glow.setAttribute('opacity','0.12');
glow.style.pointerEvents='none';
if(flow==='pulse'||flow==='both')glow.classList.add('conn-glow-pulse');
g.appendChild(glow);
const main=document.createElementNS('http://www.w3.org/2000/svg','path');
main.setAttribute('d',d);main.setAttribute('stroke','var(--accent)');main.setAttribute('stroke-width','2.5');
main.setAttribute('fill','none');main.setAttribute('stroke-linecap','round');
main.classList.add('conn-path');main.dataset.idx=idx;
main.addEventListener('click',ev=>{ev.stopPropagation();selectEdge(idx);});
main.addEventListener('dblclick',ev=>{ev.stopPropagation();edges.splice(idx,1);selectedEdgeIdx=null;propsEl.classList.remove('open');renderEdges();updateStats();saveState();});
main.addEventListener('mouseenter',()=>glow.setAttribute('opacity',flow==='off'?'0.2':'0.35'));
main.addEventListener('mouseleave',()=>glow.setAttribute('opacity',flow==='off'?'0.12':'0.18'));
g.appendChild(main);
if(flow==='flow'||flow==='both'){
const flow1=document.createElementNS('http://www.w3.org/2000/svg','path');
flow1.setAttribute('d',d);flow1.setAttribute('stroke','#00d4ff');flow1.setAttribute('stroke-width','2');
flow1.setAttribute('fill','none');flow1.setAttribute('stroke-dasharray','10,14');
flow1.setAttribute('opacity','0.85');flow1.classList.add('conn-anim-flow');
flow1.style.pointerEvents='none';
g.appendChild(flow1);
const flow2=document.createElementNS('http://www.w3.org/2000/svg','path');
flow2.setAttribute('d',d);flow2.setAttribute('stroke','#66e5ff');flow2.setAttribute('stroke-width','1.5');
flow2.setAttribute('fill','none');flow2.setAttribute('stroke-dasharray','6,18');
flow2.setAttribute('opacity','0.55');flow2.classList.add('conn-anim-flow','conn-anim-flow-delay');
flow2.style.pointerEvents='none';
g.appendChild(flow2);
}
if(flow==='pulse'||flow==='both'){
const pulse=document.createElementNS('http://www.w3.org/2000/svg','path');
pulse.setAttribute('d',d);pulse.setAttribute('stroke','#00d4ff');pulse.setAttribute('stroke-width','3');
pulse.setAttribute('fill','none');pulse.setAttribute('opacity','0.35');
pulse.classList.add('conn-anim-pulse');
pulse.style.pointerEvents='none';
g.appendChild(pulse);
}
const angle=Math.atan2(y2-y1,x2-x1);
const arrow=document.createElementNS('http://www.w3.org/2000/svg','polygon');
arrow.setAttribute('points',(x2-5)+','+(y2-3)+' '+(x2-9*Math.cos(angle-0.35))+','+(y2-9*Math.sin(angle-0.35))+' '+(x2-9*Math.cos(angle+0.35))+','+(y2-9*Math.sin(angle+0.35)));
arrow.setAttribute('fill',flow==='off'?'var(--accent)':'#00d4ff');
if(flow==='pulse'||flow==='both')arrow.classList.add('conn-anim-pulse');
arrow.style.pointerEvents='none';
g.appendChild(arrow);
svg.appendChild(g);
});
}
function saveState(){undoStack.push(JSON.stringify({nodes,edges}));if(undoStack.length>50)undoStack.shift();redoStack=[];isDirty=true;}
function undoAction(){if(undoStack.length<2)return;redoStack.push(undoStack.pop());loadState(JSON.parse(undoStack[undoStack.length-1]));}
function redoAction(){if(redoStack.length===0)return;undoStack.push(redoStack.pop());loadState(JSON.parse(undoStack[undoStack.length-1]));}
function loadState(st){
canvas.querySelectorAll('.canvas-node').forEach(el=>el.remove());svg.querySelectorAll('.conn-group').forEach(g=>g.remove());
nodes=[];edges=(st.edges||[]).map(normalizeEdge);
(st.nodes||[]).forEach(n=>addNode(n.type,n.x,n.y,n.id,n));
renderEdges();updateStats();
}
document.addEventListener('mousemove',e=>{
if(dragNodeId!==null){
const el=canvas.querySelector('.canvas-node[data-id="'+dragNodeId+'"]');if(!el)return;
const wr=viewport.getBoundingClientRect();
let x=(e.clientX-wr.left-dragOffX)/zoomLevel,y=(e.clientY-wr.top-dragOffY)/zoomLevel;
x=snap(Math.max(0,x));y=snap(Math.max(0,y));
el.style.left=x+'px';el.style.top=y+'px';
const n=nodes.find(nn=>nn.id===dragNodeId);if(n){n.x=x;n.y=y;}
renderEdges();
}
if(tempLine){const wr=viewport.getBoundingClientRect();tempLine.setAttribute('x2',(e.clientX-wr.left)/zoomLevel);tempLine.setAttribute('y2',(e.clientY-wr.top)/zoomLevel);}
if(isPanning){const dx=e.clientX-panStartX,dy=e.clientY-panStartY;panOffX+=dx;panOffY+=dy;panStartX=e.clientX;panStartY=e.clientY;viewport.style.transform='translate('+panOffX+'px,'+panOffY+'px) scale('+zoomLevel+')';}
});
document.addEventListener('mouseup',e=>{
if(dragNodeId!==null){const el=canvas.querySelector('.canvas-node[data-id="'+dragNodeId+'"]');if(el)el.style.cursor='move';dragNodeId=null;}
if(tempLine){tempLine.remove();tempLine=null;const target=e.target.closest('.node-port.in');if(target&&connectingFrom)addEdge(connectingFrom.nodeId,parseInt(target.dataset.node));connectingFrom=null;}
if(isPanning){isPanning=false;wrap.style.cursor='grab';}
});
wrap.addEventListener('mousedown',e=>{
if(e.target.closest('.canvas-node'))return;
if(e.target===wrap||e.target===viewport||e.target===canvas||e.target===svg){
deselectAll();
isPanning=true;panStartX=e.clientX;panStartY=e.clientY;wrap.style.cursor='grabbing';
}
});
wrap.addEventListener('wheel',e=>{
e.preventDefault();
const delta=e.deltaY>0?.9:1.1,newZoom=Math.max(.2,Math.min(3,zoomLevel*delta));
const mx=e.clientX,my=e.clientY,wr=wrap.getBoundingClientRect();
const vx=(mx-wr.left-panOffX)/zoomLevel,vy=(my-wr.top-panOffY)/zoomLevel;
zoomLevel=newZoom;panOffX=mx-wr.left-vx*zoomLevel;panOffY=my-wr.top-vy*zoomLevel;
viewport.style.transform='translate('+panOffX+'px,'+panOffY+'px) scale('+zoomLevel+')';
document.getElementById('zoomIndicator').textContent=Math.round(zoomLevel*100)+'%';
},{passive:false});
document.querySelectorAll('.palette-item[draggable]').forEach(item=>{
item.addEventListener('dragstart',e=>{e.dataTransfer.setData('text/plain',item.dataset.type);e.dataTransfer.effectAllowed='copy';});
});
canvas.addEventListener('dragover',e=>{e.preventDefault();e.dataTransfer.dropEffect='copy';});
canvas.addEventListener('drop',e=>{
e.preventDefault();
const type=e.dataTransfer.getData('text/plain');if(!type||!NT[type])return;
const wr=viewport.getBoundingClientRect();
let x=(e.clientX-wr.left)/zoomLevel-70,y=(e.clientY-wr.top)/zoomLevel-24;
x=snap(Math.max(0,x));y=snap(Math.max(0,y));
addNode(type,x,y);saveState();
});
function updateStats(){
document.getElementById('nodeCount').textContent=nodes.length+' componenten · '+edges.length+' connecties';
const hint=document.getElementById('canvasHint');
if(hint)hint.classList.toggle('hidden',nodes.length>0);
}
function filterPalette(q){
document.getElementById('paletteSearch').value=q;
applyPaletteFilters();
document.querySelectorAll('.pc-btn:not(.layer-btn)').forEach(b=>b.classList.toggle('active',!b.dataset.cat));
}
var _paletteCat='';
function filterPaletteCat(cat){
_paletteCat=cat;
document.querySelectorAll('.pc-btn:not(.layer-btn)').forEach(b=>b.classList.toggle('active',b.dataset.cat===cat));
document.getElementById('paletteSearch').value='';
applyPaletteFilters();
}
const LAYER_CATS={
data:['data bron','ingest','opslag','database','compute / data'],
app:['microservice','apps','ai / ml'],
integration:['api','messaging','queue'],
infra:['compute / server','netwerk','infrastructure','security','observability','ci/cd']
};
function inferLayerFromCat(cat){
const c=(cat||'').toLowerCase();
for(const [layer,keys] of Object.entries(LAYER_CATS)){
if(keys.some(k=>c.includes(k)))return layer;
}
return 'other';
}
function applyPaletteFilters(){
const q=(document.getElementById('paletteSearch').value||'').toLowerCase();
document.querySelectorAll('.cat-label').forEach(el=>{
const layer=inferLayerFromCat(el.textContent);
let showCat=!_paletteLayer||_paletteLayer===layer||layer==='other';
if(_paletteCat&&!el.textContent.trim().toLowerCase().includes(_paletteCat.toLowerCase()))showCat=false;
el.style.display=showCat?'block':'none';
let sib=el.nextElementSibling;
while(sib&&sib.classList.contains('palette-item')){
let showItem=showCat;
if(q&&!sib.textContent.toLowerCase().includes(q))showItem=false;
sib.style.display=showItem?'flex':'none';
sib=sib.nextElementSibling;
}
});
}
function filterPaletteLayer(layer){
_paletteLayer=layer;
document.querySelectorAll('.layer-btn').forEach(b=>b.classList.toggle('active',b.dataset.layer===layer));
applyPaletteFilters();
}
async function importRackDevices(){
if(!RACK_ID){setStatus('Geen rack gekoppeld','err');return;}
let devs=RACK_DEVICES;
if(!devs.length){
try{
const r=await fetch('/racks/'+RACK_ID+'/devices.json');
const d=await r.json();
devs=d.devices||[];
}catch(e){setStatus('Import mislukt','err');return;}
}
if(!devs.length){setStatus('Geen rack devices','err');return;}
const typeMap={server:'hw-dell-pe',switch:'net-catalyst',storage:'storage-netapp',firewall:'net-fortinet',router:'net-junper-srx',loadbalancer:'net-f5',san:'storage-pure',pdu:'infra-pdu',ups:'infra-apc-ups'};
let added=0;
devs.forEach(function(dev,i){
if(nodes.some(n=>n.rack_device_id===dev.id))return;
const type=typeMap[dev.device_type]||'hw-dell-pe';
addNode(type,60+(i%3)*190,60+Math.floor(i/3)*95,null,{
label:dev.name,sub:dev.model||dev.device_type,vendor:dev.manufacturer||'',model:dev.model||'',ip:dev.mgmt_ip||'',specs:dev.specs||'',rack_device_id:dev.id,layer:'infra'
});
added++;
});
saveState();
setStatus(added?added+' rack devices geïmporteerd':'Alle devices al aanwezig','ok');
}
function setStatus(msg,type){const el=document.getElementById('statusMsg');el.textContent=msg;el.className='status-msg '+(type||'');clearTimeout(el._timer);el._timer=setTimeout(()=>{el.textContent='';el.className='status-msg';},3000);}
async function saveDiagram(){
const name=document.getElementById('diagramName').value;
const clientId=document.getElementById('diagramClient').value;
const diagramType=document.getElementById('diagramType').value;
if(!name){setStatus('Naam is verplicht','err');return;}
if(!clientId){setStatus('Selecteer een cliënt','err');return;}
const payload={id:DIAGRAM_ID||null,client_id:clientId,name,description:diagramType,diagram_type:diagramType,nodes:JSON.stringify(nodes),edges:JSON.stringify(edges),rack_id:RACK_ID||null};
try{
const r=await fetch('/architecture/save',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(payload)});
const d=await r.json();setStatus('Opgeslagen ✓','ok');isDirty=false;
if(d.id){
document.getElementById('diagramMeta').dataset.id=d.id;
if(RACK_ID){
window.history.replaceState(null,'','/racks/'+RACK_ID+'/diagram/'+d.id);
}else if(!DIAGRAM_ID){
window.history.replaceState(null,'','/architecture/'+d.id);
}
}
}catch(e){setStatus('Fout bij opslaan','err');}
}
function exportJSON(){
const data={nodes,edges};const blob=new Blob([JSON.stringify(data,null,2)],{type:'application/json'});
const a=document.createElement('a');a.href=URL.createObjectURL(blob);a.download=(document.getElementById('diagramName').value||'diagram')+'.json';a.click();
}
function fitToScreen(){
if(nodes.length===0)return;
const minX=Math.min(...nodes.map(n=>n.x)),minY=Math.min(...nodes.map(n=>n.y));
const maxX=Math.max(...nodes.map(n=>n.x+(n.w||140))),maxY=Math.max(...nodes.map(n=>n.y+(n.h||48)));
const wr=wrap.getBoundingClientRect();
const w=maxX-minX+100,h=maxY-minY+100,z=Math.min(wr.width/w,(wr.height-60)/h,1.5);
zoomLevel=Math.max(.2,z);panOffX=-minX*zoomLevel+50;panOffY=-minY*zoomLevel+30;
viewport.style.transform='translate('+panOffX+'px,'+panOffY+'px) scale('+zoomLevel+')';
document.getElementById('zoomIndicator').textContent=Math.round(zoomLevel*100)+'%';
}
const GRID_ON='radial-gradient(ellipse 60% 50% at 50% 50%, rgba(0,212,255,0.04) 0%, transparent 70%),linear-gradient(rgba(0,212,255,0.045) 1px,transparent 1px),linear-gradient(90deg,rgba(0,212,255,0.045) 1px,transparent 1px)';
const GRID_OFF='radial-gradient(ellipse 60% 50% at 50% 50%, rgba(0,212,255,0.04) 0%, transparent 70%)';
function toggleGrid(){showGrid=!showGrid;wrap.style.backgroundImage=showGrid?GRID_ON:GRID_OFF;wrap.style.backgroundSize=showGrid?'100% 100%, 24px 24px, 24px 24px':'100% 100%';}
function clearCanvas(){if(nodes.length===0)return;if(!confirm('Wis alle componenten en connecties?'))return;canvas.querySelectorAll('.canvas-node').forEach(el=>el.remove());svg.querySelectorAll('.conn-group').forEach(g=>g.remove());nodes=[];edges=[];selectedNodeId=null;selectedEdgeIdx=null;propsEl.classList.remove('open');updateStats();saveState();}
window.addEventListener('keydown',e=>{
if(e.target.tagName==='INPUT'||e.target.tagName==='TEXTAREA'||e.target.tagName==='SELECT')return;
if(e.key==='Delete'||e.key==='Backspace'){deleteSelected();e.preventDefault();}
if((e.ctrlKey||e.metaKey)&&e.key==='z'){e.shiftKey?redoAction():undoAction();e.preventDefault();}
if((e.ctrlKey||e.metaKey)&&e.key==='s'){saveDiagram();e.preventDefault();}
if((e.ctrlKey||e.metaKey)&&e.key==='f'){fitToScreen();e.preventDefault();}
});
if(DIAGRAM_ID){
fetch('/architecture/render/'+DIAGRAM_ID).then(r=>r.json()).then(data=>{
if(data.nodes)data.nodes.forEach(n=>addNode(n.type,n.x,n.y,n.id,n));
if(data.edges)edges=data.edges.map(normalizeEdge);
renderEdges();updateStats();saveState();setTimeout(fitToScreen,100);
});
}
updateStats();saveState();
setDefaultFlowMode('both');
if(RACK_ID && !document.getElementById('diagramName').value){
const dt=document.getElementById('diagramType').value;
document.getElementById('diagramName').value=(dt==='application_landscape'?'Applicatie Landschap':'Applicatie Flow')+' — Rack '+RACK_ID;
}
if(typeof lucide!=='undefined')lucide.createIcons();
</script>
<script src="/js/theme.js"></script></body>
</html>