From 5066262fca0bddcfcd14eb1820046978802f7f8e Mon Sep 17 00:00:00 2001
From: Troy Grunt
Date: Thu, 5 Feb 2026 23:41:54 +0100
Subject: [PATCH] feat: Implement initial application structure with network
view and SVG editor
- Added network-view.js for visualizing network topology with devices and connections.
- Introduced svg-editor.js for managing ports on device types with drag-and-drop functionality.
- Created bootstrap.php for application initialization, including configuration and database connection.
- Established config.php for centralized configuration settings.
- Developed index.php as the main entry point with module-based routing.
- Integrated _sql.php for database abstraction.
- Added auth.php for single-user authentication handling.
- Included helpers.php for utility functions.
- Created modules for managing connections, device types, devices, and floors.
- Implemented database schema in init.sql for locations, buildings, floors, rooms, network outlets, devices, and connections.
- Added Docker support with docker-compose.yml for web and database services.
- Documented database structure and UI/UX concepts in respective markdown files.
---
NOTES.md | 4 +
README.md | 250 +++++++++++++++++++++++++
app/.htaccess | 48 +++++
app/api/connections.php | 2 +
app/api/device_type_ports.php | 2 +
app/api/upload.php | 2 +
app/assets/css/app.css | 1 +
app/assets/js/app.js | 1 +
app/assets/js/network-view.js | 289 +++++++++++++++++++++++++++++
app/assets/js/svg-editor.js | 258 +++++++++++++++++++++++++
app/bootstrap.php | 48 +++++
app/config.php | 2 +
app/index.php | 67 +++++++
app/lib/_sql.php | 2 +
app/lib/auth.php | 2 +
app/lib/helpers.php | 2 +
app/modules/connections/list.php | 2 +
app/modules/connections/save.php | 2 +
app/modules/device_types/edit.php | 2 +
app/modules/device_types/list.php | 2 +
app/modules/device_types/ports.php | 2 +
app/modules/device_types/save.php | 2 +
app/modules/devices/edit.php | 2 +
app/modules/devices/list.php | 2 +
app/modules/devices/save.php | 2 +
app/modules/floors/edit.php | 2 +
app/modules/floors/list.php | 2 +
app/modules/racks/edit.php | 2 +
app/modules/racks/list.php | 2 +
app/templates/footer.php | 19 ++
app/templates/header.php | 33 ++++
app/templates/layout.php | 28 +++
app/uploads/device_types/.keep | 1 +
app/uploads/floorplans/.keep | 1 +
doc/DATABASE.md | 282 ++++++++++++++++++++++++++++
doc/DATEISTRUKTUR.md | 11 ++
doc/UI-KONZEPT.md | 223 ++++++++++++++++++++++
docker-compose.yml | 40 ++++
init.sql | 185 ++++++++++++++++++
39 files changed, 1829 insertions(+)
create mode 100644 NOTES.md
create mode 100644 app/.htaccess
create mode 100644 app/api/connections.php
create mode 100644 app/api/device_type_ports.php
create mode 100644 app/api/upload.php
create mode 100644 app/assets/css/app.css
create mode 100644 app/assets/js/app.js
create mode 100644 app/assets/js/network-view.js
create mode 100644 app/assets/js/svg-editor.js
create mode 100644 app/bootstrap.php
create mode 100644 app/config.php
create mode 100644 app/index.php
create mode 100644 app/lib/_sql.php
create mode 100644 app/lib/auth.php
create mode 100644 app/lib/helpers.php
create mode 100644 app/modules/connections/list.php
create mode 100644 app/modules/connections/save.php
create mode 100644 app/modules/device_types/edit.php
create mode 100644 app/modules/device_types/list.php
create mode 100644 app/modules/device_types/ports.php
create mode 100644 app/modules/device_types/save.php
create mode 100644 app/modules/devices/edit.php
create mode 100644 app/modules/devices/list.php
create mode 100644 app/modules/devices/save.php
create mode 100644 app/modules/floors/edit.php
create mode 100644 app/modules/floors/list.php
create mode 100644 app/modules/racks/edit.php
create mode 100644 app/modules/racks/list.php
create mode 100644 app/templates/footer.php
create mode 100644 app/templates/header.php
create mode 100644 app/templates/layout.php
create mode 100644 app/uploads/device_types/.keep
create mode 100644 app/uploads/floorplans/.keep
create mode 100644 doc/DATABASE.md
create mode 100644 doc/DATEISTRUKTUR.md
create mode 100644 doc/UI-KONZEPT.md
create mode 100644 docker-compose.yml
create mode 100644 init.sql
diff --git a/NOTES.md b/NOTES.md
new file mode 100644
index 0000000..3838587
--- /dev/null
+++ b/NOTES.md
@@ -0,0 +1,4 @@
+# Notizen
+
+https://chatgpt.com/share/698517b9-b1e4-800e-bbd8-d207dfb326f0
+
diff --git a/README.md b/README.md
index 885eb56..1383029 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,252 @@
# netwatch
+### Stockwerksplan (SVG)
+- Pro Stockwerk ein SVG
+- Enthält:
+ - Räume (benennbar, nummerierbar)
+ - Netzwerkdosen (frei platzierbar)
+- Elemente sind:
+ - Verschiebbar
+ - Nachträglich anpassbar
+ - Eindeutig referenzierbar
+
+### Netzwerkdose
+- Name / Nummer
+- Raum
+- Anzahl Ports
+- Porttypen (z. B. RJ45, Glasfaser, BNC)
+- Ports sind vollständig normale Ports im System
+
+---
+
+## Gerätetypen
+
+### Gerätetyp
+Definiert das **Aussehen und die Port-Geometrie** eines Geräts.
+
+**Attribute**
+- Name
+- Kategorie:
+ - Switch
+ - Server
+ - Patchpanel
+ - Sonstiges
+- Darstellung:
+ - SVG **oder**
+ - JPG/PNG
+
+### Portdefinition im Bild
+Beim Anlegen eines Gerätetyps:
+- Bild wird angezeigt
+- Ports werden per Klick gesetzt:
+ - X/Y relativ zum Bild
+ - Portname
+ - Porttyp
+- Diese Portdefinition ist die Vorlage für alle Geräte dieses Typs
+
+---
+
+## Geräte
+
+### Gerät
+Instanz eines Gerätetyps.
+
+**Attribute**
+- Name
+- Gerätetyp
+- Standort / Rack / Stockwerk
+- Position im Rack:
+ - Start-HE
+ - Höhe in HE
+- Seriennummer (optional)
+- Kommentar
+
+Ports werden automatisch aus dem Gerätetyp erzeugt.
+
+---
+
+## Switches & Ports
+
+### Ports
+- Name / Nummer
+- Porttyp (frei definierbar)
+- Geschwindigkeit(en)
+- Status (aktiv / deaktiviert)
+- VLAN-Zuweisung
+- Modus:
+ - Access
+ - Trunk
+ - Hybrid
+ - Custom (Freitext)
+
+---
+
+## Module (z. B. SFP, Spezialkarten)
+
+### Modul
+Eigenständige Komponente, die in einen Port eingesetzt wird.
+
+**Eigenschaften**
+- Name
+- Typ (z. B. SFP, QSFP, BNC-Modul)
+- Kompatible Porttypen
+- Eigene Ports (z. B. LC Duplex)
+
+### Logik
+```
+Switch-Port → Modul → Modul-Port → Verbindung
+```
+
+
+Module können selbst Ports besitzen und sind vollwertige Verbindungspartner.
+
+---
+
+## Verbindungstypen
+
+### Verbindungstyp (frei definierbar)
+- Name
+- Medium:
+ - Kupfer
+ - Glasfaser
+ - Koax
+ - Sonstiges
+- Duplex:
+ - Half
+ - Full
+ - Custom
+- Max. Geschwindigkeit (optional)
+- Darstellung:
+ - Farbe
+ - Linientyp (durchgezogen, gestrichelt)
+
+Beispiele:
+- RJ45 Cat6
+- LC-LC Singlemode
+- BNC Token Ring
+- Proprietär XYZ
+
+---
+
+## Verbindungen
+
+### Verbindung
+- Port A ↔ Port B
+- Verbindungstyp
+- VLAN(s)
+- Modus
+- Kommentar
+
+Verbindungen werden:
+- logisch gespeichert
+- grafisch in allen relevanten SVGs dargestellt
+
+---
+
+## Grafische Ansichten
+
+### Rack-Ansicht (SVG)
+- Frontansicht mit HE-Raster
+- Geräte:
+ - korrekt skaliert
+ - drag & drop
+- Kabel:
+ - Linien zwischen Portpunkten
+ - Farbe gemäß Verbindungstyp
+ - Hover zeigt Details
+
+---
+
+### Netzwerkansicht (Graph)
+- Geräte als Nodes
+- Verbindungen als Edges
+- Ports optional sichtbar
+- Layout:
+ - automatisch (Force-Layout)
+ - manuell verschiebbar
+- Filter:
+ - VLAN
+ - Verbindungstyp
+ - Standort
+
+---
+
+### Stockwerks- & Raumansicht
+- SVG-Plan pro Stockwerk
+- Netzwerkdosen:
+ - anklickbar
+ - Ports sichtbar
+- Verbindungen zu Racks / Switches darstellbar
+
+---
+
+## Datenbank (konzeptionell)
+
+### Zentrale Tabellen
+- `locations`
+- `buildings`
+- `floors`
+- `rooms`
+- `floor_svgs`
+- `network_outlets`
+- `device_types`
+- `device_type_ports`
+- `devices`
+- `device_ports`
+- `modules`
+- `module_ports`
+- `connection_types`
+- `connections`
+- `vlans`
+- `racks`
+
+Custom-Eigenschaften werden teilweise als JSON gespeichert, um Erweiterungen
+ohne Schema-Brüche zu ermöglichen.
+
+---
+
+## Erweiterbarkeit
+
+Geplant, aber nicht initial:
+- Mehrbenutzerfähigkeit
+- Historisierung / Änderungsverlauf
+- Rechte & Rollen
+- Export (JSON, CSV, PDF)
+- Theming / Design-System
+- API
+
+---
+
+## Projektphasen
+
+### Phase 1 – Fundament
+- Docker Compose
+- Basisdatenbank
+- CRUD für:
+ - Verbindungstypen
+ - Gerätetypen
+ - Standorte
+
+### Phase 2 – Grafik & Geräte
+- Gerätetyp-Editor mit Port-Klick
+- Rack-Ansicht
+- Geräteplatzierung
+
+### Phase 3 – Verkabelung
+- Verbindungslogik
+- Module
+- VLANs
+- Visuelle Kabeldarstellung
+
+### Phase 4 – Gebäudepläne
+- Stockwerks-SVG
+- Räume
+- Netzwerkdosen
+- Verknüpfung mit Geräten
+
+---
+
+## Zielzustand
+Ein zuverlässiges, verständliches und visuell präzises Werkzeug zur
+Netzwerkdokumentation, das **nicht einschränkt**, sondern reale,
+auch unkonventionelle Infrastrukturen korrekt abbilden kann.
diff --git a/app/.htaccess b/app/.htaccess
new file mode 100644
index 0000000..76006f3
--- /dev/null
+++ b/app/.htaccess
@@ -0,0 +1,48 @@
+# =========================
+# Grundschutz
+# =========================
+
+# Kein Directory-Listing
+Options -Indexes
+
+# Schutz für sensible Dateien
+
+ Require all denied
+
+
+# TODO: ggf. weitere Dateien schützen, z.B. uploads oder tmp
+
+# =========================
+# Rewrite zu index.php
+# =========================
+RewriteEngine On
+
+# Alles auf index.php umleiten, außer echte Dateien/Verzeichnisse
+RewriteCond %{REQUEST_FILENAME} !-f
+RewriteCond %{REQUEST_FILENAME} !-d
+RewriteRule ^(.*)$ index.php [QSA,L]
+
+# =========================
+# Standard-Dokument
+# =========================
+DirectoryIndex index.php
+
+# =========================
+# Security Headers
+# =========================
+
+ Header set X-Content-Type-Options "nosniff"
+ Header set X-Frame-Options "SAMEORIGIN"
+ Header set X-XSS-Protection "1; mode=block"
+ Header always set Referrer-Policy "no-referrer-when-downgrade"
+ Header always set Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:;"
+
+
+# =========================
+# Upload-Sicherheit
+# =========================
+
+ Require all denied
+
+
+# TODO: Optional: Upload-Verzeichnisse (device_types, floorplans) via .htaccess zusätzlich schützen
diff --git a/app/api/connections.php b/app/api/connections.php
new file mode 100644
index 0000000..0da16c9
--- /dev/null
+++ b/app/api/connections.php
@@ -0,0 +1,2 @@
+ bewusst simpel & erweiterbar
+ */
+
+/* =========================
+ * Konfiguration
+ * ========================= */
+
+// TODO: Standort / Rack / View-Kontext vom Backend setzen
+const CONTEXT_ID = null;
+
+// TODO: API-Endpunkte definieren
+const API_LOAD_NETWORK = '/api/network_view.php?action=load';
+const API_SAVE_POSITIONS = '/api/network_view.php?action=save_positions';
+
+/* =========================
+ * State
+ * ========================= */
+
+let svgElement = null;
+
+let devices = []; // Geräte inkl. Position
+let connections = []; // Verbindungen zwischen Ports
+
+let selectedDeviceId = null;
+let isDragging = false;
+let dragOffset = { x: 0, y: 0 };
+
+/* =========================
+ * Initialisierung
+ * ========================= */
+
+document.addEventListener('DOMContentLoaded', () => {
+ svgElement = document.querySelector('#network-svg');
+
+ if (!svgElement) {
+ console.warn('Network View: #network-svg nicht gefunden');
+ return;
+ }
+
+ bindSvgEvents();
+ loadNetwork();
+});
+
+/* =========================
+ * Events
+ * ========================= */
+
+function bindSvgEvents() {
+ svgElement.addEventListener('mousemove', onMouseMove);
+ svgElement.addEventListener('mouseup', onMouseUp);
+ svgElement.addEventListener('click', onSvgClick);
+}
+
+/* =========================
+ * Laden
+ * ========================= */
+
+function loadNetwork() {
+ if (!CONTEXT_ID) {
+ console.warn('CONTEXT_ID nicht gesetzt');
+ return;
+ }
+
+ fetch(`${API_LOAD_NETWORK}&context_id=${CONTEXT_ID}`)
+ .then(res => res.json())
+ .then(data => {
+ // TODO: Datenstruktur validieren
+ devices = data.devices || [];
+ connections = data.connections || [];
+
+ renderAll();
+ })
+ .catch(err => {
+ console.error('Fehler beim Laden der Netzwerkansicht', err);
+ });
+}
+
+/* =========================
+ * Rendering
+ * ========================= */
+
+function renderAll() {
+ clearSvg();
+
+ renderConnections();
+ renderDevices();
+}
+
+function clearSvg() {
+ while (svgElement.firstChild) {
+ svgElement.removeChild(svgElement.firstChild);
+ }
+}
+
+/* ---------- Geräte ---------- */
+
+function renderDevices() {
+ devices.forEach(device => renderDevice(device));
+}
+
+function renderDevice(device) {
+ const group = document.createElementNS('http://www.w3.org/2000/svg', 'g');
+ group.classList.add('device-node');
+ group.dataset.id = device.id;
+
+ group.setAttribute(
+ 'transform',
+ `translate(${device.x || 0}, ${device.y || 0})`
+ );
+
+ // TODO: Gerätetyp (SVG oder JPG) korrekt laden
+ const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
+ rect.setAttribute('width', 120);
+ rect.setAttribute('height', 60);
+ rect.setAttribute('rx', 6);
+
+ rect.addEventListener('mousedown', (e) => {
+ startDrag(e, device.id);
+ e.stopPropagation();
+ });
+
+ // Label
+ const text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
+ text.setAttribute('x', 60);
+ text.setAttribute('y', 35);
+ text.setAttribute('text-anchor', 'middle');
+ text.textContent = device.name || 'Device';
+
+ group.appendChild(rect);
+ group.appendChild(text);
+
+ // TODO: Ports als kleine Kreise anlegen (Position aus Portdefinition)
+ // TODO: Ports klickbar machen (für Verbindungs-Erstellung)
+
+ svgElement.appendChild(group);
+}
+
+/* ---------- Verbindungen ---------- */
+
+function renderConnections() {
+ connections.forEach(conn => renderConnection(conn));
+}
+
+function renderConnection(connection) {
+ // TODO: Quell- & Ziel-Port-Koordinaten berechnen
+ // TODO: unterschiedliche Verbindungstypen (Farbe, Strichart, Dicke)
+
+ const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
+
+ line.setAttribute('x1', 0);
+ line.setAttribute('y1', 0);
+ line.setAttribute('x2', 100);
+ line.setAttribute('y2', 100);
+
+ line.classList.add('connection-line');
+
+ svgElement.appendChild(line);
+}
+
+/* =========================
+ * Interaktion
+ * ========================= */
+
+function onSvgClick(event) {
+ // Klick ins Leere -> Auswahl aufheben
+ if (event.target === svgElement) {
+ selectedDeviceId = null;
+ updateSelection();
+ }
+}
+
+function startDrag(event, deviceId) {
+ const device = getDeviceById(deviceId);
+ if (!device) return;
+
+ isDragging = true;
+ selectedDeviceId = deviceId;
+
+ const point = getSvgCoordinates(event);
+ dragOffset.x = (device.x || 0) - point.x;
+ dragOffset.y = (device.y || 0) - point.y;
+
+ updateSelection();
+}
+
+function onMouseMove(event) {
+ if (!isDragging || !selectedDeviceId) return;
+
+ const device = getDeviceById(selectedDeviceId);
+ if (!device) return;
+
+ const point = getSvgCoordinates(event);
+
+ device.x = point.x + dragOffset.x;
+ device.y = point.y + dragOffset.y;
+
+ renderAll();
+}
+
+function onMouseUp() {
+ if (!isDragging) return;
+
+ isDragging = false;
+
+ // TODO: Positionen optional automatisch speichern
+}
+
+/* =========================
+ * Auswahl
+ * ========================= */
+
+function updateSelection() {
+ svgElement.querySelectorAll('.device-node').forEach(el => {
+ el.classList.toggle(
+ 'selected',
+ el.dataset.id === String(selectedDeviceId)
+ );
+ });
+
+ // TODO: Sidebar mit Gerätedetails füllen
+}
+
+/* =========================
+ * Speichern
+ * ========================= */
+
+function savePositions() {
+ fetch(API_SAVE_POSITIONS, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ context_id: CONTEXT_ID,
+ devices: devices.map(d => ({
+ id: d.id,
+ x: d.x,
+ y: d.y
+ }))
+ })
+ })
+ .then(res => res.json())
+ .then(data => {
+ // TODO: Erfolg / Fehler anzeigen
+ console.log('Positionen gespeichert', data);
+ })
+ .catch(err => {
+ console.error('Fehler beim Speichern', err);
+ });
+}
+
+/* =========================
+ * Hilfsfunktionen
+ * ========================= */
+
+function getSvgCoordinates(event) {
+ const pt = svgElement.createSVGPoint();
+ pt.x = event.clientX;
+ pt.y = event.clientY;
+
+ const transformed = pt.matrixTransform(svgElement.getScreenCTM().inverse());
+ return { x: transformed.x, y: transformed.y };
+}
+
+function getDeviceById(id) {
+ return devices.find(d => d.id === id);
+}
+
+/* =========================
+ * Keyboard Shortcuts
+ * ========================= */
+
+document.addEventListener('keydown', (e) => {
+ if (e.key === 'Escape') {
+ selectedDeviceId = null;
+ updateSelection();
+ }
+
+ // TODO: Delete -> Gerät entfernen?
+});
diff --git a/app/assets/js/svg-editor.js b/app/assets/js/svg-editor.js
new file mode 100644
index 0000000..709fb4c
--- /dev/null
+++ b/app/assets/js/svg-editor.js
@@ -0,0 +1,258 @@
+// Logik für den SVG-Port-Editor (Klicks, Drag & Drop, Speichern)
+/**
+ * svg-editor.js
+ *
+ * Logik für den SVG-Port-Editor:
+ * - Ports per Klick anlegen
+ * - Ports auswählen
+ * - Ports verschieben (Drag & Drop)
+ * - Ports löschen
+ * - Ports laden / speichern
+ *
+ * Abhängigkeiten: keine (Vanilla JS)
+ */
+
+/* =========================
+ * Konfiguration
+ * ========================= */
+
+// TODO: vom Backend setzen (z. B. via data-Attribut)
+const DEVICE_TYPE_ID = null;
+
+// TODO: API-Endpunkte festlegen
+const API_LOAD_PORTS = '/api/device_type_ports.php?action=load';
+const API_SAVE_PORTS = '/api/device_type_ports.php?action=save';
+
+/* =========================
+ * State
+ * ========================= */
+
+let svgElement = null;
+let ports = [];
+let selectedPortId = null;
+let isDragging = false;
+let dragOffset = { x: 0, y: 0 };
+
+/* =========================
+ * Initialisierung
+ * ========================= */
+
+document.addEventListener('DOMContentLoaded', () => {
+ svgElement = document.querySelector('#device-svg');
+
+ if (!svgElement) {
+ console.warn('SVG Editor: #device-svg nicht gefunden');
+ return;
+ }
+
+ bindSvgEvents();
+ loadPorts();
+});
+
+/* =========================
+ * SVG Events
+ * ========================= */
+
+function bindSvgEvents() {
+ svgElement.addEventListener('click', onSvgClick);
+ svgElement.addEventListener('mousemove', onSvgMouseMove);
+ svgElement.addEventListener('mouseup', onSvgMouseUp);
+}
+
+/* =========================
+ * Port-Erstellung
+ * ========================= */
+
+function onSvgClick(event) {
+ // Klick auf bestehenden Port?
+ if (event.target.classList.contains('port-point')) {
+ selectPort(event.target.dataset.id);
+ return;
+ }
+
+ // TODO: Modifier-Key prüfen (z. B. nur mit SHIFT neuen Port erstellen?)
+ const point = getSvgCoordinates(event);
+
+ createPort(point.x, point.y);
+}
+
+function createPort(x, y) {
+ const id = generateTempId();
+
+ const port = {
+ id: id,
+ name: `Port ${ports.length + 1}`,
+ port_type_id: null, // TODO: Default-Porttyp?
+ x: x,
+ y: y,
+ comment: ''
+ };
+
+ ports.push(port);
+ renderPort(port);
+ selectPort(id);
+}
+
+/* =========================
+ * Rendering
+ * ========================= */
+
+function renderPort(port) {
+ const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
+
+ circle.setAttribute('cx', port.x);
+ circle.setAttribute('cy', port.y);
+ circle.setAttribute('r', 6);
+ circle.classList.add('port-point');
+ circle.dataset.id = port.id;
+
+ circle.addEventListener('mousedown', (e) => {
+ startDrag(e, port.id);
+ e.stopPropagation();
+ });
+
+ svgElement.appendChild(circle);
+}
+
+function rerenderPorts() {
+ svgElement.querySelectorAll('.port-point').forEach(p => p.remove());
+ ports.forEach(renderPort);
+}
+
+/* =========================
+ * Auswahl
+ * ========================= */
+
+function selectPort(id) {
+ selectedPortId = id;
+
+ document.querySelectorAll('.port-point').forEach(el => {
+ el.classList.toggle('selected', el.dataset.id === id);
+ });
+
+ // TODO: Sidebar-Felder mit Portdaten füllen
+}
+
+/* =========================
+ * Drag & Drop
+ * ========================= */
+
+function startDrag(event, portId) {
+ const port = getPortById(portId);
+ if (!port) return;
+
+ isDragging = true;
+ selectedPortId = portId;
+
+ const point = getSvgCoordinates(event);
+ dragOffset.x = port.x - point.x;
+ dragOffset.y = port.y - point.y;
+}
+
+function onSvgMouseMove(event) {
+ if (!isDragging || !selectedPortId) return;
+
+ const port = getPortById(selectedPortId);
+ if (!port) return;
+
+ const point = getSvgCoordinates(event);
+
+ port.x = point.x + dragOffset.x;
+ port.y = point.y + dragOffset.y;
+
+ rerenderPorts();
+}
+
+function onSvgMouseUp() {
+ isDragging = false;
+}
+
+/* =========================
+ * Löschen
+ * ========================= */
+
+function deleteSelectedPort() {
+ if (!selectedPortId) return;
+
+ // TODO: Sicherheitsabfrage (confirm)
+ ports = ports.filter(p => p.id !== selectedPortId);
+ selectedPortId = null;
+
+ rerenderPorts();
+
+ // TODO: Sidebar zurücksetzen
+}
+
+/* =========================
+ * Laden / Speichern
+ * ========================= */
+
+function loadPorts() {
+ if (!DEVICE_TYPE_ID) {
+ console.warn('DEVICE_TYPE_ID nicht gesetzt');
+ return;
+ }
+
+ fetch(`${API_LOAD_PORTS}&device_type_id=${DEVICE_TYPE_ID}`)
+ .then(res => res.json())
+ .then(data => {
+ // TODO: Datenformat validieren
+ ports = data;
+ rerenderPorts();
+ })
+ .catch(err => {
+ console.error('Fehler beim Laden der Ports', err);
+ });
+}
+
+function savePorts() {
+ if (!DEVICE_TYPE_ID) return;
+
+ fetch(API_SAVE_PORTS, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ device_type_id: DEVICE_TYPE_ID,
+ ports: ports
+ })
+ })
+ .then(res => res.json())
+ .then(data => {
+ // TODO: Erfolg / Fehler anzeigen
+ console.log('Ports gespeichert', data);
+ })
+ .catch(err => {
+ console.error('Fehler beim Speichern', err);
+ });
+}
+
+/* =========================
+ * Hilfsfunktionen
+ * ========================= */
+
+function getSvgCoordinates(event) {
+ const pt = svgElement.createSVGPoint();
+ pt.x = event.clientX;
+ pt.y = event.clientY;
+
+ const transformed = pt.matrixTransform(svgElement.getScreenCTM().inverse());
+ return { x: transformed.x, y: transformed.y };
+}
+
+function getPortById(id) {
+ return ports.find(p => p.id === id);
+}
+
+function generateTempId() {
+ return 'tmp_' + Math.random().toString(36).substr(2, 9);
+}
+
+/* =========================
+ * Keyboard Shortcuts
+ * ========================= */
+
+document.addEventListener('keydown', (e) => {
+ if (e.key === 'Delete') {
+ deleteSelectedPort();
+ }
+});
diff --git a/app/bootstrap.php b/app/bootstrap.php
new file mode 100644
index 0000000..aba3307
--- /dev/null
+++ b/app/bootstrap.php
@@ -0,0 +1,48 @@
+Die Seite existiert noch nicht.
";
+}
+
+/* =========================
+ * Template-Footer laden
+ * ========================= */
+require_once __DIR__ . '/templates/footer.php';
diff --git a/app/lib/_sql.php b/app/lib/_sql.php
new file mode 100644
index 0000000..beb9bf7
--- /dev/null
+++ b/app/lib/_sql.php
@@ -0,0 +1,2 @@
+
+
+
+
+
+
+