verbings administration

This commit is contained in:
2026-02-16 14:43:15 +01:00
parent 510a248edb
commit 4a23713d31
7 changed files with 250 additions and 23 deletions

View File

@@ -8,7 +8,7 @@ $module = $_GET['module'] ?? 'dashboard';
$action = $_GET['action'] ?? 'list';
$validModules = ['dashboard', 'locations', 'buildings', 'rooms', 'device_types', 'devices', 'racks', 'floors', 'floor_infrastructure', 'connections', 'port_types'];
$validActions = ['list', 'edit', 'save', 'ports', 'delete'];
$validActions = ['list', 'edit', 'save', 'ports', 'delete', 'swap'];
if (!in_array($module, $validModules, true)) {
renderClientError(400, 'Ungueltiges Modul');
@@ -20,7 +20,7 @@ if (!in_array($action, $validActions, true)) {
exit;
}
if (!in_array($action, ['save', 'delete'], true)) {
if (!in_array($action, ['save', 'delete', 'swap'], true)) {
require_once __DIR__ . '/templates/header.php';
}
@@ -32,6 +32,6 @@ if (file_exists($modulePath)) {
renderClientError(404, 'Die angeforderte Seite existiert nicht.');
}
if (!in_array($action, ['save', 'delete'], true)) {
if (!in_array($action, ['save', 'delete', 'swap'], true)) {
require_once __DIR__ . '/templates/footer.php';
}

View File

@@ -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),
];
}

View File

@@ -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) {
<td class="actions">
<a href="?module=connections&action=edit&id=<?php echo $conn['id']; ?>" class="button button-small">Bearbeiten</a>
<a href="?module=connections&action=swap&id=<?php echo $conn['id']; ?>" class="button button-small" onclick="return confirm('Von/Nach fuer diese Verbindung vertauschen?');">Von/Nach tauschen</a>
<a href="#" class="button button-small button-danger"
data-confirm-delete="true"
data-confirm-message="Diese Verbindung wirklich löschen?"

View File

@@ -51,6 +51,41 @@ if ($portAId <= 0 || $portBId <= 0) {
$errors[] = "Beide Ports sind erforderlich";
}
$otherConnections = $sql->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]
);
}

View File

@@ -0,0 +1,46 @@
<?php
/**
* app/modules/connections/swap.php
*
* Vertauscht Endpunkt A und Endpunkt B einer Verbindung.
*/
$connectionId = (int)($_GET['id'] ?? 0);
if ($connectionId <= 0) {
$_SESSION['error'] = 'Ungueltige Verbindungs-ID';
header('Location: ?module=connections&action=list');
exit;
}
$connection = $sql->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;

View File

@@ -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');

View File

@@ -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');
-- --------------------------------------------------------
--