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)) { +if (!in_array($action, ['save', 'delete', 'swap'], 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/connections/edit.php b/app/modules/connections/edit.php index 0a0d75a..331eb86 100644 --- a/app/modules/connections/edit.php +++ b/app/modules/connections/edit.php @@ -46,6 +46,68 @@ $endpointOptions = [ 'patchpanel' => [], ]; +$occupiedByType = [ + 'device' => [], + 'module' => [], + 'outlet' => [], + 'patchpanel' => [], +]; +$occupiedRows = $sql->get( + "SELECT id, port_a_type, port_a_id, port_b_type, port_b_id + FROM connections + WHERE id <> ?", + "i", + [$connectionId] +); +foreach ((array)$occupiedRows as $row) { + $typeA = $normalizePortType((string)($row['port_a_type'] ?? '')); + $idA = (int)($row['port_a_id'] ?? 0); + if ($idA > 0 && isset($occupiedByType[$typeA])) { + $occupiedByType[$typeA][$idA] = true; + } + + $typeB = $normalizePortType((string)($row['port_b_type'] ?? '')); + $idB = (int)($row['port_b_id'] ?? 0); + if ($idB > 0 && isset($occupiedByType[$typeB])) { + $occupiedByType[$typeB][$idB] = true; + } +} + +$isEndpointAllowed = static function (string $type, int $id) use ($occupiedByType, $portAType, $portAId, $portBType, $portBId): bool { + if ($id <= 0) { + return false; + } + if ($type === $portAType && $id === $portAId) { + return true; + } + if ($type === $portBType && $id === $portBId) { + return true; + } + return empty($occupiedByType[$type][$id]); +}; + +// Auto-heal: ensure each outlet has at least one selectable port. +$outletsWithoutPorts = $sql->get( + "SELECT o.id + FROM network_outlets o + LEFT JOIN network_outlet_ports nop ON nop.outlet_id = o.id + GROUP BY o.id + HAVING COUNT(nop.id) = 0", + "", + [] +); +foreach ((array)$outletsWithoutPorts as $outletRow) { + $outletId = (int)($outletRow['id'] ?? 0); + if ($outletId <= 0) { + continue; + } + $sql->set( + "INSERT INTO network_outlet_ports (outlet_id, name) VALUES (?, 'Port 1')", + "i", + [$outletId] + ); +} + $devicePorts = $sql->get( "SELECT dp.id, dp.name, d.name AS owner_name FROM device_ports dp @@ -55,8 +117,12 @@ $devicePorts = $sql->get( [] ); foreach ($devicePorts as $row) { + $id = (int)$row['id']; + if (!$isEndpointAllowed('device', $id)) { + continue; + } $endpointOptions['device'][] = [ - 'id' => (int)$row['id'], + 'id' => $id, 'label' => $row['owner_name'] . ' / ' . $row['name'], ]; } @@ -78,27 +144,35 @@ $modulePorts = $sql->get( [] ); foreach ($modulePorts as $row) { + $id = (int)$row['id']; + if (!$isEndpointAllowed('module', $id)) { + continue; + } $deviceName = trim((string)($row['device_name'] ?? '')) ?: 'Unzugeordnet'; $endpointOptions['module'][] = [ - 'id' => (int)$row['id'], + 'id' => $id, 'label' => $deviceName . ' / ' . $row['module_name'] . ' / ' . $row['name'], ]; } $outletPorts = $sql->get( - "SELECT nop.id, nop.name, no.name AS outlet_name, r.name AS room_name, f.name AS floor_name + "SELECT nop.id, nop.name, o.name AS outlet_name, r.name AS room_name, f.name AS floor_name FROM network_outlet_ports nop - JOIN network_outlets no ON no.id = nop.outlet_id - LEFT JOIN rooms r ON r.id = no.room_id + JOIN network_outlets o ON o.id = nop.outlet_id + LEFT JOIN rooms r ON r.id = o.room_id LEFT JOIN floors f ON f.id = r.floor_id ORDER BY floor_name, room_name, outlet_name, nop.name", "", [] ); foreach ($outletPorts as $row) { + $id = (int)$row['id']; + if (!$isEndpointAllowed('outlet', $id)) { + continue; + } $parts = array_filter([(string)($row['floor_name'] ?? ''), (string)($row['room_name'] ?? ''), (string)$row['outlet_name'], (string)$row['name']]); $endpointOptions['outlet'][] = [ - 'id' => (int)$row['id'], + 'id' => $id, 'label' => implode(' / ', $parts), ]; } @@ -113,9 +187,13 @@ $patchpanelPorts = $sql->get( [] ); foreach ($patchpanelPorts as $row) { + $id = (int)$row['id']; + if (!$isEndpointAllowed('patchpanel', $id)) { + continue; + } $parts = array_filter([(string)($row['floor_name'] ?? ''), (string)$row['patchpanel_name'], (string)$row['name']]); $endpointOptions['patchpanel'][] = [ - 'id' => (int)$row['id'], + 'id' => $id, 'label' => implode(' / ', $parts), ]; } diff --git a/app/modules/connections/list.php b/app/modules/connections/list.php index 58b0d0e..192bc8f 100644 --- a/app/modules/connections/list.php +++ b/app/modules/connections/list.php @@ -42,11 +42,11 @@ $endpointUnionSql = " 'outlet' AS endpoint_type, nop.id AS endpoint_id, nop.name AS port_name, - CONCAT(no.name, ' / ', IFNULL(r.name, ''), ' / ', IFNULL(f.name, '')) AS owner_name, + CONCAT(o.name, ' / ', IFNULL(r.name, ''), ' / ', IFNULL(f.name, '')) AS owner_name, NULL AS owner_device_id FROM network_outlet_ports nop - JOIN network_outlets no ON no.id = nop.outlet_id - LEFT JOIN rooms r ON r.id = no.room_id + JOIN network_outlets o ON o.id = nop.outlet_id + LEFT JOIN rooms r ON r.id = o.room_id LEFT JOIN floors f ON f.id = r.floor_id UNION ALL SELECT @@ -328,6 +328,7 @@ if ($deviceId > 0) { Bearbeiten + Von/Nach tauschen get( + "SELECT id, port_a_type, port_a_id, port_b_type, port_b_id + FROM connections + WHERE id <> ?", + "i", + [$connId] +); + +$isEndpointUsed = static function (string $endpointType, int $endpointId) use ($otherConnections, $normalizePortType): bool { + if ($endpointId <= 0) { + return false; + } + foreach ((array)$otherConnections as $row) { + $typeA = $normalizePortType((string)($row['port_a_type'] ?? '')); + $idA = (int)($row['port_a_id'] ?? 0); + if ($typeA === $endpointType && $idA === $endpointId) { + return true; + } + + $typeB = $normalizePortType((string)($row['port_b_type'] ?? '')); + $idB = (int)($row['port_b_id'] ?? 0); + if ($typeB === $endpointType && $idB === $endpointId) { + return true; + } + } + return false; +}; + +if ($isEndpointUsed($portAType, $portAId)) { + $errors[] = "Port an Endpunkt A ist bereits in Verwendung"; +} +if ($isEndpointUsed($portBType, $portBId)) { + $errors[] = "Port an Endpunkt B ist bereits in Verwendung"; +} + if (!empty($errors)) { $_SESSION['error'] = implode(', ', $errors); $redirectUrl = $connId ? "?module=connections&action=edit&id=$connId" : "?module=connections&action=edit"; @@ -67,15 +102,36 @@ if ($connId > 0) { // UPDATE $sql->set( "UPDATE connections SET port_a_type = ?, port_a_id = ?, port_b_type = ?, port_b_id = ?, vlan_config = ?, comment = ? WHERE id = ?", - "siisisi", + "sisissi", [$portAType, $portAId, $portBType, $portBId, $vlanJson, $comment, $connId] ); } else { + $connectionTypeId = (int)($sql->single( + "SELECT id FROM connection_types ORDER BY id LIMIT 1", + "", + [] + )['id'] ?? 0); + + if ($connectionTypeId <= 0) { + $connectionTypeId = (int)$sql->set( + "INSERT INTO connection_types (name, medium, duplex, line_style, comment) VALUES (?, ?, ?, ?, ?)", + "sssss", + ['Default', 'copper', 'custom', 'solid', 'Auto-created by connections/save'], + true + ); + } + + if ($connectionTypeId <= 0) { + $_SESSION['error'] = "Kein Verbindungstyp verfuegbar"; + header("Location: ?module=connections&action=edit"); + exit; + } + // INSERT $sql->set( - "INSERT INTO connections (port_a_type, port_a_id, port_b_type, port_b_id, vlan_config, comment) VALUES (?, ?, ?, ?, ?, ?)", - "siisis", - [$portAType, $portAId, $portBType, $portBId, $vlanJson, $comment] + "INSERT INTO connections (connection_type_id, port_a_type, port_a_id, port_b_type, port_b_id, vlan_config, comment) VALUES (?, ?, ?, ?, ?, ?, ?)", + "isisiss", + [$connectionTypeId, $portAType, $portAId, $portBType, $portBId, $vlanJson, $comment] ); } diff --git a/app/modules/connections/swap.php b/app/modules/connections/swap.php new file mode 100644 index 0000000..9480077 --- /dev/null +++ b/app/modules/connections/swap.php @@ -0,0 +1,46 @@ +single( + "SELECT id, port_a_type, port_a_id, port_b_type, port_b_id + FROM connections + WHERE id = ?", + "i", + [$connectionId] +); + +if (!$connection) { + $_SESSION['error'] = 'Verbindung nicht gefunden'; + header('Location: ?module=connections&action=list'); + exit; +} + +$sql->set( + "UPDATE connections + SET port_a_type = ?, port_a_id = ?, port_b_type = ?, port_b_id = ? + WHERE id = ?", + "sisii", + [ + (string)$connection['port_b_type'], + (int)$connection['port_b_id'], + (string)$connection['port_a_type'], + (int)$connection['port_a_id'], + $connectionId + ] +); + +$_SESSION['success'] = 'Endpunkte wurden vertauscht'; +header('Location: ?module=connections&action=list'); +exit; 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:

- +
- - - - -
- - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
#NameTypMediumModusVLANAktionen
- - 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/floor_infrastructure/save.php b/app/modules/floor_infrastructure/save.php index bd44496..c416bee 100644 --- a/app/modules/floor_infrastructure/save.php +++ b/app/modules/floor_infrastructure/save.php @@ -21,6 +21,8 @@ if ($type === 'patchpanel') { $portCount = (int)($_POST['port_count'] ?? 0); $comment = trim($_POST['comment'] ?? ''); + $panelId = $id; + if ($id > 0) { $sql->set( "UPDATE floor_patchpanels SET name = ?, floor_id = ?, pos_x = ?, pos_y = ?, width = ?, height = ?, port_count = ?, comment = ? WHERE id = ?", @@ -28,18 +30,38 @@ if ($type === 'patchpanel') { [$name, $floorId, $posX, $posY, $width, $height, $portCount, $comment, $id] ); } else { - $sql->set( + $panelId = (int)$sql->set( "INSERT INTO floor_patchpanels (name, floor_id, pos_x, pos_y, width, height, port_count, comment) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", "siiiiiss", - [$name, $floorId, $posX, $posY, $width, $height, $portCount, $comment] + [$name, $floorId, $posX, $posY, $width, $height, $portCount, $comment], + true ); } + + if ($panelId > 0 && $portCount > 0) { + $existingCount = (int)($sql->single( + "SELECT COUNT(*) AS cnt FROM floor_patchpanel_ports WHERE patchpanel_id = ?", + "i", + [$panelId] + )['cnt'] ?? 0); + + if ($existingCount < $portCount) { + for ($i = $existingCount + 1; $i <= $portCount; $i++) { + $sql->set( + "INSERT INTO floor_patchpanel_ports (patchpanel_id, name) VALUES (?, ?)", + "is", + [$panelId, 'Port ' . $i] + ); + } + } + } } elseif ($type === 'outlet') { $name = trim($_POST['name'] ?? ''); $roomId = (int)($_POST['room_id'] ?? 0); $x = (int)($_POST['x'] ?? 0); $y = (int)($_POST['y'] ?? 0); $comment = trim($_POST['comment'] ?? ''); + $outletId = $id; if ($id > 0) { $sql->set( @@ -48,12 +70,29 @@ if ($type === 'patchpanel') { [$name, $roomId, $x, $y, $comment, $id] ); } else { - $sql->set( + $outletId = (int)$sql->set( "INSERT INTO network_outlets (name, room_id, x, y, comment) VALUES (?, ?, ?, ?, ?)", "siiis", - [$name, $roomId, $x, $y, $comment] + [$name, $roomId, $x, $y, $comment], + true ); } + + if ($outletId > 0) { + $existingPortCount = (int)($sql->single( + "SELECT COUNT(*) AS cnt FROM network_outlet_ports WHERE outlet_id = ?", + "i", + [$outletId] + )['cnt'] ?? 0); + + if ($existingPortCount === 0) { + $sql->set( + "INSERT INTO network_outlet_ports (outlet_id, name) VALUES (?, 'Port 1')", + "i", + [$outletId] + ); + } + } } header('Location: ?module=floor_infrastructure&action=list'); 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", '', []); ?>

- - + -
Allgemein
- +
- +
-
- Standort & Größe + Standort und Groesse
- - - Standard: 42 HE (ca. 2 Meter) + + + Standard: 42 HE
-
Abbrechen - Löschen + Loeschen
-
@@ -197,60 +167,25 @@ $floors = $sql->get("SELECT id, name FROM floors ORDER BY name", "", []); - - - -
- Rack-Layout - -
- - - -
- -

- Geräte per Drag & Drop im Rack positionieren. -

-
- - - -
- - - -
- - - - - - 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

-
-
+ - + Reset - + Neues Rack + + Neues Rack
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameStockwerkHöhe (HE)GeräteBeschreibungAktionen
- - - - - HE - - - - - - Bearbeiten - Löschen -
- + + + + + + + + + + + + + + + + + + + + + + + +
NameStockwerkHoehe (HE)GeraeteBeschreibungAktionen
HE + Bearbeiten + Loeschen +
-
-

Keine Racks gefunden.

-

- - Erstes Rack anlegen - -

-
+
+

Keine Racks gefunden.

+

Erstes Rack anlegen

+
-
- 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 @@ - + + + + diff --git a/app/templates/header.php b/app/templates/header.php index f527764..12e43fd 100644 --- a/app/templates/header.php +++ b/app/templates/header.php @@ -10,7 +10,10 @@ + + Netzwerk-Dokumentation + @@ -21,7 +24,6 @@ -
diff --git a/app/templates/layout.php b/app/templates/layout.php index 888bcc7..10ba0ee 100644 --- a/app/templates/layout.php +++ b/app/templates/layout.php @@ -2,25 +2,18 @@ /** * layout.php * - * Grundlayout: Header + Content + Footer - * Kann als Basis-Template dienen, falls Module HTML ausgeben - * - * Beispiel-Aufruf in Modul: - * include __DIR__ . '/../templates/layout.php'; - * - * TODO: In Zukunft: zentrales Template-System (z.B. mit $content) + * Basislayout fuer Header + Content + Footer. */ ?>
- Inhalt fehlt

"; + echo '

Kein Inhalt uebergeben.

'; } ?>
diff --git a/init.sql b/init.sql index a78af85..4b0252b 100644 --- a/init.sql +++ b/init.sql @@ -44,9 +44,9 @@ INSERT INTO `buildings` (`id`, `location_id`, `name`, `comment`) VALUES CREATE TABLE `connections` ( `id` int(11) NOT NULL, `connection_type_id` int(11) NOT NULL, - `port_a_type` enum('device','module','outlet') NOT NULL, + `port_a_type` enum('device','module','outlet','patchpanel') NOT NULL, `port_a_id` int(11) NOT NULL, - `port_b_type` enum('device','module','outlet') NOT NULL, + `port_b_type` enum('device','module','outlet','patchpanel') NOT NULL, `port_b_id` int(11) NOT NULL, `vlan_config` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL CHECK (json_valid(`vlan_config`)), `mode` varchar(50) DEFAULT NULL, @@ -70,6 +70,13 @@ CREATE TABLE `connection_types` ( `comment` text DEFAULT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_uca1400_ai_ci; +-- +-- Daten fuer Tabelle `connection_types` +-- + +INSERT INTO `connection_types` (`id`, `name`, `medium`, `duplex`, `max_speed`, `color`, `line_style`, `comment`) VALUES +(1, 'Default', 'copper', 'custom', NULL, NULL, 'solid', 'Auto-created default type'); + -- -------------------------------------------------------- --