From 510a248edbb00445af8415488f49541f6801feba Mon Sep 17 00:00:00 2001
From: fixclean
Date: Mon, 16 Feb 2026 13:56:01 +0100
Subject: [PATCH 1/2] div TODOs
---
TODO.md | 15 +
app/api/connections.php | 351 ++++++++++-------
app/api/device_type_ports.php | 256 +++++++------
app/api/upload.php | 142 +++----
app/assets/icons/favicon.svg | 4 +
app/assets/js/app.js | 179 ++++-----
app/assets/js/dashboard.js | 156 +++-----
app/assets/js/device-type-edit-form.js | 20 +-
app/assets/js/device-types-list.js | 16 +-
app/assets/js/locations-list.js | 30 +-
app/assets/js/network-view.js | 251 ++++++------
app/assets/js/svg-editor.js | 213 +++++------
app/bootstrap.php | 35 +-
app/config.php | 17 +
app/index.php | 53 +--
app/lib/_sql.php | 18 +-
app/lib/auth.php | 80 ++--
app/modules/buildings/delete.php | 35 ++
app/modules/buildings/edit.php | 28 +-
app/modules/buildings/list.php | 31 +-
app/modules/dashboard/list.php | 54 ++-
app/modules/device_types/delete.php | 39 ++
app/modules/device_types/edit.php | 2 +-
app/modules/device_types/ports.php | 398 ++++++++------------
app/modules/floor_infrastructure/delete.php | 31 ++
app/modules/floor_infrastructure/edit.php | 5 +-
app/modules/floor_infrastructure/list.php | 2 +
app/modules/floors/delete.php | 36 ++
app/modules/floors/list.php | 27 +-
app/modules/locations/edit.php | 25 +-
app/modules/racks/delete.php | 36 ++
app/modules/racks/edit.php | 131 ++-----
app/modules/racks/list.php | 277 ++++----------
app/templates/footer.php | 25 +-
app/templates/header.php | 4 +-
app/templates/layout.php | 11 +-
36 files changed, 1500 insertions(+), 1533 deletions(-)
create mode 100644 app/assets/icons/favicon.svg
create mode 100644 app/modules/buildings/delete.php
create mode 100644 app/modules/device_types/delete.php
create mode 100644 app/modules/floor_infrastructure/delete.php
create mode 100644 app/modules/floors/delete.php
create mode 100644 app/modules/racks/delete.php
diff --git a/TODO.md b/TODO.md
index 81c30cc..8119d0d 100644
--- a/TODO.md
+++ b/TODO.md
@@ -4,6 +4,21 @@ Zentrale Sammlung aller TODO-Markierungen im Repository (Stand: 13. Februar 2026
Hinweis: Die Eintraege sind direkt aus den Quelldateien aggregiert.
+## Arbeitsnotizen (16. Februar 2026)
+
+- [x] API-Basis umgesetzt: `app/api/connections.php`, `app/api/device_type_ports.php`, `app/api/upload.php` auf aktuelles Schema gebracht (Auth, Validierung, Existenzpruefungen, Fehlerantworten).
+- [x] Bootstrap/Auth/Config/Routing umgesetzt: `app/config.php`, `app/bootstrap.php`, `app/lib/_sql.php`, `app/lib/auth.php`, `app/index.php`.
+- [x] Frontend-Grundlagen aktualisiert: `app/assets/js/app.js`, `app/assets/js/dashboard.js`, `app/assets/js/svg-editor.js`, `app/assets/js/network-view.js`.
+- [x] Delete-Flow fuer zentrale Module umgesetzt: `buildings`, `floors`, `racks`, `device_types`, `floor_infrastructure`.
+- [x] Legacy-Mock ersetzt: `app/modules/device_types/ports.php` lauffaehig gemacht (anzeigen, hinzufuegen, loeschen).
+- [x] TODO-Reste in `header.php`, `footer.php`, `layout.php`, `floor_infrastructure/edit.php` entfernt.
+
+Offene Blocker / naechste Punkte:
+- [ ] `app/modules/connections/list.php`: Detailbereich fuer ausgewaehlte Verbindung sowie Bearbeiten/Loeschen im UI fehlen noch.
+- [ ] `app/modules/dashboard/list.php`: grosse zoombare Gesamt-Topologie-Wand (fachlich/grafisch groesseres Feature).
+- [ ] `app/lib/helpers.php`: generischer Sammel-TODO ohne konkreten Scope.
+- [ ] Vollstaendiger End-to-End-Test aktuell nicht moeglich, da in dieser Shell kein `php` CLI verfuegbar ist.
+
## app\api\connections.php
- [ ] L15: // TODO: Single-User-Auth prüfen
diff --git a/app/api/connections.php b/app/api/connections.php
index ffeba17..d05631b 100644
--- a/app/api/connections.php
+++ b/app/api/connections.php
@@ -1,194 +1,273 @@
'Unbekannte Aktion']);
- break;
+ jsonError('Unbekannte Aktion', 400);
}
-/* =========================
- * Aktionen
- * ========================= */
-
-/**
- * Lädt alle Geräte + Ports + Verbindungen für eine Netzwerkansicht
- */
-function loadConnections($sql)
+function jsonError(string $message, int $status = 400): void
{
- $contextId = $_GET['context_id'] ?? null;
+ http_response_code($status);
+ echo json_encode(['error' => $message]);
+ exit;
+}
- if (!$contextId) {
- http_response_code(400);
- echo json_encode(['error' => 'context_id fehlt']);
- return;
+function normalizeEndpointType(string $type): ?string
+{
+ $map = [
+ 'device' => 'device',
+ 'device_ports' => 'device',
+ 'module' => 'module',
+ 'module_ports' => 'module',
+ 'outlet' => 'outlet',
+ 'network_outlet_ports' => 'outlet',
+ 'patchpanel' => 'patchpanel',
+ 'floor_patchpanel_ports' => 'patchpanel',
+ ];
+
+ $key = strtolower(trim($type));
+ return $map[$key] ?? null;
+}
+
+function endpointExists($sql, string $type, int $id): bool
+{
+ if ($id <= 0) {
+ return false;
}
- // TODO: Kontext definieren (Standort, Rack, Floor, gesamtes Netz)
+ if ($type === 'device') {
+ $row = $sql->single('SELECT id FROM device_ports WHERE id = ?', 'i', [$id]);
+ return !empty($row);
+ }
+
+ if ($type === 'module') {
+ $row = $sql->single('SELECT id FROM module_ports WHERE id = ?', 'i', [$id]);
+ return !empty($row);
+ }
+
+ if ($type === 'outlet') {
+ $row = $sql->single('SELECT id FROM network_outlet_ports WHERE id = ?', 'i', [$id]);
+ return !empty($row);
+ }
+
+ if ($type === 'patchpanel') {
+ $row = $sql->single('SELECT id FROM floor_patchpanel_ports WHERE id = ?', 'i', [$id]);
+ return !empty($row);
+ }
+
+ return false;
+}
+
+function loadConnections($sql): void
+{
+ $contextType = strtolower(trim((string)($_GET['context_type'] ?? 'all')));
+ $contextId = isset($_GET['context_id']) ? (int)$_GET['context_id'] : 0;
+
+ $where = '';
+ $bindType = '';
+ $bindValues = [];
+
+ if ($contextType !== 'all') {
+ if ($contextId <= 0) {
+ jsonError('context_id fehlt oder ist ungueltig', 400);
+ }
+
+ if ($contextType === 'location') {
+ $where = ' WHERE b.location_id = ?';
+ $bindType = 'i';
+ $bindValues = [$contextId];
+ } elseif ($contextType === 'building') {
+ $where = ' WHERE f.building_id = ?';
+ $bindType = 'i';
+ $bindValues = [$contextId];
+ } elseif ($contextType === 'floor') {
+ $where = ' WHERE r.floor_id = ?';
+ $bindType = 'i';
+ $bindValues = [$contextId];
+ } elseif ($contextType === 'rack') {
+ $where = ' WHERE d.rack_id = ?';
+ $bindType = 'i';
+ $bindValues = [$contextId];
+ } else {
+ jsonError('Ungueltiger Kontext. Erlaubt: all, location, building, floor, rack', 400);
+ }
+ }
- /* ---------- Geräte ---------- */
$devices = $sql->get(
- "SELECT id, name, device_type_id, pos_x, pos_y
- FROM devices
- WHERE context_id = ?",
- "i",
- [$contextId]
+ "SELECT d.id, d.name, d.device_type_id, d.rack_id, 0 AS pos_x, 0 AS pos_y
+ FROM devices d
+ LEFT JOIN racks r ON r.id = d.rack_id
+ LEFT JOIN floors f ON f.id = r.floor_id
+ LEFT JOIN buildings b ON b.id = f.building_id" . $where,
+ $bindType,
+ $bindValues
);
- /* ---------- Ports ---------- */
$ports = $sql->get(
- "SELECT p.id, p.device_id, p.name, p.port_type_id
- FROM ports p
- JOIN devices d ON d.id = p.device_id
- WHERE d.context_id = ?",
- "i",
- [$contextId]
+ "SELECT dp.id, dp.device_id, dp.name, dp.port_type_id
+ FROM device_ports dp
+ JOIN devices d ON d.id = dp.device_id
+ LEFT JOIN racks r ON r.id = d.rack_id
+ LEFT JOIN floors f ON f.id = r.floor_id
+ LEFT JOIN buildings b ON b.id = f.building_id" . $where,
+ $bindType,
+ $bindValues
);
- /* ---------- Verbindungen ---------- */
$connections = $sql->get(
- "SELECT
- c.id,
- c.connection_type_id,
- c.port_a_id,
- c.port_b_id,
- c.vlan,
- c.mode,
- c.comment
- FROM connections c",
- "",
+ "SELECT id, connection_type_id, port_a_type, port_a_id, port_b_type, port_b_id, vlan_config, mode, comment
+ FROM connections",
+ '',
[]
);
echo json_encode([
- 'devices' => $devices,
- 'ports' => $ports,
- 'connections' => $connections
+ 'devices' => $devices ?: [],
+ 'ports' => $ports ?: [],
+ 'connections' => $connections ?: []
]);
}
-/**
- * Speichert eine Verbindung (neu oder Update)
- */
-function saveConnection($sql)
+function resolveConnectionTypeId($sql, array $data): int
{
- $data = json_decode(file_get_contents('php://input'), true);
-
- if (!$data) {
- http_response_code(400);
- echo json_encode(['error' => 'Ungültige JSON-Daten']);
- return;
+ if (!empty($data['connection_type_id'])) {
+ $requestedId = (int)$data['connection_type_id'];
+ $exists = $sql->single('SELECT id FROM connection_types WHERE id = ?', 'i', [$requestedId]);
+ if (!$exists) {
+ jsonError('connection_type_id existiert nicht', 400);
+ }
+ return $requestedId;
}
- // TODO: Validierung
- // - port_a_id vorhanden
- // - port_b_id vorhanden
- // - Verbindungstyp erlaubt
+ $defaultType = $sql->single('SELECT id FROM connection_types ORDER BY id ASC LIMIT 1');
+ if (!$defaultType) {
+ jsonError('Kein Verbindungstyp vorhanden', 400);
+ }
+
+ return (int)$defaultType['id'];
+}
+
+function saveConnection($sql): void
+{
+ if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
+ jsonError('Methode nicht erlaubt', 405);
+ }
+
+ $data = json_decode(file_get_contents('php://input'), true);
+ if (!is_array($data)) {
+ jsonError('Ungueltige JSON-Daten', 400);
+ }
+
+ $portAType = normalizeEndpointType((string)($data['port_a_type'] ?? ''));
+ $portBType = normalizeEndpointType((string)($data['port_b_type'] ?? ''));
+ $portAId = (int)($data['port_a_id'] ?? 0);
+ $portBId = (int)($data['port_b_id'] ?? 0);
+
+ if ($portAType === null || $portBType === null) {
+ jsonError('port_a_type/port_b_type ungueltig', 400);
+ }
+
+ if ($portAId <= 0 || $portBId <= 0) {
+ jsonError('port_a_id und port_b_id sind erforderlich', 400);
+ }
+
+ if ($portAType === $portBType && $portAId === $portBId) {
+ jsonError('Port A und Port B duerfen nicht identisch sein', 400);
+ }
+
+ if (!endpointExists($sql, $portAType, $portAId) || !endpointExists($sql, $portBType, $portBId)) {
+ jsonError('Mindestens ein Endpunkt existiert nicht', 400);
+ }
+
+ $connectionTypeId = resolveConnectionTypeId($sql, $data);
+
+ $vlanConfig = $data['vlan_config'] ?? null;
+ if (is_array($vlanConfig)) {
+ $vlanConfig = json_encode($vlanConfig);
+ } elseif (!is_string($vlanConfig) && $vlanConfig !== null) {
+ jsonError('vlan_config muss String, Array oder null sein', 400);
+ }
+
+ $mode = isset($data['mode']) ? (string)$data['mode'] : null;
+ $comment = isset($data['comment']) ? (string)$data['comment'] : null;
if (!empty($data['id'])) {
- // UPDATE
+ $id = (int)$data['id'];
+ $existing = $sql->single('SELECT id FROM connections WHERE id = ?', 'i', [$id]);
+ if (!$existing) {
+ jsonError('Verbindung existiert nicht', 404);
+ }
+
$rows = $sql->set(
- "UPDATE connections
- SET connection_type_id = ?, port_a_id = ?, port_b_id = ?, vlan = ?, mode = ?, comment = ?
- WHERE id = ?",
- "iiiissi",
- [
- $data['connection_type_id'],
- $data['port_a_id'],
- $data['port_b_id'],
- $data['vlan'],
- $data['mode'],
- $data['comment'],
- $data['id']
- ]
+ 'UPDATE connections
+ SET connection_type_id = ?, port_a_type = ?, port_a_id = ?, port_b_type = ?, port_b_id = ?, vlan_config = ?, mode = ?, comment = ?
+ WHERE id = ?',
+ 'isisisssi',
+ [$connectionTypeId, $portAType, $portAId, $portBType, $portBId, $vlanConfig, $mode, $comment, $id]
);
- echo json_encode([
- 'status' => 'updated',
- 'rows' => $rows
- ]);
- } else {
- // INSERT
- $id = $sql->set(
- "INSERT INTO connections
- (connection_type_id, port_a_id, port_b_id, vlan, mode, comment)
- VALUES (?, ?, ?, ?, ?, ?)",
- "iiiiss",
- [
- $data['connection_type_id'],
- $data['port_a_id'],
- $data['port_b_id'],
- $data['vlan'],
- $data['mode'],
- $data['comment']
- ],
- true
- );
+ if ($rows === false) {
+ jsonError('Update fehlgeschlagen', 500);
+ }
- echo json_encode([
- 'status' => 'created',
- 'id' => $id
- ]);
- }
-}
-
-/**
- * Löscht eine Verbindung
- */
-function deleteConnection($sql)
-{
- $id = $_GET['id'] ?? null;
-
- if (!$id) {
- http_response_code(400);
- echo json_encode(['error' => 'ID fehlt']);
+ echo json_encode(['status' => 'updated', 'rows' => $rows]);
return;
}
- // TODO: Prüfen, ob Verbindung existiert
-
- $rows = $sql->set(
- "DELETE FROM connections WHERE id = ?",
- "i",
- [$id]
+ $id = $sql->set(
+ 'INSERT INTO connections (connection_type_id, port_a_type, port_a_id, port_b_type, port_b_id, vlan_config, mode, comment)
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)',
+ 'isisisss',
+ [$connectionTypeId, $portAType, $portAId, $portBType, $portBId, $vlanConfig, $mode, $comment],
+ true
);
- echo json_encode([
- 'status' => 'deleted',
- 'rows' => $rows
- ]);
+ if ($id === false) {
+ jsonError('Insert fehlgeschlagen', 500);
+ }
+
+ echo json_encode(['status' => 'created', 'id' => $id]);
+}
+
+function deleteConnection($sql): void
+{
+ if ($_SERVER['REQUEST_METHOD'] !== 'POST' && $_SERVER['REQUEST_METHOD'] !== 'DELETE') {
+ jsonError('Methode nicht erlaubt', 405);
+ }
+
+ $id = isset($_GET['id']) ? (int)$_GET['id'] : 0;
+ if ($id <= 0) {
+ jsonError('ID fehlt', 400);
+ }
+
+ $existing = $sql->single('SELECT id FROM connections WHERE id = ?', 'i', [$id]);
+ if (!$existing) {
+ jsonError('Verbindung existiert nicht', 404);
+ }
+
+ $rows = $sql->set('DELETE FROM connections WHERE id = ?', 'i', [$id]);
+ if ($rows === false) {
+ jsonError('Loeschen fehlgeschlagen', 500);
+ }
+
+ echo json_encode(['status' => 'deleted', 'rows' => $rows]);
}
diff --git a/app/api/device_type_ports.php b/app/api/device_type_ports.php
index deeaf1d..8bfb1bc 100644
--- a/app/api/device_type_ports.php
+++ b/app/api/device_type_ports.php
@@ -1,175 +1,199 @@
'Unbekannte Aktion']);
- break;
+ jsonError('Unbekannte Aktion', 400);
}
-/* =========================
- * Aktionen
- * ========================= */
-
-/**
- * Lädt alle Ports eines Gerätetyps
- */
-function loadPorts($sql)
+function jsonError(string $message, int $status = 400): void
{
- $deviceTypeId = $_GET['device_type_id'] ?? null;
+ http_response_code($status);
+ echo json_encode(['error' => $message]);
+ exit;
+}
- if (!$deviceTypeId) {
- http_response_code(400);
- echo json_encode(['error' => 'device_type_id fehlt']);
- return;
+function loadPorts($sql): void
+{
+ $deviceTypeId = isset($_GET['device_type_id']) ? (int)$_GET['device_type_id'] : 0;
+ if ($deviceTypeId <= 0) {
+ jsonError('device_type_id fehlt', 400);
}
$ports = $sql->get(
- "SELECT
- id,
- name,
- port_type_id,
- pos_x,
- pos_y,
- comment
+ 'SELECT id, name, port_type_id, x, y, metadata
FROM device_type_ports
WHERE device_type_id = ?
- ORDER BY id ASC",
- "i",
+ ORDER BY id ASC',
+ 'i',
[$deviceTypeId]
);
- echo json_encode($ports);
+ echo json_encode($ports ?: []);
}
-/**
- * Speichert alle Ports eines Gerätetyps
- * (Bulk-Save aus dem SVG-Editor)
- */
-function savePorts($sql)
+function validatePortTypeId($sql, $portTypeId): ?int
{
- $data = json_decode(file_get_contents('php://input'), true);
-
- if (!$data || empty($data['device_type_id']) || !is_array($data['ports'])) {
- http_response_code(400);
- echo json_encode(['error' => 'Ungültige Daten']);
- return;
+ if ($portTypeId === null || $portTypeId === '' || (int)$portTypeId <= 0) {
+ return null;
}
- $deviceTypeId = $data['device_type_id'];
- $ports = $data['ports'];
+ $value = (int)$portTypeId;
+ $exists = $sql->single('SELECT id FROM port_types WHERE id = ?', 'i', [$value]);
+ if (!$exists) {
+ jsonError('port_type_id ist ungueltig', 400);
+ }
- // TODO: Transaktion starten (falls SQL-Klasse das unterstützt)
+ return $value;
+}
- foreach ($ports as $port) {
+function savePorts($sql): void
+{
+ if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
+ jsonError('Methode nicht erlaubt', 405);
+ }
- // TODO: Validierung:
- // - name nicht leer
- // - pos_x / pos_y numerisch
- // - port_type_id erlaubt
+ $data = json_decode(file_get_contents('php://input'), true);
+ if (!is_array($data) || empty($data['device_type_id']) || !isset($data['ports']) || !is_array($data['ports'])) {
+ jsonError('Ungueltige Daten', 400);
+ }
- if (!empty($port['id']) && !str_starts_with($port['id'], 'tmp_')) {
+ $deviceTypeId = (int)$data['device_type_id'];
+ if ($deviceTypeId <= 0) {
+ jsonError('device_type_id ist ungueltig', 400);
+ }
- /* ---------- UPDATE ---------- */
- $sql->set(
- "UPDATE device_type_ports
- SET name = ?, port_type_id = ?, pos_x = ?, pos_y = ?, comment = ?
- WHERE id = ? AND device_type_id = ?",
- "siddsii",
- [
- $port['name'],
- $port['port_type_id'],
- $port['x'],
- $port['y'],
- $port['comment'],
- $port['id'],
- $deviceTypeId
- ]
+ $deviceType = $sql->single('SELECT id FROM device_types WHERE id = ?', 'i', [$deviceTypeId]);
+ if (!$deviceType) {
+ jsonError('Geraetetyp existiert nicht', 404);
+ }
+
+ $sql->set('START TRANSACTION');
+
+ foreach ($data['ports'] as $index => $port) {
+ if (!is_array($port)) {
+ $sql->set('ROLLBACK');
+ jsonError('Port-Eintrag an Position ' . $index . ' ist ungueltig', 400);
+ }
+
+ $name = trim((string)($port['name'] ?? ''));
+ if ($name === '') {
+ $sql->set('ROLLBACK');
+ jsonError('Port-Name darf nicht leer sein', 400);
+ }
+
+ $x = $port['x'] ?? null;
+ $y = $port['y'] ?? null;
+ if (!is_numeric($x) || !is_numeric($y)) {
+ $sql->set('ROLLBACK');
+ jsonError('x und y muessen numerisch sein', 400);
+ }
+
+ $x = (int)round((float)$x);
+ $y = (int)round((float)$y);
+ $portTypeId = validatePortTypeId($sql, $port['port_type_id'] ?? null);
+
+ $metadataRaw = $port['metadata'] ?? null;
+ $metadata = null;
+ if (is_array($metadataRaw)) {
+ $metadata = json_encode($metadataRaw);
+ } elseif (is_string($metadataRaw) && $metadataRaw !== '') {
+ json_decode($metadataRaw, true);
+ if (json_last_error() !== JSON_ERROR_NONE) {
+ $sql->set('ROLLBACK');
+ jsonError('metadata ist kein gueltiges JSON', 400);
+ }
+ $metadata = $metadataRaw;
+ }
+
+ $isExisting = !empty($port['id']) && !str_starts_with((string)$port['id'], 'tmp_');
+ if ($isExisting) {
+ $portId = (int)$port['id'];
+ $ok = $sql->set(
+ 'UPDATE device_type_ports
+ SET name = ?, port_type_id = ?, x = ?, y = ?, metadata = ?
+ WHERE id = ? AND device_type_id = ?',
+ 'siiisii',
+ [$name, $portTypeId, $x, $y, $metadata, $portId, $deviceTypeId]
);
- } else {
+ if ($ok === false) {
+ $sql->set('ROLLBACK');
+ jsonError('Update fehlgeschlagen', 500);
+ }
+ continue;
+ }
- /* ---------- INSERT ---------- */
- $sql->set(
- "INSERT INTO device_type_ports
- (device_type_id, name, port_type_id, pos_x, pos_y, comment)
- VALUES (?, ?, ?, ?, ?, ?)",
- "isidds",
- [
- $deviceTypeId,
- $port['name'],
- $port['port_type_id'],
- $port['x'],
- $port['y'],
- $port['comment']
- ],
- true
- );
+ $ok = $sql->set(
+ 'INSERT INTO device_type_ports (device_type_id, name, port_type_id, x, y, metadata)
+ VALUES (?, ?, ?, ?, ?, ?)',
+ 'isiiis',
+ [$deviceTypeId, $name, $portTypeId, $x, $y, $metadata],
+ true
+ );
+
+ if ($ok === false) {
+ $sql->set('ROLLBACK');
+ jsonError('Insert fehlgeschlagen', 500);
}
}
- echo json_encode([
- 'status' => 'ok'
- ]);
+ $sql->set('COMMIT');
+
+ echo json_encode(['status' => 'ok']);
}
-/**
- * Löscht einen einzelnen Port
- */
-function deletePort($sql)
+function deletePort($sql): void
{
- $id = $_GET['id'] ?? null;
-
- if (!$id) {
- http_response_code(400);
- echo json_encode(['error' => 'ID fehlt']);
- return;
+ if ($_SERVER['REQUEST_METHOD'] !== 'POST' && $_SERVER['REQUEST_METHOD'] !== 'DELETE') {
+ jsonError('Methode nicht erlaubt', 405);
}
- // TODO: Prüfen, ob Port existiert und nicht verwendet wird
+ $id = isset($_GET['id']) ? (int)$_GET['id'] : 0;
+ if ($id <= 0) {
+ jsonError('ID fehlt', 400);
+ }
- $rows = $sql->set(
- "DELETE FROM device_type_ports WHERE id = ?",
- "i",
- [$id]
+ $port = $sql->single('SELECT id, device_type_id, name FROM device_type_ports WHERE id = ?', 'i', [$id]);
+ if (!$port) {
+ jsonError('Port existiert nicht', 404);
+ }
+
+ $usage = $sql->single(
+ 'SELECT COUNT(*) AS cnt
+ FROM devices d
+ JOIN device_ports dp ON dp.device_id = d.id
+ WHERE d.device_type_id = ? AND dp.name = ?',
+ 'is',
+ [(int)$port['device_type_id'], (string)$port['name']]
);
- echo json_encode([
- 'status' => 'deleted',
- 'rows' => $rows
- ]);
+ if (!empty($usage) && (int)$usage['cnt'] > 0) {
+ jsonError('Port wird bereits von realen Geraeten genutzt und kann nicht geloescht werden', 409);
+ }
+
+ $rows = $sql->set('DELETE FROM device_type_ports WHERE id = ?', 'i', [$id]);
+ if ($rows === false) {
+ jsonError('Loeschen fehlgeschlagen', 500);
+ }
+
+ echo json_encode(['status' => 'deleted', 'rows' => $rows]);
}
diff --git a/app/api/upload.php b/app/api/upload.php
index f8a2bd3..2441a8f 100644
--- a/app/api/upload.php
+++ b/app/api/upload.php
@@ -1,133 +1,89 @@
Upload + Rückgabe von Pfad / Metadaten
*/
require_once __DIR__ . '/../bootstrap.php';
+requireAuth();
header('Content-Type: application/json');
-// TODO: Single-User-Auth prüfen
-// if (!$_SESSION['user']) { http_response_code(403); exit; }
-
-/* =========================
- * Konfiguration
- * ========================= */
-
-// TODO: Upload-Basisverzeichnis aus config.php
-$baseUploadDir = __DIR__ . '/../uploads';
-
-// Erlaubte Typen
+$baseUploadDir = defined('UPLOAD_BASE_DIR') ? UPLOAD_BASE_DIR : (__DIR__ . '/../uploads');
+$maxFileSize = defined('UPLOAD_MAX_FILE_SIZE') ? (int)UPLOAD_MAX_FILE_SIZE : (5 * 1024 * 1024);
+$allowedCategories = defined('UPLOAD_ALLOWED_CATEGORIES') ? UPLOAD_ALLOWED_CATEGORIES : ['misc'];
$allowedMimeTypes = [
- 'image/svg+xml',
- 'image/png',
- 'image/jpeg'
+ 'image/svg+xml' => 'svg',
+ 'image/png' => 'png',
+ 'image/jpeg' => 'jpg',
];
-// TODO: Max. Dateigröße festlegen (z.B. 5MB)
-$maxFileSize = 5 * 1024 * 1024;
-
-/* =========================
- * Validierung
- * ========================= */
+if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
+ jsonError('Methode nicht erlaubt', 405);
+}
if (empty($_FILES['file'])) {
- http_response_code(400);
- echo json_encode(['error' => 'Keine Datei hochgeladen']);
- exit;
+ jsonError('Keine Datei hochgeladen', 400);
}
$file = $_FILES['file'];
-
-if ($file['error'] !== UPLOAD_ERR_OK) {
- http_response_code(400);
- echo json_encode(['error' => 'Upload-Fehler']);
- exit;
+if (!is_array($file) || $file['error'] !== UPLOAD_ERR_OK) {
+ jsonError('Upload-Fehler', 400);
}
-if ($file['size'] > $maxFileSize) {
- http_response_code(400);
- echo json_encode(['error' => 'Datei zu groß']);
- exit;
+if ((int)$file['size'] > $maxFileSize) {
+ jsonError('Datei zu gross', 400);
}
-// MIME-Type prüfen
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mimeType = finfo_file($finfo, $file['tmp_name']);
finfo_close($finfo);
-if (!in_array($mimeType, $allowedMimeTypes)) {
- http_response_code(400);
- echo json_encode(['error' => 'Dateityp nicht erlaubt']);
- exit;
+if (!isset($allowedMimeTypes[$mimeType])) {
+ jsonError('Dateityp nicht erlaubt', 400);
}
-/* =========================
- * Zielverzeichnis
- * ========================= */
-
-// TODO: Kategorie definieren (device_types, floors, racks, etc.)
-$category = $_POST['category'] ?? 'misc';
-
-// Zielpfad
-$targetDir = $baseUploadDir . '/' . preg_replace('/[^a-z0-9_-]/i', '', $category);
-
-// Verzeichnis anlegen falls nötig
-if (!is_dir($targetDir)) {
- mkdir($targetDir, 0755, true);
+$category = strtolower(trim((string)($_POST['category'] ?? 'misc')));
+if ($category === '' || !in_array($category, $allowedCategories, true)) {
+ jsonError('Ungueltige Kategorie', 400);
}
-/* =========================
- * Dateiname
- * ========================= */
+$targetDir = rtrim($baseUploadDir, '/\\') . DIRECTORY_SEPARATOR . $category;
+if (!is_dir($targetDir) && !mkdir($targetDir, 0755, true) && !is_dir($targetDir)) {
+ jsonError('Upload-Verzeichnis konnte nicht erstellt werden', 500);
+}
-// Originalname bereinigen
-$extension = pathinfo($file['name'], PATHINFO_EXTENSION);
-
-// TODO: Eindeutigen Namen besser definieren (UUID?)
-$filename = uniqid('upload_', true) . '.' . strtolower($extension);
-
-$targetPath = $targetDir . '/' . $filename;
-
-/* =========================
- * Datei speichern
- * ========================= */
+$extension = $allowedMimeTypes[$mimeType];
+$filename = sprintf('%s_%s.%s', $category, bin2hex(random_bytes(16)), $extension);
+$targetPath = $targetDir . DIRECTORY_SEPARATOR . $filename;
if (!move_uploaded_file($file['tmp_name'], $targetPath)) {
- http_response_code(500);
- echo json_encode(['error' => 'Datei konnte nicht gespeichert werden']);
- exit;
+ jsonError('Datei konnte nicht gespeichert werden', 500);
}
-/* =========================
- * Optional: DB-Eintrag
- * ========================= */
+$publicPath = '/uploads/' . $category . '/' . $filename;
+$uploadId = null;
-// TODO: Optional in Tabelle `uploads` speichern
-// $uploadId = $sql->set(
-// "INSERT INTO uploads (filename, path, mime_type, category)
-// VALUES (?, ?, ?, ?)",
-// "ssss",
-// [$filename, $targetPath, $mimeType, $category],
-// true
-// );
-
-/* =========================
- * Antwort
- * ========================= */
+$uploadTableExists = $sql->single("SHOW TABLES LIKE 'uploads'");
+if (!empty($uploadTableExists)) {
+ $uploadId = $sql->set(
+ 'INSERT INTO uploads (filename, path, mime_type, category) VALUES (?, ?, ?, ?)',
+ 'ssss',
+ [$filename, $publicPath, $mimeType, $category],
+ true
+ );
+}
echo json_encode([
'status' => 'ok',
'filename' => $filename,
- 'path' => str_replace(__DIR__ . '/..', '', $targetPath),
- 'mime_type' => $mimeType
- // 'id' => $uploadId ?? null
+ 'path' => $publicPath,
+ 'mime_type' => $mimeType,
+ 'id' => $uploadId,
]);
+
+function jsonError(string $message, int $status = 400): void
+{
+ http_response_code($status);
+ echo json_encode(['error' => $message]);
+ exit;
+}
diff --git a/app/assets/icons/favicon.svg b/app/assets/icons/favicon.svg
new file mode 100644
index 0000000..0ed9925
--- /dev/null
+++ b/app/assets/icons/favicon.svg
@@ -0,0 +1,4 @@
+
diff --git a/app/assets/js/app.js b/app/assets/js/app.js
index 001bc7f..074c64c 100644
--- a/app/assets/js/app.js
+++ b/app/assets/js/app.js
@@ -1,135 +1,110 @@
/**
* app/assets/js/app.js
- *
- * Zentrale JS-Datei für die Webanwendung
- * - Initialisiert alle Module
- * - SVG-Editor, Netzwerk-Ansicht, Drag & Drop, Floorplan
- * - Event-Handler, globale Variablen
*/
-// =========================
-// Global Variables / Config
-// =========================
-
window.APP = {
- deviceTypes: [], // TODO: alle Gerätetypen laden
- devices: [], // TODO: alle Geräte laden
- racks: [], // TODO: alle Racks laden
- floors: [], // TODO: alle Floors laden
- connections: [], // TODO: alle Verbindungen laden
+ state: {
+ deviceTypes: [],
+ devices: [],
+ racks: [],
+ floors: [],
+ connections: [],
+ },
+ capabilities: {
+ hasGlobalDataApi: false,
+ }
};
-// =========================
-// Init Functions
-// =========================
-
document.addEventListener('DOMContentLoaded', () => {
-
- console.log('App initialized');
-
- // =========================
- // SVG-Port-Editor initialisieren
- // =========================
- // TODO: import / init svg-editor.js
- // if (window.SVGEditor) window.SVGEditor.init();
-
- // =========================
- // Netzwerk-Ansicht initialisieren
- // =========================
- // TODO: import / init network-view.js
- // if (window.NetworkView) window.NetworkView.init();
-
- // =========================
- // Drag & Drop für Floors / Racks / Devices
- // =========================
- // TODO: init drag & drop logic
-
- // =========================
- // Event-Handler für Buttons / Forms
- // =========================
+ initViewModules();
initEventHandlers();
});
-// =========================
-// Event Handler Setup
-// =========================
+function initViewModules() {
+ if (typeof window.Dashboard?.init === 'function') {
+ window.Dashboard.init();
+ }
+
+ // Both modules are loaded via script tags in header.php.
+ // They are self-initializing and only run when expected DOM nodes exist.
+ window.dispatchEvent(new CustomEvent('app:modules-initialized'));
+}
function initEventHandlers() {
-
- // TODO: Save-Button Device-Type
- const saveDeviceTypeBtn = document.querySelector('#save-device-type');
- if (saveDeviceTypeBtn) {
- saveDeviceTypeBtn.addEventListener('click', (e) => {
- e.preventDefault();
- // TODO: Save Device-Type via AJAX
- });
- }
-
- // TODO: Save-Button Device
- const saveDeviceBtn = document.querySelector('#save-device');
- if (saveDeviceBtn) {
- saveDeviceBtn.addEventListener('click', (e) => {
- e.preventDefault();
- // TODO: Save Device via AJAX
- });
- }
-
- // TODO: Save-Button Floor
- const saveFloorBtn = document.querySelector('#save-floor');
- if (saveFloorBtn) {
- saveFloorBtn.addEventListener('click', (e) => {
- e.preventDefault();
- // TODO: Save Floor via AJAX
- });
- }
-
- // TODO: Save-Button Rack
- const saveRackBtn = document.querySelector('#save-rack');
- if (saveRackBtn) {
- saveRackBtn.addEventListener('click', (e) => {
- e.preventDefault();
- // TODO: Save Rack via AJAX
- });
- }
-
- // TODO: Weitere Event-Handler (Import, Export, Filter)
+ bindFormSubmitButton('#save-device-type', 'form[action*="module=device_types"][action*="save"]');
+ bindFormSubmitButton('#save-device', 'form[action*="module=devices"][action*="save"]');
+ bindFormSubmitButton('#save-floor', 'form[action*="module=floors"][action*="save"]');
+ bindFormSubmitButton('#save-rack', 'form[action*="module=racks"][action*="save"]');
document.querySelectorAll('[data-confirm-delete]').forEach((btn) => {
btn.addEventListener('click', (event) => {
event.preventDefault();
- const message = btn.getAttribute('data-confirm-message') || 'Aktion ausführen?';
+ const message = btn.getAttribute('data-confirm-message') || 'Aktion ausfuehren?';
if (confirm(message)) {
- alert(btn.getAttribute('data-confirm-feedback') || 'Diese Funktion ist noch nicht verfügbar.');
+ const href = btn.getAttribute('href') || btn.dataset.href;
+ if (href) {
+ window.location.href = href;
+ }
+ }
+ });
+ });
+
+ document.querySelectorAll('[data-filter-submit]').forEach((el) => {
+ el.addEventListener('change', () => {
+ const form = el.closest('form');
+ if (form) {
+ form.requestSubmit();
}
});
});
}
-// =========================
-// Utility Functions
-// =========================
+function bindFormSubmitButton(buttonSelector, formSelector) {
+ const button = document.querySelector(buttonSelector);
+ if (!button) {
+ return;
+ }
-/**
- * AJAX Request Helper
- * @param {string} url
- * @param {object} data
- * @param {function} callback
- */
-function ajaxPost(url, data, callback) {
+ button.addEventListener('click', (event) => {
+ event.preventDefault();
+ const form = button.closest('form') || document.querySelector(formSelector);
+ if (form) {
+ form.requestSubmit();
+ }
+ });
+}
+
+function ajaxPost(url, data, callback, onError) {
const xhr = new XMLHttpRequest();
xhr.open('POST', url, true);
xhr.setRequestHeader('Content-Type', 'application/json;charset=UTF-8');
- xhr.onload = function() {
+ xhr.onload = function onLoad() {
if (xhr.status >= 200 && xhr.status < 300) {
- callback(JSON.parse(xhr.responseText));
- } else {
- console.error('AJAX Error:', xhr.statusText);
+ let parsed = null;
+ try {
+ parsed = JSON.parse(xhr.responseText);
+ } catch (error) {
+ if (typeof onError === 'function') {
+ onError(error);
+ }
+ return;
+ }
+ callback(parsed);
+ return;
+ }
+
+ if (typeof onError === 'function') {
+ onError(new Error('AJAX error: ' + xhr.status));
}
};
+
+ xhr.onerror = function onXhrError() {
+ if (typeof onError === 'function') {
+ onError(new Error('Netzwerkfehler'));
+ }
+ };
+
xhr.send(JSON.stringify(data));
}
-// TODO: weitere Utility-Funktionen (DOM-Helper, SVG-Helper, etc.)
-
-// Dashboard initialisieren
-if (window.Dashboard) window.Dashboard.init();
+window.APP.ajaxPost = ajaxPost;
diff --git a/app/assets/js/dashboard.js b/app/assets/js/dashboard.js
index f009f70..2237c25 100644
--- a/app/assets/js/dashboard.js
+++ b/app/assets/js/dashboard.js
@@ -1,99 +1,35 @@
/**
* app/assets/js/dashboard.js
- *
- * Dashboard-Modul
- * - Zentrale Übersicht aller Grundfunktionen
- * - Einstiegspunkt für das Tool
- * - Kann später Status, Warnungen, Statistiken anzeigen
*/
window.Dashboard = (function () {
-
- // =========================
- // Interne Daten
- // =========================
-
const modules = [
- {
- id: 'device_types',
- label: 'Gerätetypen',
- description: 'Gerätetypen, Port-Definitionen, Module',
- url: '/app/device_types/list.php',
- icon: '🔌'
- },
- {
- id: 'devices',
- label: 'Geräte',
- description: 'Physische Geräte in Racks und Räumen',
- url: '/app/devices/list.php',
- icon: '🖥️'
- },
- {
- id: 'connections',
- label: 'Verbindungen',
- description: 'Kabel, Ports, VLANs, Protokolle',
- url: '/app/connections/list.php',
- icon: '🧵'
- },
- {
- id: 'floors',
- label: 'Standorte & Stockwerke',
- description: 'Gebäude, Etagen, Räume, Dosen',
- url: '/app/floors/list.php',
- icon: '🏢'
- },
- {
- id: 'racks',
- label: 'Serverschränke',
- description: 'Racks, Positionierung, Höheneinheiten',
- url: '/app/racks/list.php',
- icon: '🗄️'
- },
- {
- id: 'network_view',
- label: 'Netzwerk-Ansicht',
- description: 'Grafische Netzwerkdarstellung',
- url: '/network.php',
- icon: '🌐'
- },
- {
- id: 'svg_editor',
- label: 'SVG-Port-Editor',
- description: 'Ports auf Gerätetypen definieren',
- url: '/svg-editor.php',
- icon: '✏️'
- }
+ { id: 'device_types', label: 'Geraetetypen', description: 'Geraetetypen und Port-Definitionen', url: '?module=device_types&action=list', icon: 'DT' },
+ { id: 'devices', label: 'Geraete', description: 'Physische Geraete in Racks und Raeumen', url: '?module=devices&action=list', icon: 'DV' },
+ { id: 'connections', label: 'Verbindungen', description: 'Kabel, Ports und VLANs', url: '?module=connections&action=list', icon: 'CN' },
+ { id: 'floors', label: 'Stockwerke', description: 'Standorte, Gebaeude und Etagen', url: '?module=floors&action=list', icon: 'FL' },
+ { id: 'racks', label: 'Racks', description: 'Racks und Positionierung', url: '?module=racks&action=list', icon: 'RK' },
+ { id: 'infra', label: 'Infrastruktur', description: 'Patchpanels und Wandbuchsen', url: '?module=floor_infrastructure&action=list', icon: 'IF' }
];
- // =========================
- // Public API
- // =========================
-
function init() {
- console.log('Dashboard initialized');
+ const container = document.querySelector('#dashboard-modules');
+ if (container) {
+ renderModules(container);
+ }
- // TODO: Dashboard-Container ermitteln
- // const container = document.querySelector('#dashboard');
-
- // TODO: Module rendern
- // renderModules(container);
-
- // TODO: Optional: Status-Daten laden (Counts, Warnings)
+ loadStats();
+ showWarnings();
+ renderRecentChanges();
}
- // =========================
- // Render Functions
- // =========================
-
function renderModules(container) {
- if (!container) return;
-
container.innerHTML = '';
- modules.forEach(module => {
- const el = document.createElement('div');
+ modules.forEach((module) => {
+ const el = document.createElement('a');
el.className = 'dashboard-tile';
-
+ el.href = module.url;
el.innerHTML = `
${module.icon}
@@ -101,30 +37,54 @@ window.Dashboard = (function () {
${module.description}
`;
-
- el.addEventListener('click', () => {
- window.location.href = module.url;
- });
-
container.appendChild(el);
});
}
- // =========================
- // Optional Erweiterungen
- // =========================
+ function loadStats() {
+ const stats = {
+ devices: countRows('.device-list tbody tr'),
+ connections: countRows('.connection-list tbody tr'),
+ outlets: countRows('.infra-table tbody tr')
+ };
- // TODO: loadStats() → Anzahl Geräte, offene Ports, unverbundene Dosen
- // TODO: showWarnings() → unverbundene Ports, VLAN-Konflikte
- // TODO: RecentChanges() → letzte Änderungen
+ const target = document.querySelector('[data-dashboard-stats]');
+ if (!target) {
+ return;
+ }
- // =========================
- // Expose Public Methods
- // =========================
+ target.textContent = `Geraete: ${stats.devices} | Verbindungen: ${stats.connections} | Infrastruktur-Eintraege: ${stats.outlets}`;
+ }
- return {
- init,
- // renderModules // optional öffentlich machen
- };
+ function showWarnings() {
+ const target = document.querySelector('[data-dashboard-warnings]');
+ if (!target) {
+ return;
+ }
+ const warnings = [];
+ if (countRows('.device-list tbody tr') === 0) {
+ warnings.push('Noch keine Geraete vorhanden');
+ }
+ if (countRows('.connection-list tbody tr') === 0) {
+ warnings.push('Noch keine Verbindungen vorhanden');
+ }
+
+ target.textContent = warnings.length ? warnings.join(' | ') : 'Keine offenen Warnungen erkannt';
+ }
+
+ function renderRecentChanges() {
+ const target = document.querySelector('[data-dashboard-recent]');
+ if (!target) {
+ return;
+ }
+
+ target.textContent = 'Letzte Aenderungen werden serverseitig noch nicht protokolliert.';
+ }
+
+ function countRows(selector) {
+ return document.querySelectorAll(selector).length;
+ }
+
+ return { init };
})();
diff --git a/app/assets/js/device-type-edit-form.js b/app/assets/js/device-type-edit-form.js
index 57a785d..50cb9bd 100644
--- a/app/assets/js/device-type-edit-form.js
+++ b/app/assets/js/device-type-edit-form.js
@@ -66,10 +66,24 @@
return;
}
- if (window.confirm('Diesen Gerätetyp wirklich löschen? Alle zugeordneten Geräte werden angepasst.')) {
- // TODO: Delete-Endpoint/Flow ist noch nicht implementiert.
- window.alert('Löschen noch nicht implementiert');
+ if (!window.confirm('Diesen Geraetetyp wirklich loeschen?')) {
+ return;
}
+
+ fetch('?module=device_types&action=delete', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' },
+ body: 'id=' + encodeURIComponent(id)
+ })
+ .then((response) => response.json())
+ .then((data) => {
+ if (data && data.success) {
+ window.location.href = '?module=device_types&action=list';
+ return;
+ }
+ window.alert((data && data.message) ? data.message : 'Loeschen fehlgeschlagen');
+ })
+ .catch(() => window.alert('Loeschen fehlgeschlagen'));
});
deleteButton.dataset.deleteBound = '1';
diff --git a/app/assets/js/device-types-list.js b/app/assets/js/device-types-list.js
index b79b61b..c55b40c 100644
--- a/app/assets/js/device-types-list.js
+++ b/app/assets/js/device-types-list.js
@@ -13,8 +13,20 @@
}
if (window.confirm('Diesen Geraetetyp wirklich loeschen?')) {
- // TODO: AJAX-Delete implementieren
- window.alert('Loeschen noch nicht implementiert');
+ fetch('?module=device_types&action=delete', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' },
+ body: 'id=' + encodeURIComponent(id)
+ })
+ .then((response) => response.json())
+ .then((data) => {
+ if (data && data.success) {
+ window.location.reload();
+ return;
+ }
+ window.alert((data && data.message) ? data.message : 'Loeschen fehlgeschlagen');
+ })
+ .catch(() => window.alert('Loeschen fehlgeschlagen'));
}
});
diff --git a/app/assets/js/locations-list.js b/app/assets/js/locations-list.js
index 19cbc2d..4b7caa0 100644
--- a/app/assets/js/locations-list.js
+++ b/app/assets/js/locations-list.js
@@ -21,16 +21,34 @@
.catch(() => alert('Loeschen fehlgeschlagen'));
}
- function handleBuildingDelete() {
- if (confirm('Dieses Gebaeude wirklich loeschen? Alle Stockwerke werden geloescht.')) {
- alert('Loeschen noch nicht implementiert');
+ function handleBuildingDelete(id) {
+ if (!confirm('Dieses Gebaeude wirklich loeschen? Alle Stockwerke werden geloescht.')) {
+ return;
}
+ postDelete('?module=buildings&action=delete&id=' + encodeURIComponent(id))
+ .then((data) => {
+ if (data && (data.success || data.status === 'ok')) {
+ window.location.reload();
+ return;
+ }
+ alert((data && (data.message || data.error)) ? (data.message || data.error) : 'Loeschen fehlgeschlagen');
+ })
+ .catch(() => alert('Loeschen fehlgeschlagen'));
}
- function handleFloorDelete() {
- if (confirm('Dieses Stockwerk wirklich loeschen? Alle Raeume und Racks werden geloescht.')) {
- alert('Loeschen noch nicht implementiert');
+ function handleFloorDelete(id) {
+ if (!confirm('Dieses Stockwerk wirklich loeschen? Alle Raeume und Racks werden geloescht.')) {
+ return;
}
+ postDelete('?module=floors&action=delete&id=' + encodeURIComponent(id))
+ .then((data) => {
+ if (data && data.success) {
+ window.location.reload();
+ return;
+ }
+ alert((data && data.message) ? data.message : 'Loeschen fehlgeschlagen');
+ })
+ .catch(() => alert('Loeschen fehlgeschlagen'));
}
function handleRoomDelete(id) {
diff --git a/app/assets/js/network-view.js b/app/assets/js/network-view.js
index e5fd5d6..0eb7e08 100644
--- a/app/assets/js/network-view.js
+++ b/app/assets/js/network-view.js
@@ -1,61 +1,23 @@
-// Netzwerk-Graph-Ansicht (Nodes, Kanten, Filter)
-/**
- * network-view.js
- *
- * Darstellung der Netzwerk-Topologie:
- * - Geräte als Nodes
- * - Ports als Ankerpunkte
- * - Verbindungen als Linien
- * - Freie / selbstdefinierte Verbindungstypen
- *
- * Kein Layout-Framework (kein D3, kein Cytoscape)
- * -> bewusst simpel & erweiterbar
- */
-
(() => {
-/* =========================
- * Konfiguration
- * ========================= */
+const svgElement = document.querySelector('#network-svg');
+if (!svgElement) {
+ return;
+}
-// 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 CONTEXT_TYPE = svgElement.dataset.contextType || 'all';
+const CONTEXT_ID = Number(svgElement.dataset.contextId || 0);
+const API_LOAD_NETWORK = '/api/connections.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 devices = [];
+let ports = [];
+let connections = [];
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
- * ========================= */
+bindSvgEvents();
+loadNetwork();
function bindSvgEvents() {
svgElement.addEventListener('mousemove', onMouseMove);
@@ -63,37 +25,40 @@ function bindSvgEvents() {
svgElement.addEventListener('click', onSvgClick);
}
-/* =========================
- * Laden
- * ========================= */
+function buildLoadUrl() {
+ const params = new URLSearchParams();
+ params.set('action', 'load');
+ params.set('context_type', CONTEXT_TYPE);
+ if (CONTEXT_TYPE !== 'all') {
+ params.set('context_id', String(CONTEXT_ID));
+ }
+ return '/api/connections.php?' + params.toString();
+}
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 || [];
+ fetch(buildLoadUrl())
+ .then((res) => res.json())
+ .then((data) => {
+ if (!data || !Array.isArray(data.devices) || !Array.isArray(data.connections)) {
+ throw new Error('Antwortformat ungueltig');
+ }
+ devices = data.devices.map((device, index) => ({
+ ...device,
+ x: Number(device.pos_x ?? device.x ?? 50 + (index % 6) * 150),
+ y: Number(device.pos_y ?? device.y ?? 60 + Math.floor(index / 6) * 120)
+ }));
+ ports = Array.isArray(data.ports) ? data.ports : [];
+ connections = data.connections;
renderAll();
})
- .catch(err => {
+ .catch((err) => {
console.error('Fehler beim Laden der Netzwerkansicht', err);
});
}
-/* =========================
- * Rendering
- * ========================= */
-
function renderAll() {
clearSvg();
-
renderConnections();
renderDevices();
}
@@ -104,34 +69,27 @@ function clearSvg() {
}
}
-/* ---------- Geräte ---------- */
-
function renderDevices() {
- devices.forEach(device => renderDevice(device));
+ 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})`);
- 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.classList.add('device-node-rect');
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);
@@ -141,40 +99,59 @@ function renderDevice(device) {
group.appendChild(rect);
group.appendChild(text);
- // TODO: Ports als kleine Kreise anlegen (Position aus Portdefinition)
- // TODO: Ports klickbar machen (für Verbindungs-Erstellung)
+ const devicePorts = ports.filter((port) => Number(port.device_id) === Number(device.id));
+ const spacing = 120 / (Math.max(1, devicePorts.length) + 1);
+ devicePorts.forEach((port, index) => {
+ const dot = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
+ dot.setAttribute('cx', String(Math.round((index + 1) * spacing)));
+ dot.setAttribute('cy', '62');
+ dot.setAttribute('r', '3');
+ dot.classList.add('device-port-dot');
+ dot.dataset.portId = String(port.id);
+ dot.dataset.deviceId = String(device.id);
+ dot.addEventListener('click', (event) => {
+ event.stopPropagation();
+ console.info('Port ausgewaehlt', port.id);
+ });
+ group.appendChild(dot);
+ });
svgElement.appendChild(group);
}
-/* ---------- Verbindungen ---------- */
-
function renderConnections() {
- connections.forEach(conn => renderConnection(conn));
+ connections.forEach((connection) => renderConnection(connection));
}
function renderConnection(connection) {
- // TODO: Quell- & Ziel-Port-Koordinaten berechnen
- // TODO: unterschiedliche Verbindungstypen (Farbe, Strichart, Dicke)
+ const sourcePort = ports.find((port) => Number(port.id) === Number(connection.port_a_id));
+ const targetPort = ports.find((port) => Number(port.id) === Number(connection.port_b_id));
+ if (!sourcePort || !targetPort) {
+ return;
+ }
+
+ const sourceDevice = devices.find((device) => Number(device.id) === Number(sourcePort.device_id));
+ const targetDevice = devices.find((device) => Number(device.id) === Number(targetPort.device_id));
+ if (!sourceDevice || !targetDevice) {
+ return;
+ }
const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
+ line.setAttribute('x1', String(sourceDevice.x + 60));
+ line.setAttribute('y1', String(sourceDevice.y + 60));
+ line.setAttribute('x2', String(targetDevice.x + 60));
+ line.setAttribute('y2', String(targetDevice.y + 60));
- line.setAttribute('x1', 0);
- line.setAttribute('y1', 0);
- line.setAttribute('x2', 100);
- line.setAttribute('y2', 100);
-
+ const isFiber = String(connection.mode || '').toLowerCase().includes('fiber');
line.classList.add('connection-line');
+ line.setAttribute('stroke', isFiber ? '#2f6fef' : '#1f8b4c');
+ line.setAttribute('stroke-width', isFiber ? '2.5' : '2');
+ line.setAttribute('stroke-dasharray', isFiber ? '6 4' : '');
svgElement.appendChild(line);
}
-/* =========================
- * Interaktion
- * ========================= */
-
function onSvgClick(event) {
- // Klick ins Leere -> Auswahl aufheben
if (event.target === svgElement) {
selectedDeviceId = null;
updateSelection();
@@ -202,7 +179,6 @@ function onMouseMove(event) {
if (!device) return;
const point = getSvgCoordinates(event);
-
device.x = point.x + dragOffset.x;
device.y = point.y + dragOffset.y;
@@ -210,31 +186,28 @@ function onMouseMove(event) {
}
function onMouseUp() {
- if (!isDragging) return;
+ 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)
- );
+ svgElement.querySelectorAll('.device-node').forEach((el) => {
+ el.classList.toggle('selected', el.dataset.id === String(selectedDeviceId));
});
- // TODO: Sidebar mit Gerätedetails füllen
-}
+ const sidebar = document.querySelector('[data-network-selected-device]');
+ if (!sidebar) {
+ return;
+ }
-/* =========================
- * Speichern
- * ========================= */
+ const device = getDeviceById(selectedDeviceId);
+ sidebar.textContent = device
+ ? `${device.name} (ID ${device.id})`
+ : 'Kein Geraet ausgewaehlt';
+}
function savePositions() {
fetch(API_SAVE_POSITIONS, {
@@ -242,27 +215,21 @@ function savePositions() {
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
- }))
+ devices: devices.map((device) => ({ id: device.id, x: device.x, y: device.y }))
})
})
- .then(res => res.json())
- .then(data => {
- // TODO: Erfolg / Fehler anzeigen
- console.log('Positionen gespeichert', data);
- })
- .catch(err => {
- console.error('Fehler beim Speichern', err);
- });
+ .then((res) => res.json())
+ .then((data) => {
+ if (data?.error) {
+ throw new Error(data.error);
+ }
+ alert('Positionen gespeichert');
+ })
+ .catch((err) => {
+ alert('Positionen konnten nicht gespeichert werden: ' + err.message);
+ });
}
-/* =========================
- * Hilfsfunktionen
- * ========================= */
-
function getSvgCoordinates(event) {
const pt = svgElement.createSVGPoint();
pt.x = event.clientX;
@@ -273,19 +240,23 @@ function getSvgCoordinates(event) {
}
function getDeviceById(id) {
- return devices.find(d => d.id === id);
+ return devices.find((device) => Number(device.id) === Number(id));
}
-/* =========================
- * Keyboard Shortcuts
- * ========================= */
-
-document.addEventListener('keydown', (e) => {
- if (e.key === 'Escape') {
+document.addEventListener('keydown', (event) => {
+ if (event.key === 'Escape') {
selectedDeviceId = null;
updateSelection();
+ return;
}
- // TODO: Delete -> Gerät entfernen?
+ if (event.key === 'Delete' && selectedDeviceId) {
+ console.warn('Delete von Geraeten ist in der Netzwerkansicht noch nicht implementiert.');
+ }
+
+ if (event.key.toLowerCase() === 's' && (event.ctrlKey || event.metaKey)) {
+ event.preventDefault();
+ savePositions();
+ }
});
})();
diff --git a/app/assets/js/svg-editor.js b/app/assets/js/svg-editor.js
index e94e4a3..84fba51 100644
--- a/app/assets/js/svg-editor.js
+++ b/app/assets/js/svg-editor.js
@@ -1,92 +1,60 @@
-// 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
- * ========================= */
+const svgElement = document.querySelector('#device-svg');
+if (!svgElement) {
+ return;
+}
-// TODO: vom Backend setzen (z. B. via data-Attribut)
-const DEVICE_TYPE_ID = null;
-
-// TODO: API-Endpunkte festlegen
+const DEVICE_TYPE_ID = Number(svgElement.dataset.deviceTypeId || 0);
const API_LOAD_PORTS = '/api/device_type_ports.php?action=load';
const API_SAVE_PORTS = '/api/device_type_ports.php?action=save';
+const DEFAULT_PORT_TYPE_ID = null;
-/* =========================
- * 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
- * ========================= */
+bindSvgEvents();
+loadPorts();
function bindSvgEvents() {
svgElement.addEventListener('click', onSvgClick);
svgElement.addEventListener('mousemove', onSvgMouseMove);
svgElement.addEventListener('mouseup', onSvgMouseUp);
+
+ const saveButton = document.querySelector('[data-save-svg-ports]');
+ if (saveButton) {
+ saveButton.addEventListener('click', (event) => {
+ event.preventDefault();
+ savePorts();
+ });
+ }
}
-/* =========================
- * 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);
+ // New ports are only created while SHIFT is held.
+ if (!event.shiftKey) {
+ return;
+ }
+ const point = getSvgCoordinates(event);
createPort(point.x, point.y);
}
function createPort(x, y) {
const id = generateTempId();
-
const port = {
- id: id,
+ id,
name: `Port ${ports.length + 1}`,
- port_type_id: null, // TODO: Default-Porttyp?
- x: x,
- y: y,
- comment: ''
+ port_type_id: DEFAULT_PORT_TYPE_ID,
+ x,
+ y,
+ metadata: null
};
ports.push(port);
@@ -94,13 +62,8 @@ function createPort(x, y) {
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);
@@ -116,27 +79,39 @@ function renderPort(port) {
}
function rerenderPorts() {
- svgElement.querySelectorAll('.port-point').forEach(p => p.remove());
+ svgElement.querySelectorAll('.port-point').forEach((node) => node.remove());
ports.forEach(renderPort);
+ if (selectedPortId !== null) {
+ selectPort(selectedPortId);
+ }
}
-/* =========================
- * Auswahl
- * ========================= */
-
function selectPort(id) {
selectedPortId = id;
- document.querySelectorAll('.port-point').forEach(el => {
- el.classList.toggle('selected', el.dataset.id === id);
+ document.querySelectorAll('.port-point').forEach((el) => {
+ el.classList.toggle('selected', el.dataset.id === String(id));
});
- // TODO: Sidebar-Felder mit Portdaten füllen
+ const selected = getPortById(id);
+ fillSidebar(selected);
}
-/* =========================
- * Drag & Drop
- * ========================= */
+function fillSidebar(port) {
+ const nameField = document.querySelector('[data-port-name]');
+ const typeField = document.querySelector('[data-port-type-id]');
+ const xField = document.querySelector('[data-port-x]');
+ const yField = document.querySelector('[data-port-y]');
+
+ if (nameField) nameField.value = port?.name || '';
+ if (typeField) typeField.value = port?.port_type_id || '';
+ if (xField) xField.value = port ? Math.round(port.x) : '';
+ if (yField) yField.value = port ? Math.round(port.y) : '';
+}
+
+function resetSidebar() {
+ fillSidebar(null);
+}
function startDrag(event, portId) {
const port = getPortById(portId);
@@ -157,7 +132,6 @@ function onSvgMouseMove(event) {
if (!port) return;
const point = getSvgCoordinates(event);
-
port.x = point.x + dragOffset.x;
port.y = point.y + dragOffset.y;
@@ -168,92 +142,95 @@ function onSvgMouseUp() {
isDragging = false;
}
-/* =========================
- * Löschen
- * ========================= */
-
function deleteSelectedPort() {
- if (!selectedPortId) return;
+ if (!selectedPortId) {
+ return;
+ }
- // TODO: Sicherheitsabfrage (confirm)
- ports = ports.filter(p => p.id !== selectedPortId);
+ if (!confirm('Ausgewaehlten Port loeschen?')) {
+ return;
+ }
+
+ ports = ports.filter((port) => String(port.id) !== String(selectedPortId));
selectedPortId = null;
-
rerenderPorts();
-
- // TODO: Sidebar zurücksetzen
+ resetSidebar();
}
-/* =========================
- * Laden / Speichern
- * ========================= */
-
function loadPorts() {
if (!DEVICE_TYPE_ID) {
- console.warn('DEVICE_TYPE_ID nicht gesetzt');
+ console.warn('SVG Editor: DEVICE_TYPE_ID fehlt auf #device-svg');
return;
}
fetch(`${API_LOAD_PORTS}&device_type_id=${DEVICE_TYPE_ID}`)
- .then(res => res.json())
- .then(data => {
- // TODO: Datenformat validieren
- ports = data;
+ .then((res) => res.json())
+ .then((data) => {
+ if (!Array.isArray(data)) {
+ throw new Error('Antwortformat ungueltig');
+ }
+
+ ports = data
+ .filter((entry) => entry && typeof entry === 'object')
+ .map((entry) => ({
+ id: entry.id,
+ name: String(entry.name || ''),
+ port_type_id: entry.port_type_id ? Number(entry.port_type_id) : null,
+ x: Number(entry.x || 0),
+ y: Number(entry.y || 0),
+ metadata: entry.metadata || null
+ }));
+
rerenderPorts();
})
- .catch(err => {
+ .catch((err) => {
console.error('Fehler beim Laden der Ports', err);
});
}
function savePorts() {
- if (!DEVICE_TYPE_ID) return;
+ 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
+ ports
})
})
- .then(res => res.json())
- .then(data => {
- // TODO: Erfolg / Fehler anzeigen
- console.log('Ports gespeichert', data);
- })
- .catch(err => {
- console.error('Fehler beim Speichern', err);
- });
+ .then((res) => res.json())
+ .then((data) => {
+ if (data?.error) {
+ throw new Error(data.error);
+ }
+ alert('Ports gespeichert');
+ })
+ .catch((err) => {
+ alert('Speichern fehlgeschlagen: ' + err.message);
+ });
}
-/* =========================
- * 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);
+ return ports.find((port) => String(port.id) === String(id));
}
function generateTempId() {
- return 'tmp_' + Math.random().toString(36).substr(2, 9);
+ return 'tmp_' + Math.random().toString(36).slice(2, 11);
}
-/* =========================
- * Keyboard Shortcuts
- * ========================= */
-
-document.addEventListener('keydown', (e) => {
- if (e.key === 'Delete') {
+document.addEventListener('keydown', (event) => {
+ if (event.key === 'Delete') {
deleteSelectedPort();
}
});
diff --git a/app/bootstrap.php b/app/bootstrap.php
index 591373d..fbb71df 100644
--- a/app/bootstrap.php
+++ b/app/bootstrap.php
@@ -2,41 +2,18 @@
/**
* bootstrap.php
*
- * Initialisierung der Anwendung
- * - Config laden
- * - Session starten
- * - DB-Verbindung über _sql.php
- * - Helper einbinden
+ * Application initialization.
*/
-/* =========================
- * Config laden
- * ========================= */
require_once __DIR__ . '/config.php';
-// TODO: Config-Datei mit DB-Zugang, Pfaden, globalen Settings füllen
+date_default_timezone_set(defined('APP_TIMEZONE') ? APP_TIMEZONE : 'UTC');
-/* =========================
- * Session starten
- * ========================= */
-session_start();
-// TODO: Single-User Auth prüfen
-// z.B. $_SESSION['user'] setzen oder Login erzwingen
+if (session_status() !== PHP_SESSION_ACTIVE) {
+ session_start();
+}
-/* =========================
- * DB-Verbindung initialisieren
- * ========================= */
require_once __DIR__ . '/lib/_sql.php';
-
-// TODO: Host, User, Passwort, DB aus config.php nutzen
$sql = new SQL();
-/* =========================
- * Helper laden
- * ========================= */
require_once __DIR__ . '/lib/helpers.php';
-
-/* =========================
- * Optional: Fehlerbehandlung
- * ========================= */
-// error_reporting(E_ALL);
-// ini_set('display_errors', 1);
+require_once __DIR__ . '/lib/auth.php';
diff --git a/app/config.php b/app/config.php
index 35584f3..d4a20ef 100644
--- a/app/config.php
+++ b/app/config.php
@@ -1,2 +1,19 @@
Die Seite existiert noch nicht.
".$modulePath;
- }
+ renderClientError(404, 'Die angeforderte Seite existiert nicht.');
}
-/* =========================
- * Template-Footer laden (nur für View-Aktionen)
- * ========================= */
if (!in_array($action, ['save', 'delete'], true)) {
require_once __DIR__ . '/templates/footer.php';
}
-
diff --git a/app/lib/_sql.php b/app/lib/_sql.php
index 474060f..5b35112 100644
--- a/app/lib/_sql.php
+++ b/app/lib/_sql.php
@@ -8,11 +8,19 @@ class SQL {
public $cnt_get = 0;
public $cnt_set = 0;
public function __construct() {
- require_once ('secret.php');
+ if (defined('DB_HOST') && defined('DB_USER') && defined('DB_PASS') && defined('DB_NAME')) {
+ $this->m = [
+ 'host' => DB_HOST,
+ 'user' => DB_USER,
+ 'pass' => DB_PASS,
+ 'data' => DB_NAME
+ ];
+ } else {
+ require_once ('secret.php');
+ $this->m = $_m;
+ }
- $this->m = $_m;
-
- $this->h = new mysqli ( $_m ['host'], $_m ['user'], $_m ['pass'], $_m ['data'] );
+ $this->h = new mysqli ( $this->m ['host'], $this->m ['user'], $this->m ['pass'], $this->m ['data'] );
if ($this->h->connect_errno) {
return false;
}
@@ -171,4 +179,4 @@ class SQL {
// echo 'DESTROY';
}
}
-?>
\ No newline at end of file
+?>
diff --git a/app/lib/auth.php b/app/lib/auth.php
index 11a1b13..bcb63dd 100644
--- a/app/lib/auth.php
+++ b/app/lib/auth.php
@@ -2,82 +2,60 @@
/**
* app/lib/auth.php
*
- * Single-User-Authentifizierung
- * - Login / Logout
- * - Session-Check
- * - Optional: Passwortschutz für Admin-Tool
- *
- * KEIN Mehrbenutzer-System
+ * Single-user authentication helpers.
*/
-/* =========================
- * Login prüfen
- * ========================= */
-
-/**
- * Prüft, ob der Benutzer eingeloggt ist
- *
- * @return bool
- */
function isAuthenticated(): bool
{
- // TODO: Session-Variable definieren, z.B. $_SESSION['auth'] === true
+ if (!defined('AUTH_REQUIRED') || AUTH_REQUIRED === false) {
+ return true;
+ }
+
return isset($_SESSION['auth']) && $_SESSION['auth'] === true;
}
-/* =========================
- * Login durchführen
- * ========================= */
-
-/**
- * Führt einen Login durch
- *
- * @param string $password
- * @return bool
- */
function login(string $password): bool
{
- // TODO: Passwort aus config.php vergleichen
- // TODO: Passwort-Hash verwenden (password_hash / password_verify)
+ $hash = defined('ADMIN_PASSWORD_HASH') ? trim((string)ADMIN_PASSWORD_HASH) : '';
+ if ($hash === '') {
+ return false;
+ }
- /*
- if (password_verify($password, ADMIN_PASSWORD_HASH)) {
+ if (password_verify($password, $hash)) {
$_SESSION['auth'] = true;
+ $_SESSION['auth_at'] = time();
return true;
}
- */
return false;
}
-/* =========================
- * Logout
- * ========================= */
-
-/**
- * Loggt den Benutzer aus
- */
function logout(): void
{
- // TODO: Session-Variablen löschen
- // unset($_SESSION['auth']);
+ unset($_SESSION['auth'], $_SESSION['auth_at']);
- // TODO: Optional komplette Session zerstören
- // session_destroy();
+ if (session_status() === PHP_SESSION_ACTIVE) {
+ session_regenerate_id(true);
+ }
}
-/* =========================
- * Zugriff erzwingen
- * ========================= */
-
-/**
- * Erzwingt Login, sonst Redirect
- */
function requireAuth(): void
{
+ if (!defined('AUTH_REQUIRED') || AUTH_REQUIRED === false) {
+ return;
+ }
+
if (!isAuthenticated()) {
- // TODO: Redirect auf Login-Seite
- // header('Location: /login.php');
+ $isApiRequest = str_starts_with($_SERVER['REQUEST_URI'] ?? '', '/api/');
+ if ($isApiRequest) {
+ http_response_code(401);
+ header('Content-Type: application/json');
+ echo json_encode(['error' => 'Nicht authentifiziert']);
+ exit;
+ }
+
+ $target = defined('LOGIN_PATH') ? LOGIN_PATH : '/login.php';
+ header('Location: ' . $target);
exit;
}
}
diff --git a/app/modules/buildings/delete.php b/app/modules/buildings/delete.php
new file mode 100644
index 0000000..65819c6
--- /dev/null
+++ b/app/modules/buildings/delete.php
@@ -0,0 +1,35 @@
+ 'Methode nicht erlaubt']);
+ exit;
+}
+
+header('Content-Type: application/json');
+
+$id = (int)($_POST['id'] ?? $_GET['id'] ?? 0);
+if ($id <= 0) {
+ http_response_code(400);
+ echo json_encode(['error' => 'ID fehlt']);
+ exit;
+}
+
+$exists = $sql->single("SELECT id FROM buildings WHERE id = ?", "i", [$id]);
+if (!$exists) {
+ http_response_code(404);
+ echo json_encode(['error' => 'Gebaeude nicht gefunden']);
+ exit;
+}
+
+$rows = $sql->set("DELETE FROM buildings WHERE id = ?", "i", [$id]);
+if ($rows === false) {
+ http_response_code(500);
+ echo json_encode(['error' => 'Loeschen fehlgeschlagen']);
+ exit;
+}
+
+echo json_encode(['status' => 'ok', 'success' => true, 'rows' => $rows]);
diff --git a/app/modules/buildings/edit.php b/app/modules/buildings/edit.php
index f315e43..ff4ea76 100644
--- a/app/modules/buildings/edit.php
+++ b/app/modules/buildings/edit.php
@@ -47,7 +47,7 @@ $selectedLocationId = $building['location_id'] ?? $prefillLocationId;
@@ -57,7 +57,7 @@ $selectedLocationId = $building['location_id'] ?? $prefillLocationId;
@@ -67,7 +67,7 @@ $selectedLocationId = $building['location_id'] ?? $prefillLocationId;
+ placeholder="Adresse, Besonderheiten, etc.">
@@ -172,9 +172,25 @@ $selectedLocationId = $building['location_id'] ?? $prefillLocationId;
+
+
diff --git a/app/modules/buildings/list.php b/app/modules/buildings/list.php
index 3a147c5..01889d7 100644
--- a/app/modules/buildings/list.php
+++ b/app/modules/buildings/list.php
@@ -8,17 +8,17 @@
// =========================
// Filter einlesen
// =========================
-$search = trim($_GET['search'] ?? '');
+$search = trim($_GET['search'] ?? ');
$locationId = (int)($_GET['location_id'] ?? 0);
// =========================
// WHERE-Clause bauen
// =========================
$where = [];
-$types = '';
+$types = ';
$params = [];
-if ($search !== '') {
+if ($search !== ') {
$where[] = "b.name LIKE ? OR b.comment LIKE ?";
$types .= "ss";
$params[] = "%$search%";
@@ -70,7 +70,7 @@ $locations = $sql->get("SELECT id, name FROM locations ORDER BY name", "", []);
@@ -112,7 +112,7 @@ $locations = $sql->get("SELECT id, name FROM locations ORDER BY name", "", []);
-
+
|
@@ -241,9 +241,24 @@ $locations = $sql->get("SELECT id, name FROM locations ORDER BY name", "", []);
+
diff --git a/app/modules/dashboard/list.php b/app/modules/dashboard/list.php
index 593036f..71c4593 100644
--- a/app/modules/dashboard/list.php
+++ b/app/modules/dashboard/list.php
@@ -34,6 +34,10 @@ $recentDevices = $sql->get(
Dashboard
+
+
+
+
@@ -100,6 +104,54 @@ $recentDevices = $sql->get(
margin: 20px 0;
}
+.dashboard-modules {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
+ gap: 12px;
+ margin: 12px 0 20px;
+}
+
+.dashboard-tile {
+ display: flex;
+ gap: 10px;
+ align-items: center;
+ border: 1px solid #d7d7d7;
+ border-radius: 8px;
+ padding: 12px;
+ text-decoration: none;
+ color: #222;
+ background: #fff;
+}
+
+.dashboard-icon {
+ width: 34px;
+ height: 34px;
+ border-radius: 999px;
+ background: #0c4da2;
+ color: #fff;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 0.75rem;
+ font-weight: 700;
+}
+
+.dashboard-content h3 {
+ margin: 0 0 4px;
+ font-size: 1rem;
+}
+
+.dashboard-content p {
+ margin: 0;
+ font-size: 0.85rem;
+ color: #444;
+}
+
+.dashboard-inline-status {
+ margin: 6px 0;
+ color: #333;
+}
+
.stat-card {
border: 1px solid #ddd;
padding: 20px;
@@ -143,4 +195,4 @@ $recentDevices = $sql->get(
.recent-devices th {
background: #f0f0f0;
}
-
\ No newline at end of file
+
diff --git a/app/modules/device_types/delete.php b/app/modules/device_types/delete.php
new file mode 100644
index 0000000..11841e6
--- /dev/null
+++ b/app/modules/device_types/delete.php
@@ -0,0 +1,39 @@
+ false, 'message' => 'Methode nicht erlaubt']);
+ exit;
+}
+
+$id = (int)($_POST['id'] ?? $_GET['id'] ?? 0);
+if ($id <= 0) {
+ http_response_code(400);
+ echo json_encode(['success' => false, 'message' => 'ID fehlt']);
+ exit;
+}
+
+$exists = $sql->single("SELECT id FROM device_types WHERE id = ?", "i", [$id]);
+if (!$exists) {
+ http_response_code(404);
+ echo json_encode(['success' => false, 'message' => 'Geraetetyp nicht gefunden']);
+ exit;
+}
+
+$rows = $sql->set("DELETE FROM device_types WHERE id = ?", "i", [$id]);
+if ($rows === false) {
+ http_response_code(409);
+ echo json_encode([
+ 'success' => false,
+ 'message' => 'Geraetetyp konnte nicht geloescht werden (wird ggf. noch von Geraeten verwendet)'
+ ]);
+ exit;
+}
+
+echo json_encode(['success' => true, 'message' => 'Geraetetyp geloescht']);
+
diff --git a/app/modules/device_types/edit.php b/app/modules/device_types/edit.php
index 981b826..b77051f 100644
--- a/app/modules/device_types/edit.php
+++ b/app/modules/device_types/edit.php
@@ -15,7 +15,6 @@ $deviceTypeId = (int)($_GET['id'] ?? 0);
$deviceType = null;
$ports = [];
-//TODO port hinzufügen geht nicht
if ($deviceTypeId > 0) {
$deviceType = $sql->single(
@@ -339,3 +338,4 @@ $pageTitle = $isEdit ? "Gerätetyp bearbeiten: " . htmlspecialchars($deviceType[
+
diff --git a/app/modules/device_types/ports.php b/app/modules/device_types/ports.php
index af91f0a..d449e0c 100644
--- a/app/modules/device_types/ports.php
+++ b/app/modules/device_types/ports.php
@@ -1,265 +1,165 @@
single(
+ "SELECT id, name, image_path, image_type FROM device_types WHERE id = ?",
+ 'i',
+ [$deviceTypeId]
+);
-// =========================
-// Kontext bestimmen
-// =========================
+if (!$deviceType) {
+ renderClientError(404, 'Geraetetyp nicht gefunden');
+ return;
+}
-// TODO: device_type_id aus GET lesen
-// $deviceTypeId = (int)($_GET['id'] ?? 0);
+if ($_SERVER['REQUEST_METHOD'] === 'POST') {
+ $formAction = $_POST['form_action'] ?? '';
-// TODO: Gerätetyp laden
-// $deviceType = null;
+ if ($formAction === 'add_port') {
+ $name = trim((string)($_POST['name'] ?? ''));
+ $portTypeId = (int)($_POST['port_type_id'] ?? 0);
+ $x = (int)($_POST['x'] ?? 0);
+ $y = (int)($_POST['y'] ?? 0);
-// TODO: Ports dieses Gerätetyps laden
-// $ports = [];
+ if ($name !== '') {
+ if ($portTypeId > 0) {
+ $sql->set(
+ "INSERT INTO device_type_ports (device_type_id, name, port_type_id, x, y) VALUES (?, ?, ?, ?, ?)",
+ "isiii",
+ [$deviceTypeId, $name, $portTypeId, $x, $y]
+ );
+ } else {
+ $sql->set(
+ "INSERT INTO device_type_ports (device_type_id, name, port_type_id, x, y) VALUES (?, ?, NULL, ?, ?)",
+ "isii",
+ [$deviceTypeId, $name, $x, $y]
+ );
+ }
+ $_SESSION['success'] = 'Port hinzugefuegt';
+ } else {
+ $_SESSION['error'] = 'Portname darf nicht leer sein';
+ }
+ header('Location: ?module=device_types&action=ports&id=' . $deviceTypeId);
+ exit;
+ }
+
+ if ($formAction === 'delete_port') {
+ $portId = (int)($_POST['port_id'] ?? 0);
+ if ($portId > 0) {
+ $sql->set(
+ "DELETE FROM device_type_ports WHERE id = ? AND device_type_id = ?",
+ "ii",
+ [$portId, $deviceTypeId]
+ );
+ $_SESSION['success'] = 'Port geloescht';
+ }
+
+ header('Location: ?module=device_types&action=ports&id=' . $deviceTypeId);
+ exit;
+ }
+}
+
+$ports = $sql->get(
+ "SELECT dtp.id, dtp.name, dtp.port_type_id, dtp.x, dtp.y, pt.name AS port_type_name
+ FROM device_type_ports dtp
+ LEFT JOIN port_types pt ON pt.id = dtp.port_type_id
+ WHERE dtp.device_type_id = ?
+ ORDER BY dtp.id ASC",
+ 'i',
+ [$deviceTypeId]
+);
+
+$portTypes = $sql->get("SELECT id, name FROM port_types ORDER BY name", '', []);
+$svgPath = trim((string)($deviceType['image_path'] ?? ''));
+$svgUrl = $svgPath !== '' ? '/' . ltrim($svgPath, '/\\') : '';
?>
- Ports – Gerätetyp
+
+ Ports:
-
+
-
- Gerätetypen
- →
-
-
- Gerätetyp
-
- →
- Ports
+
+
+
+
+
+
+
+ | # |
+ Name |
+ Typ |
+ X |
+ Y |
+ Aktionen |
+
+
+
+
+
+ |
+ |
+ |
+ |
+ |
+
+
+ |
+
+
+
+
+
+
+
+ SVG Vorschau
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- | # |
- Name |
- Typ |
- Medium |
- Modus |
- VLAN |
- Aktionen |
-
-
-
-
-
-
- |
-
- 1
- |
-
-
- Port 1
- |
-
-
- |
-
-
- |
-
-
- |
-
-
- |
-
-
-
-
- |
-
-
-
-
-
-
-
-
-
- Port-Positionen
-
-
- Ports per Drag & Drop auf dem Gerät platzieren.
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/modules/floor_infrastructure/delete.php b/app/modules/floor_infrastructure/delete.php
new file mode 100644
index 0000000..8b2ce33
--- /dev/null
+++ b/app/modules/floor_infrastructure/delete.php
@@ -0,0 +1,31 @@
+set(
+ "DELETE FROM floor_patchpanels WHERE id = ?",
+ "i",
+ [$id]
+ );
+} else {
+ $sql->set(
+ "DELETE FROM network_outlets WHERE id = ?",
+ "i",
+ [$id]
+ );
+}
+
+header('Location: ?module=floor_infrastructure&action=list');
+exit;
diff --git a/app/modules/floor_infrastructure/edit.php b/app/modules/floor_infrastructure/edit.php
index b67e2c0..f6441cb 100644
--- a/app/modules/floor_infrastructure/edit.php
+++ b/app/modules/floor_infrastructure/edit.php
@@ -293,11 +293,8 @@ $mapOutlets = $sql->get(
-
+
diff --git a/app/modules/floor_infrastructure/list.php b/app/modules/floor_infrastructure/list.php
index 8b9cd48..aff72f7 100644
--- a/app/modules/floor_infrastructure/list.php
+++ b/app/modules/floor_infrastructure/list.php
@@ -180,6 +180,7 @@ if ($editorFloor) {
|
Bearbeiten
+ Loeschen
|
@@ -225,6 +226,7 @@ if ($editorFloor) {
|
Bearbeiten
+ Loeschen
|
diff --git a/app/modules/floors/delete.php b/app/modules/floors/delete.php
new file mode 100644
index 0000000..2bb7917
--- /dev/null
+++ b/app/modules/floors/delete.php
@@ -0,0 +1,36 @@
+ false, 'message' => 'Methode nicht erlaubt']);
+ exit;
+}
+
+$id = (int)($_POST['id'] ?? $_GET['id'] ?? 0);
+if ($id <= 0) {
+ http_response_code(400);
+ echo json_encode(['success' => false, 'message' => 'ID fehlt']);
+ exit;
+}
+
+$exists = $sql->single("SELECT id FROM floors WHERE id = ?", "i", [$id]);
+if (!$exists) {
+ http_response_code(404);
+ echo json_encode(['success' => false, 'message' => 'Stockwerk nicht gefunden']);
+ exit;
+}
+
+$rows = $sql->set("DELETE FROM floors WHERE id = ?", "i", [$id]);
+if ($rows === false) {
+ http_response_code(500);
+ echo json_encode(['success' => false, 'message' => 'Loeschen fehlgeschlagen']);
+ exit;
+}
+
+echo json_encode(['success' => true, 'message' => 'Stockwerk geloescht']);
+
diff --git a/app/modules/floors/list.php b/app/modules/floors/list.php
index 4c066bb..15a5cdb 100644
--- a/app/modules/floors/list.php
+++ b/app/modules/floors/list.php
@@ -10,7 +10,7 @@
// =========================
// Filter einlesen
// =========================
-$search = trim($_GET['search'] ?? '');
+$search = trim($_GET['search'] ?? ');
// =========================
// Floors laden
@@ -19,7 +19,7 @@ $whereClause = "";
$types = "";
$params = [];
-if ($search !== '') {
+if ($search !== ') {
$whereClause = "WHERE f.name LIKE ? OR f.comment LIKE ?";
$types = "ss";
$params = ["%$search%", "%$search%"];
@@ -105,7 +105,7 @@ $buildings = $sql->get("SELECT id, name FROM buildings ORDER BY name", "", []);
|
-
+
|
@@ -233,9 +233,24 @@ $buildings = $sql->get("SELECT id, name FROM buildings ORDER BY name", "", []);
+
diff --git a/app/modules/locations/edit.php b/app/modules/locations/edit.php
index 3dcbfe5..3db6b05 100644
--- a/app/modules/locations/edit.php
+++ b/app/modules/locations/edit.php
@@ -42,14 +42,14 @@ $pageTitle = $isEdit ? "Standort bearbeiten: " . htmlspecialchars($location['nam
+ placeholder="Adresse, Kontaktinformationen, Besonderheiten">
@@ -153,9 +153,24 @@ $pageTitle = $isEdit ? "Standort bearbeiten: " . htmlspecialchars($location['nam
+
diff --git a/app/modules/racks/delete.php b/app/modules/racks/delete.php
new file mode 100644
index 0000000..b627162
--- /dev/null
+++ b/app/modules/racks/delete.php
@@ -0,0 +1,36 @@
+ false, 'message' => 'Methode nicht erlaubt']);
+ exit;
+}
+
+$id = (int)($_POST['id'] ?? $_GET['id'] ?? 0);
+if ($id <= 0) {
+ http_response_code(400);
+ echo json_encode(['success' => false, 'message' => 'ID fehlt']);
+ exit;
+}
+
+$exists = $sql->single("SELECT id FROM racks WHERE id = ?", "i", [$id]);
+if (!$exists) {
+ http_response_code(404);
+ echo json_encode(['success' => false, 'message' => 'Rack nicht gefunden']);
+ exit;
+}
+
+$rows = $sql->set("DELETE FROM racks WHERE id = ?", "i", [$id]);
+if ($rows === false) {
+ http_response_code(500);
+ echo json_encode(['success' => false, 'message' => 'Loeschen fehlgeschlagen']);
+ exit;
+}
+
+echo json_encode(['success' => true, 'message' => 'Rack geloescht']);
+
diff --git a/app/modules/racks/edit.php b/app/modules/racks/edit.php
index 24fe8fd..a63de09 100644
--- a/app/modules/racks/edit.php
+++ b/app/modules/racks/edit.php
@@ -1,16 +1,8 @@
0) {
}
$isEdit = !empty($rack);
-$pageTitle = $isEdit ? "Rack bearbeiten: " . htmlspecialchars($rack['name']) : "Neues Rack";
-
-// =========================
-// Floors laden
-// =========================
-$floors = $sql->get("SELECT id, name FROM floors ORDER BY name", "", []);
-
+$pageTitle = $isEdit ? 'Rack bearbeiten: ' . htmlspecialchars((string)$rack['name']) : 'Neues Rack';
+$floors = $sql->get("SELECT id, name FROM floors ORDER BY name", '', []);
?>
@@ -197,60 +167,25 @@ $floors = $sql->get("SELECT id, name FROM floors ORDER BY name", "", []);
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/modules/racks/list.php b/app/modules/racks/list.php
index b4d0d43..6dfd747 100644
--- a/app/modules/racks/list.php
+++ b/app/modules/racks/list.php
@@ -2,44 +2,30 @@
/**
* app/modules/racks/list.php
*
- * Übersicht aller Racks
- * - Anzeigen, Bearbeiten, Löschen
- * - Zugehöriges Stockwerk anzeigen
- * - Gerätecount
+ * Uebersicht aller Racks.
*/
-// =========================
-// Filter einlesen
-// =========================
-$search = trim($_GET['search'] ?? '');
+$search = trim((string)($_GET['search'] ?? ''));
$floorId = (int)($_GET['floor_id'] ?? 0);
-//TODO racks beim editieren auf der stockwerkkarte platzieren und verschieben können
-
-// =========================
-// WHERE-Clause bauen
-// =========================
$where = [];
$types = '';
$params = [];
if ($search !== '') {
- $where[] = "r.name LIKE ?";
- $types .= "s";
+ $where[] = 'r.name LIKE ?';
+ $types .= 's';
$params[] = "%$search%";
}
if ($floorId > 0) {
- $where[] = "r.floor_id = ?";
- $types .= "i";
+ $where[] = 'r.floor_id = ?';
+ $types .= 'i';
$params[] = $floorId;
}
-$whereSql = $where ? "WHERE " . implode(" AND ", $where) : "";
+$whereSql = $where ? ('WHERE ' . implode(' AND ', $where)) : '';
-// =========================
-// Racks laden
-// =========================
$racks = $sql->get(
"SELECT r.*, f.name AS floor_name, COUNT(d.id) AS device_count
FROM racks r
@@ -52,212 +38,111 @@ $racks = $sql->get(
$params
);
-// =========================
-// Filter-Daten laden
-// =========================
-$floors = $sql->get("SELECT id, name FROM floors ORDER BY name", "", []);
-
+$floors = $sql->get('SELECT id, name FROM floors ORDER BY name', '', []);
?>
Racks
-
-
-
-
-
- | Name |
- Stockwerk |
- Höhe (HE) |
- Geräte |
- Beschreibung |
- Aktionen |
-
-
-
-
-
- |
-
- |
-
-
-
- |
-
-
- HE
- |
-
-
-
- |
-
-
-
- |
-
-
- Bearbeiten
- Löschen
- |
-
-
-
-
-
+
+
+
+ | Name |
+ Stockwerk |
+ Hoehe (HE) |
+ Geraete |
+ Beschreibung |
+ Aktionen |
+
+
+
+
+
+ |
+ |
+ HE |
+ |
+ |
+
+ Bearbeiten
+ Loeschen
+ |
+
+
+
+
-
+
-
-
diff --git a/app/templates/footer.php b/app/templates/footer.php
index d0c639e..6e333c5 100644
--- a/app/templates/footer.php
+++ b/app/templates/footer.php
@@ -1,19 +1,32 @@
-
+
+
+
+
|