feat: Implement initial application structure with network view and SVG editor

- Added network-view.js for visualizing network topology with devices and connections.
- Introduced svg-editor.js for managing ports on device types with drag-and-drop functionality.
- Created bootstrap.php for application initialization, including configuration and database connection.
- Established config.php for centralized configuration settings.
- Developed index.php as the main entry point with module-based routing.
- Integrated _sql.php for database abstraction.
- Added auth.php for single-user authentication handling.
- Included helpers.php for utility functions.
- Created modules for managing connections, device types, devices, and floors.
- Implemented database schema in init.sql for locations, buildings, floors, rooms, network outlets, devices, and connections.
- Added Docker support with docker-compose.yml for web and database services.
- Documented database structure and UI/UX concepts in respective markdown files.
This commit is contained in:
Troy Grunt
2026-02-05 23:41:54 +01:00
parent 13995695db
commit 5066262fca
39 changed files with 1829 additions and 0 deletions

View File

@@ -0,0 +1,289 @@
// 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
* ========================= */
// TODO: Standort / Rack / View-Kontext vom Backend setzen
const CONTEXT_ID = null;
// TODO: API-Endpunkte definieren
const API_LOAD_NETWORK = '/api/network_view.php?action=load';
const API_SAVE_POSITIONS = '/api/network_view.php?action=save_positions';
/* =========================
* State
* ========================= */
let svgElement = null;
let devices = []; // Geräte inkl. Position
let connections = []; // Verbindungen zwischen Ports
let selectedDeviceId = null;
let isDragging = false;
let dragOffset = { x: 0, y: 0 };
/* =========================
* Initialisierung
* ========================= */
document.addEventListener('DOMContentLoaded', () => {
svgElement = document.querySelector('#network-svg');
if (!svgElement) {
console.warn('Network View: #network-svg nicht gefunden');
return;
}
bindSvgEvents();
loadNetwork();
});
/* =========================
* Events
* ========================= */
function bindSvgEvents() {
svgElement.addEventListener('mousemove', onMouseMove);
svgElement.addEventListener('mouseup', onMouseUp);
svgElement.addEventListener('click', onSvgClick);
}
/* =========================
* Laden
* ========================= */
function loadNetwork() {
if (!CONTEXT_ID) {
console.warn('CONTEXT_ID nicht gesetzt');
return;
}
fetch(`${API_LOAD_NETWORK}&context_id=${CONTEXT_ID}`)
.then(res => res.json())
.then(data => {
// TODO: Datenstruktur validieren
devices = data.devices || [];
connections = data.connections || [];
renderAll();
})
.catch(err => {
console.error('Fehler beim Laden der Netzwerkansicht', err);
});
}
/* =========================
* Rendering
* ========================= */
function renderAll() {
clearSvg();
renderConnections();
renderDevices();
}
function clearSvg() {
while (svgElement.firstChild) {
svgElement.removeChild(svgElement.firstChild);
}
}
/* ---------- Geräte ---------- */
function renderDevices() {
devices.forEach(device => renderDevice(device));
}
function renderDevice(device) {
const group = document.createElementNS('http://www.w3.org/2000/svg', 'g');
group.classList.add('device-node');
group.dataset.id = device.id;
group.setAttribute(
'transform',
`translate(${device.x || 0}, ${device.y || 0})`
);
// TODO: Gerätetyp (SVG oder JPG) korrekt laden
const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
rect.setAttribute('width', 120);
rect.setAttribute('height', 60);
rect.setAttribute('rx', 6);
rect.addEventListener('mousedown', (e) => {
startDrag(e, device.id);
e.stopPropagation();
});
// Label
const text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
text.setAttribute('x', 60);
text.setAttribute('y', 35);
text.setAttribute('text-anchor', 'middle');
text.textContent = device.name || 'Device';
group.appendChild(rect);
group.appendChild(text);
// TODO: Ports als kleine Kreise anlegen (Position aus Portdefinition)
// TODO: Ports klickbar machen (für Verbindungs-Erstellung)
svgElement.appendChild(group);
}
/* ---------- Verbindungen ---------- */
function renderConnections() {
connections.forEach(conn => renderConnection(conn));
}
function renderConnection(connection) {
// TODO: Quell- & Ziel-Port-Koordinaten berechnen
// TODO: unterschiedliche Verbindungstypen (Farbe, Strichart, Dicke)
const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
line.setAttribute('x1', 0);
line.setAttribute('y1', 0);
line.setAttribute('x2', 100);
line.setAttribute('y2', 100);
line.classList.add('connection-line');
svgElement.appendChild(line);
}
/* =========================
* Interaktion
* ========================= */
function onSvgClick(event) {
// Klick ins Leere -> Auswahl aufheben
if (event.target === svgElement) {
selectedDeviceId = null;
updateSelection();
}
}
function startDrag(event, deviceId) {
const device = getDeviceById(deviceId);
if (!device) return;
isDragging = true;
selectedDeviceId = deviceId;
const point = getSvgCoordinates(event);
dragOffset.x = (device.x || 0) - point.x;
dragOffset.y = (device.y || 0) - point.y;
updateSelection();
}
function onMouseMove(event) {
if (!isDragging || !selectedDeviceId) return;
const device = getDeviceById(selectedDeviceId);
if (!device) return;
const point = getSvgCoordinates(event);
device.x = point.x + dragOffset.x;
device.y = point.y + dragOffset.y;
renderAll();
}
function onMouseUp() {
if (!isDragging) return;
isDragging = false;
// TODO: Positionen optional automatisch speichern
}
/* =========================
* Auswahl
* ========================= */
function updateSelection() {
svgElement.querySelectorAll('.device-node').forEach(el => {
el.classList.toggle(
'selected',
el.dataset.id === String(selectedDeviceId)
);
});
// TODO: Sidebar mit Gerätedetails füllen
}
/* =========================
* Speichern
* ========================= */
function savePositions() {
fetch(API_SAVE_POSITIONS, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
context_id: CONTEXT_ID,
devices: devices.map(d => ({
id: d.id,
x: d.x,
y: d.y
}))
})
})
.then(res => res.json())
.then(data => {
// TODO: Erfolg / Fehler anzeigen
console.log('Positionen gespeichert', data);
})
.catch(err => {
console.error('Fehler beim Speichern', err);
});
}
/* =========================
* Hilfsfunktionen
* ========================= */
function getSvgCoordinates(event) {
const pt = svgElement.createSVGPoint();
pt.x = event.clientX;
pt.y = event.clientY;
const transformed = pt.matrixTransform(svgElement.getScreenCTM().inverse());
return { x: transformed.x, y: transformed.y };
}
function getDeviceById(id) {
return devices.find(d => d.id === id);
}
/* =========================
* Keyboard Shortcuts
* ========================= */
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
selectedDeviceId = null;
updateSelection();
}
// TODO: Delete -> Gerät entfernen?
});