Compare commits
2 Commits
4a23713d31
...
c8fb5b140c
| Author | SHA1 | Date | |
|---|---|---|---|
| c8fb5b140c | |||
| 463ab97c4b |
@@ -1,6 +1,6 @@
|
||||
# 📋 NÄCHSTE ARBEITSPAKETE
|
||||
# 📋 NÄCHSTE ARBEITSPAKETE
|
||||
|
||||
## 🎯 Für die nächsten Sessions
|
||||
## 🎯 Für die nächsten Sessions
|
||||
|
||||
### Package 1: Fehlerbehandlung & Sessions (1-2h)
|
||||
- [ ] Session-Handling in `bootstrap.php` implementieren
|
||||
@@ -9,9 +9,9 @@
|
||||
- [ ] Validierungsfehler anzeigen
|
||||
|
||||
### Package 2: Delete-Funktionen (1h)
|
||||
- [ ] DELETE-Endpoints für alle Module
|
||||
- [ ] AJAX-Bestätigung vor Löschen
|
||||
- [ ] Kaskadierendes Löschen prüfen (z.B. Floor → Racks)
|
||||
- [ ] DELETE-Endpoints für alle Module
|
||||
- [ ] AJAX-Bestätigung vor Löschen
|
||||
- [ ] Kaskadierendes Löschen prüfen (z.B. Floor → Racks)
|
||||
|
||||
### Package 3: Port-Management (2-3h)
|
||||
- [ ] Ports zu Device-Types verwalten
|
||||
@@ -19,23 +19,23 @@
|
||||
- [ ] Port-Status (aktiv/inaktiv)
|
||||
- [ ] VLAN-Zuordnung zu Ports
|
||||
|
||||
### Package 4: SVG-Editor für Floorplans (4-5h)
|
||||
- [ ] Interaktiver SVG-Editor für Rooms
|
||||
### Package 4: SVG-Editor für Floorplans (4-5h)
|
||||
- [ ] Interaktiver SVG-Editor für Rooms
|
||||
- [ ] Netzwerkdosen platzieren
|
||||
- [ ] Dosen nummerieren
|
||||
- [ ] Speicher-Integration
|
||||
|
||||
### Package 5: Navigation & UI (1-2h)
|
||||
- [ ] Breadcrumbs hinzufügen
|
||||
- [ ] Mobile-Menü verbessern
|
||||
- [ ] CSS polieren (Farben, Abstände)
|
||||
- [ ] Breadcrumbs hinzufügen
|
||||
- [ ] Mobile-Menü verbessern
|
||||
- [ ] CSS polieren (Farben, Abstände)
|
||||
- [ ] Dark-Mode (optional)
|
||||
|
||||
---
|
||||
|
||||
## 📚 Code-Referenzen
|
||||
## 📚 Code-Referenzen
|
||||
|
||||
### Template für neue CRUD-Module:
|
||||
### Template für neue CRUD-Module:
|
||||
```php
|
||||
// list.php: Filter + Tabelle
|
||||
// edit.php: Formular
|
||||
@@ -71,7 +71,7 @@ $whereSql = $where ? "WHERE " . implode(" AND ", $where) : "";
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Bekannte TODOs im Code
|
||||
## 🔧 Bekannte TODOs im Code
|
||||
|
||||
Alle noch offenen Punkte sind mit `// TODO:` gekennzeichnet:
|
||||
|
||||
@@ -83,48 +83,48 @@ grep -r "TODO:" app/modules/ --include="*.php"
|
||||
Wichtigste TODOs:
|
||||
- `index.php:19` - Session starten
|
||||
- `*/save.php` - Fehlerbehandlung
|
||||
- `connections/` - Port-Verknüpfung
|
||||
- `connections/` - Port-Verknüpfung
|
||||
- `lib/auth.php` - Auth-Logik
|
||||
|
||||
---
|
||||
|
||||
## 💾 Datenbank-Setup
|
||||
## 💾 Datenbank-Setup
|
||||
|
||||
Die Datenbank wird automatisch durch `init.sql` initialisiert.
|
||||
|
||||
Wichtige Tabellen:
|
||||
- `locations` - Standorte
|
||||
- `buildings` - Gebäude
|
||||
- `buildings` - Gebäude
|
||||
- `floors` - Stockwerke
|
||||
- `rooms` - Räume
|
||||
- `rooms` - Räume
|
||||
- `network_outlets` - Netzwerkdosen
|
||||
- `device_types` - Gerätetypen
|
||||
- `device_types` - Gerätetypen
|
||||
- `device_type_ports` - Port-Templates
|
||||
- `devices` - konkrete Geräte
|
||||
- `device_ports` - Gerätports
|
||||
- `devices` - konkrete Geräte
|
||||
- `device_ports` - Gerätports
|
||||
- `racks` - Racks
|
||||
- `connections` - Verbindungen zwischen Ports
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Testing-Checklist
|
||||
## 🧪 Testing-Checklist
|
||||
|
||||
Bei jeder Änderung checken:
|
||||
Bei jeder Änderung checken:
|
||||
- [ ] Formular sendet Daten korrekt
|
||||
- [ ] Daten werden in DB gespeichert
|
||||
- [ ] Liste zeigt neue Daten
|
||||
- [ ] Edit lädt existierende Daten vor
|
||||
- [ ] Edit lädt existierende Daten vor
|
||||
- [ ] Filter funktioniert
|
||||
- [ ] Validierungsfehler werden angezeigt
|
||||
|
||||
---
|
||||
|
||||
## 🎨 Design-Richtlinien
|
||||
## 🎨 Design-Richtlinien
|
||||
|
||||
### Farben:
|
||||
- Primary (Buttons): `#007bff` (Blau)
|
||||
- Success (Speichern): `#28a745` (Grün)
|
||||
- Danger (Löschen): `#dc3545` (Rot)
|
||||
- Success (Speichern): `#28a745` (Grün)
|
||||
- Danger (Löschen): `#dc3545` (Rot)
|
||||
- Background: `#f9f9f9` (Hell)
|
||||
- Border: `#ddd` (Hell-Grau)
|
||||
|
||||
@@ -135,8 +135,28 @@ Bei jeder Änderung checken:
|
||||
|
||||
### Schriftarten:
|
||||
- Erben von HTML (derzeit: System)
|
||||
- Monospace für Code/IDs: `font-family: monospace`
|
||||
- Monospace für Code/IDs: `font-family: monospace`
|
||||
|
||||
---
|
||||
|
||||
**Happy Coding! 🚀**
|
||||
**Happy Coding! 🚀**
|
||||
|
||||
## Aktuell offene TODOs (Stand: 18. Februar 2026)
|
||||
|
||||
- [ ] #15 Neue Verbindung: Es kann keine Netzwerkdose ausgewahlt werden.
|
||||
- [ ] #14 Hilfslinien der Stockwerkskarten nur im Edit-Mode anzeigen; im Anzeige-Mode ausblenden.
|
||||
- [ ] #11 Encoding- und Umlautfehler beheben (inkl. ae/oe/ue-Themen).
|
||||
- [ ] #10 Dashboard-Grafik erzeugen:
|
||||
- Oberste Ebene: Locations, ggf. mit Unterordnung.
|
||||
- Darunter: Gebaudekomplexe mit Rack-Verbindungen.
|
||||
- Darunter: Stockwerke.
|
||||
- Darunter: Etagenweise Verbindungen.
|
||||
- [ ] #8 Gerate löschen fehlt: Es erfolgt Weiterleitung, aber keine echte Fehlermeldung.
|
||||
- [ ] #7 Letzten Punkt im Floor-Editor löschen:
|
||||
- URL: `http://localhost/?module=floors&action=edit&id=1`
|
||||
- [ ] #5 Dashboard als zoombare und verschiebbare SVG-Flache:
|
||||
- Gerate anordnen.
|
||||
- Gerate, Ports und Verbindungen anklickbar.
|
||||
- Sprechblase mit Infos und Buttons zu Bereichen (editieren, entfernen, ...).
|
||||
- [ ] #4 `device_types/edit`: Option "Ports automatisch erstellen" nur beim Erstellen anzeigen, nicht beim Editieren.
|
||||
|
||||
|
||||
276
TODO.md
276
TODO.md
@@ -1,258 +1,36 @@
|
||||
# TODO
|
||||
# TODO
|
||||
|
||||
Zentrale Sammlung aller TODO-Markierungen im Repository (Stand: 13. Februar 2026).
|
||||
Bereinigte und aktuelle TODO-Liste (Stand: 18. Februar 2026).
|
||||
Quelle: vorhandene `TODO`-Marker im Repository plus offene Architekturpunkte.
|
||||
|
||||
Hinweis: Die Eintraege sind direkt aus den Quelldateien aggregiert.
|
||||
## Erledigt (bereits umgesetzt)
|
||||
|
||||
## Arbeitsnotizen (16. Februar 2026)
|
||||
- [x] API-Basis umgesetzt (`app/api/connections.php`, `app/api/device_type_ports.php`, `app/api/upload.php`).
|
||||
- [x] Bootstrap/Auth/Config/Routing-Grundlagen 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 in `app/modules/device_types/ports.php` ersetzt.
|
||||
- [x] Veraltete Sammel-TODO-Liste (nicht mehr im Code vorhanden) entfernt.
|
||||
|
||||
- [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.
|
||||
## Offen (direkt im Code markiert)
|
||||
|
||||
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.
|
||||
- [x] `app/modules/dashboard/list.php`: zoombare/verschiebbare SVG-Wand mit klickbaren Punkten und Overlay-Drilldown umgesetzt.
|
||||
- [x] `app/modules/connections/list.php`: Detailbereich fuer ausgewaehlte Verbindung inkl. Bearbeiten/Loeschen im UI umgesetzt.
|
||||
- [x] `app/lib/helpers.php`: konkrete allgemeine Helper ergaenzt (`formatDateTime`, `formatBytes`, `generateUuidV4`, `normalizeSvgCoordinate`).
|
||||
|
||||
## app\api\connections.php
|
||||
## Offen (Bugs / Doku / Statusdateien)
|
||||
|
||||
- [ ] L15: // TODO: Single-User-Auth prüfen
|
||||
- [ ] L61: // TODO: Kontext definieren (Standort, Rack, Floor, gesamtes Netz)
|
||||
- [ ] L117: // TODO: Validierung
|
||||
- [ ] L182: // TODO: Prüfen, ob Verbindung existiert
|
||||
- [ ] `BUGS.md:3`: Design vereinheitlichen.
|
||||
- [ ] `IMPLEMENTATION_STATUS.md:80`: Delete-Funktionen-Status aktualisieren.
|
||||
- [ ] `IMPLEMENTATION_STATUS.md:109`: Auth-Status aktualisieren.
|
||||
- [ ] `README.md:241`: Patchpanel-Infrastruktur umsetzen und danach Doku abhaken.
|
||||
- [ ] `README.md:253`: SVG-Editor fuer Floor-Infrastruktur (Drag-and-Drop + direkte Modul-Interaktion).
|
||||
- [ ] `doc/DATABASE.md:127`: TODO-Abschnitt fuer Patchpanel/Floorplan finalisieren.
|
||||
- [ ] `init.sql:379`: Kommentar zur Port-Konfiguration in verbindliche Implementierungsregeln ueberfuehren.
|
||||
|
||||
## app\api\device_type_ports.php
|
||||
## Topologie-Backlog (ausstehend)
|
||||
|
||||
- [ ] L15: // TODO: Single-User-Auth prüfen
|
||||
- [ ] L96: // TODO: Transaktion starten (falls SQL-Klasse das unterstützt)
|
||||
- [ ] L100: // TODO: Validierung:
|
||||
- [ ] L163: // TODO: Prüfen, ob Port existiert und nicht verwendet wird
|
||||
|
||||
## app\api\upload.php
|
||||
|
||||
- [ ] L18: // TODO: Single-User-Auth prüfen
|
||||
- [ ] L25: // TODO: Upload-Basisverzeichnis aus config.php
|
||||
- [ ] L35: // TODO: Max. Dateigröße festlegen (z.B. 5MB)
|
||||
- [ ] L77: // TODO: Kategorie definieren (device_types, floors, racks, etc.)
|
||||
- [ ] L95: // TODO: Eindeutigen Namen besser definieren (UUID?)
|
||||
- [ ] L114: // TODO: Optional in Tabelle `uploads` speichern
|
||||
|
||||
## app\assets\js\app.js
|
||||
|
||||
- [ ] L15: deviceTypes: [], // TODO: alle Gerätetypen laden
|
||||
- [ ] L16: devices: [], // TODO: alle Geräte laden
|
||||
- [ ] L17: racks: [], // TODO: alle Racks laden
|
||||
- [ ] L18: floors: [], // TODO: alle Floors laden
|
||||
- [ ] L19: connections: [], // TODO: alle Verbindungen laden
|
||||
- [ ] L33: // TODO: import / init svg-editor.js
|
||||
- [ ] L39: // TODO: import / init network-view.js
|
||||
- [ ] L45: // TODO: init drag & drop logic
|
||||
- [ ] L59: // TODO: Save-Button Device-Type
|
||||
- [ ] L64: // TODO: Save Device-Type via AJAX
|
||||
- [ ] L68: // TODO: Save-Button Device
|
||||
- [ ] L73: // TODO: Save Device via AJAX
|
||||
- [ ] L77: // TODO: Save-Button Floor
|
||||
- [ ] L82: // TODO: Save Floor via AJAX
|
||||
- [ ] L86: // TODO: Save-Button Rack
|
||||
- [ ] L91: // TODO: Save Rack via AJAX
|
||||
- [ ] L95: // TODO: Weitere Event-Handler (Import, Export, Filter)
|
||||
- [ ] L132: // TODO: weitere Utility-Funktionen (DOM-Helper, SVG-Helper, etc.)
|
||||
|
||||
## app\assets\js\dashboard.js
|
||||
|
||||
- [ ] L75: // TODO: Dashboard-Container ermitteln
|
||||
- [ ] L78: // TODO: Module rendern
|
||||
- [ ] L81: // TODO: Optional: Status-Daten laden (Counts, Warnings)
|
||||
- [ ] L117: // TODO: loadStats() → Anzahl Geräte, offene Ports, unverbundene Dosen
|
||||
- [ ] L118: // TODO: showWarnings() → unverbundene Ports, VLAN-Konflikte
|
||||
- [ ] L119: // TODO: RecentChanges() → letzte Änderungen
|
||||
|
||||
## app\assets\js\network-view.js
|
||||
|
||||
- [ ] L20: // TODO: Standort / Rack / View-Kontext vom Backend setzen
|
||||
- [ ] L23: // TODO: API-Endpunkte definieren
|
||||
- [ ] L79: // TODO: Datenstruktur validieren
|
||||
- [ ] L123: // TODO: Gerätetyp (SVG oder JPG) korrekt laden
|
||||
- [ ] L144: // TODO: Ports als kleine Kreise anlegen (Position aus Portdefinition)
|
||||
- [ ] L145: // TODO: Ports klickbar machen (für Verbindungs-Erstellung)
|
||||
- [ ] L157: // TODO: Quell- & Ziel-Port-Koordinaten berechnen
|
||||
- [ ] L158: // TODO: unterschiedliche Verbindungstypen (Farbe, Strichart, Dicke)
|
||||
- [ ] L217: // TODO: Positionen optional automatisch speichern
|
||||
- [ ] L232: // TODO: Sidebar mit Gerätedetails füllen
|
||||
- [ ] L254: // TODO: Erfolg / Fehler anzeigen
|
||||
- [ ] L289: // TODO: Delete -> Gerät entfernen?
|
||||
|
||||
## app\assets\js\svg-editor.js
|
||||
|
||||
- [ ] L20: // TODO: vom Backend setzen (z. B. via data-Attribut)
|
||||
- [ ] L23: // TODO: API-Endpunkte festlegen
|
||||
- [ ] L74: // TODO: Modifier-Key prüfen (z. B. nur mit SHIFT neuen Port erstellen?)
|
||||
- [ ] L86: port_type_id: null, // TODO: Default-Porttyp?
|
||||
- [ ] L134: // TODO: Sidebar-Felder mit Portdaten füllen
|
||||
- [ ] L178: // TODO: Sicherheitsabfrage (confirm)
|
||||
- [ ] L184: // TODO: Sidebar zurücksetzen
|
||||
- [ ] L200: // TODO: Datenformat validieren
|
||||
- [ ] L222: // TODO: Erfolg / Fehler anzeigen
|
||||
|
||||
## app\bootstrap.php
|
||||
|
||||
- [ ] L16: // TODO: Config-Datei mit DB-Zugang, Pfaden, globalen Settings füllen
|
||||
- [ ] L22: // TODO: Single-User Auth prüfen
|
||||
- [ ] L30: // TODO: Host, User, Passwort, DB aus config.php nutzen
|
||||
|
||||
## app\index.php
|
||||
|
||||
- [ ] L19: // TODO: Session starten / Single-User-Auth prüfen
|
||||
- [ ] L37: // TODO: Fehlerseite anzeigen, nutze renderClientError(...)
|
||||
- [ ] L42: // TODO: Fehlerseite anzeigen, nutze renderClientError(...)
|
||||
- [ ] L61: // TODO: Fehlerseite oder 404, nutze renderClientError(...)
|
||||
|
||||
## app\lib\auth.php
|
||||
|
||||
- [ ] L24: // TODO: Session-Variable definieren, z.B. $_SESSION['auth'] === true
|
||||
- [ ] L40: // TODO: Passwort aus config.php vergleichen
|
||||
- [ ] L41: // TODO: Passwort-Hash verwenden (password_hash / password_verify)
|
||||
- [ ] L62: // TODO: Session-Variablen löschen
|
||||
- [ ] L65: // TODO: Optional komplette Session zerstören
|
||||
- [ ] L79: // TODO: Redirect auf Login-Seite
|
||||
|
||||
## app\lib\helpers.php
|
||||
|
||||
- [ ] L300: // TODO: Weitere Helfer nach Bedarf
|
||||
|
||||
## app\modules\buildings\edit.php
|
||||
|
||||
- [ ] L176: // TODO: AJAX-Delete implementieren
|
||||
|
||||
## app\modules\buildings\list.php
|
||||
|
||||
- [ ] L245: // TODO: AJAX-Delete implementieren
|
||||
|
||||
## app\modules\connections\list.php
|
||||
|
||||
- [ ] L198: <!-- TODO: Details zum ausgewählten Gerät anzeigen -->
|
||||
- [ ] L207: <!-- TODO: Verbindung bearbeiten / löschen -->
|
||||
|
||||
## app\modules\device_types\edit.php
|
||||
|
||||
- [ ] L18: //TODO port hinzufügen geht nicht
|
||||
- [ ] L378: // TODO: AJAX-Delete implementieren
|
||||
|
||||
## app\modules\device_types\list.php
|
||||
|
||||
- [ ] L252: // TODO: AJAX-Delete implementieren
|
||||
|
||||
## app\modules\device_types\ports.php
|
||||
|
||||
- [ ] L12: // TODO: bootstrap laden
|
||||
- [ ] L15: // TODO: Auth erzwingen
|
||||
- [ ] L22: // TODO: device_type_id aus GET lesen
|
||||
- [ ] L25: // TODO: Gerätetyp laden
|
||||
- [ ] L28: // TODO: Ports dieses Gerätetyps laden
|
||||
- [ ] L43: <!-- TODO: Gerätetyp-Name -->
|
||||
- [ ] L59: <!-- TODO: Port-Typen verwalten -->
|
||||
- [ ] L60: <!-- TODO: Import / Export -->
|
||||
- [ ] L109: <!-- TODO: Port-Nummer -->
|
||||
- [ ] L113: <!-- TODO: Port-Name -->
|
||||
- [ ] L117: <!-- TODO: Port-Typ (RJ45, SFP, ...) -->
|
||||
- [ ] L120: <!-- TODO: Medium (Kupfer, LWL, BNC, Custom) -->
|
||||
- [ ] L123: <!-- TODO: Modus (Access, Trunk, Custom) -->
|
||||
- [ ] L126: <!-- TODO: VLANs -->
|
||||
- [ ] L161: <!-- TODO: SVG des Gerätetyps laden -->
|
||||
- [ ] L262: * TODO: Replace this mock logic with real AJAX once ports are
|
||||
|
||||
## app\modules\devices\list.php
|
||||
|
||||
- [ ] L206: //TODO löschen geht nicht
|
||||
|
||||
## app\modules\floor_infrastructure\edit.php
|
||||
|
||||
- [ ] L277: //TODO drag an drop auf der stockwerkskarte für die patchfelder und wandbuchsen. buchsen haben eine einheitliche größe, und sind quadratisch, patchfelder sind auch für sich einheitlich, sind rechteckig und breiter als hoch
|
||||
- [ ] L278: //TODO style in css files einsortieren
|
||||
|
||||
## app\modules\floor_infrastructure\list.php
|
||||
|
||||
- [ ] L143: <p><small>//TODO: SVG-Editor mit Drag & Drop für diese Objekte erweitern (siehe Stockwerke-Modul).</small></p>
|
||||
|
||||
## app\modules\floors\list.php
|
||||
|
||||
- [ ] L237: // TODO: AJAX-Delete implementieren
|
||||
|
||||
## app\modules\locations\edit.php
|
||||
|
||||
- [ ] L157: // TODO: AJAX-Delete implementieren
|
||||
|
||||
## app\modules\locations\list.php
|
||||
|
||||
- [ ] L134: //TODO design schlecht, mach es hübscher
|
||||
- [ ] L208: //TODO style in css file
|
||||
|
||||
## app\modules\racks\edit.php
|
||||
|
||||
- [ ] L201: // TODO: AJAX-Delete implementieren
|
||||
- [ ] L221: <!-- TODO: Rack-SVG laden -->
|
||||
- [ ] L237: <!-- TODO: Löschen, falls edit -->
|
||||
- [ ] L251: // TODO: Rack-ID aus PHP setzen
|
||||
- [ ] L254: // TODO: Gerätepositionen an JS übergeben
|
||||
|
||||
## app\modules\racks\list.php
|
||||
|
||||
- [ ] L255: // TODO: AJAX-Delete implementieren
|
||||
|
||||
## app\templates\footer.php
|
||||
|
||||
- [ ] L14: <!-- TODO: Optional: Statusanzeige, Debug-Info, Session-Hinweis -->
|
||||
- [ ] L17: <!-- TODO: evtl. JS für modale Fenster oder Flash Messages -->
|
||||
|
||||
## app\templates\header.php
|
||||
|
||||
- [ ] L24: <!-- TODO: Meta-Tags, Favicon -->
|
||||
|
||||
## app\templates\layout.php
|
||||
|
||||
- [ ] L11: * TODO: In Zukunft: zentrales Template-System (z.B. mit $content)
|
||||
- [ ] L18: <!-- TODO: Dynamischen Content hier einfügen -->
|
||||
|
||||
## BUGS.md
|
||||
|
||||
- [ ] L3: - [ ] TODO Design vereinheitlichen
|
||||
|
||||
## doc\DATABASE.md
|
||||
|
||||
- [ ] L126: **TODO**
|
||||
|
||||
## IMPLEMENTATION_STATUS.md
|
||||
|
||||
- [ ] L80: - [ ] **Delete-Funktionen** - Löschen noch als TODO (als AJAX implementieren)
|
||||
- [ ] L109: │ └── auth.php 🚧 TODO: Auth
|
||||
|
||||
## init.sql
|
||||
|
||||
- [ ] L372: -- TODO: Port-Konfiguration (Patchpanel ↔ Patchpanel, Patchpanel ↔ Netzwerkbuchse) wird über die `connections`-Tabelle geregelt.
|
||||
|
||||
## NEXT_STEPS.md
|
||||
|
||||
- [ ] L74: ## 🔧 Bekannte TODOs im Code
|
||||
- [ ] L76: Alle noch offenen Punkte sind mit `// TODO:` gekennzeichnet:
|
||||
- [ ] L79: # Alle TODOs finden:
|
||||
- [ ] L80: grep -r "TODO:" app/modules/ --include="*.php"
|
||||
- [ ] L83: Wichtigste TODOs:
|
||||
|
||||
## README.md
|
||||
|
||||
- [ ] L241: ### TODO: Patchpanel-Infrastruktur
|
||||
- [ ] L253: - TODO: SVG-Editor um Drag & Drop für diese Objekte erweitern und Klicks direkt mit dem Modul verbinden.
|
||||
|
||||
- [ ] //TODO infrastruktur patchfelder löschen soll implementiert werden.
|
||||
|
||||
## Topologie-Abgleich (16. Februar 2026)
|
||||
|
||||
- [ ] #TODO: `connections.port_a_type` und `connections.port_b_type` um einen Patchpanel-Port-Typ erweitern (z. B. `patchpanel`) und auf `floor_patchpanel_ports.id` referenzieren.
|
||||
- [ ] #TODO: Business-Regeln fuer Topologie in der Verbindungs-Validierung hinterlegen: Patchpanel-Port nur mit Patchpanel-Port oder Netzwerkbuchsen-Port verbinden.
|
||||
- [ ] #TODO: Port-CRUD fuer Patchpanels ergaenzen: `floor_patchpanel_ports` beim Speichern aus `port_count` erzeugen/synchronisieren.
|
||||
- [ ] #TODO: Port-CRUD fuer Netzwerkbuchsen ergaenzen: `network_outlet_ports` pflegen (mindestens ein Port je Buchse) und fuer Verbindungen nutzbar machen.
|
||||
- [ ] `connections.port_a_type` / `connections.port_b_type` um `patchpanel` erweitern und auf `floor_patchpanel_ports.id` referenzieren.
|
||||
- [ ] Validierungsregeln fuer Topologie fest verdrahten (Patchpanel-Port nur mit Patchpanel-Port oder Netzwerkbuchsen-Port).
|
||||
- [ ] Port-CRUD fuer Patchpanels: `floor_patchpanel_ports` aus `port_count` erzeugen/synchronisieren.
|
||||
- [ ] Port-CRUD fuer Netzwerkbuchsen: `network_outlet_ports` pflegen (mindestens ein Port je Buchse) und in Verbindungen nutzbar machen.
|
||||
|
||||
@@ -297,8 +297,83 @@ HTML, [
|
||||
* Sonstiges
|
||||
* ========================= */
|
||||
|
||||
// TODO: Weitere Helfer nach Bedarf
|
||||
// - Datum formatieren
|
||||
// - Bytes → MB
|
||||
// - UUID erzeugen
|
||||
// - SVG-Koordinaten normalisieren
|
||||
/**
|
||||
* Formatiert Datum/Uhrzeit robust oder gibt Fallback zurueck.
|
||||
*
|
||||
* @param string|null $value
|
||||
* @param string $format
|
||||
* @param string $fallback
|
||||
* @return string
|
||||
*/
|
||||
function formatDateTime(?string $value, string $format = 'd.m.Y H:i', string $fallback = '-'): string
|
||||
{
|
||||
if ($value === null || trim($value) === '') {
|
||||
return $fallback;
|
||||
}
|
||||
|
||||
$timestamp = strtotime($value);
|
||||
if ($timestamp === false) {
|
||||
return $fallback;
|
||||
}
|
||||
|
||||
return date($format, $timestamp);
|
||||
}
|
||||
|
||||
/**
|
||||
* Formatiert Byte-Werte in menschenlesbare Einheit.
|
||||
*
|
||||
* @param int|float $bytes
|
||||
* @param int $precision
|
||||
* @return string
|
||||
*/
|
||||
function formatBytes($bytes, int $precision = 2): string
|
||||
{
|
||||
$value = (float)$bytes;
|
||||
if ($value <= 0) {
|
||||
return '0 B';
|
||||
}
|
||||
|
||||
$units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
$power = min((int)floor(log($value, 1024)), count($units) - 1);
|
||||
$scaled = $value / (1024 ** $power);
|
||||
|
||||
return number_format($scaled, $precision, '.', '') . ' ' . $units[$power];
|
||||
}
|
||||
|
||||
/**
|
||||
* Erzeugt eine UUID v4.
|
||||
*
|
||||
* @return string
|
||||
* @throws Exception
|
||||
*/
|
||||
function generateUuidV4(): string
|
||||
{
|
||||
$bytes = random_bytes(16);
|
||||
$bytes[6] = chr((ord($bytes[6]) & 0x0f) | 0x40);
|
||||
$bytes[8] = chr((ord($bytes[8]) & 0x3f) | 0x80);
|
||||
$hex = bin2hex($bytes);
|
||||
|
||||
return sprintf(
|
||||
'%s-%s-%s-%s-%s',
|
||||
substr($hex, 0, 8),
|
||||
substr($hex, 8, 4),
|
||||
substr($hex, 12, 4),
|
||||
substr($hex, 16, 4),
|
||||
substr($hex, 20, 12)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Klemmt eine SVG-Koordinate auf gueltigen Bereich.
|
||||
*
|
||||
* @param float $value
|
||||
* @param float $min
|
||||
* @param float $max
|
||||
* @param int $precision
|
||||
* @return float
|
||||
*/
|
||||
function normalizeSvgCoordinate(float $value, float $min, float $max, int $precision = 2): float
|
||||
{
|
||||
$normalized = max($min, min($max, $value));
|
||||
return round($normalized, $precision);
|
||||
}
|
||||
|
||||
41
app/modules/connections/delete.php
Normal file
41
app/modules/connections/delete.php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
/**
|
||||
* app/modules/connections/delete.php
|
||||
*
|
||||
* Loescht eine Verbindung und leitet zur Liste zurueck.
|
||||
*/
|
||||
|
||||
$connectionId = (int)($_GET['id'] ?? $_POST['id'] ?? 0);
|
||||
|
||||
if ($connectionId <= 0) {
|
||||
$_SESSION['error'] = 'Ungueltige Verbindungs-ID';
|
||||
header('Location: ?module=connections&action=list');
|
||||
exit;
|
||||
}
|
||||
|
||||
$connection = $sql->single(
|
||||
"SELECT id FROM connections WHERE id = ?",
|
||||
"i",
|
||||
[$connectionId]
|
||||
);
|
||||
|
||||
if (!$connection) {
|
||||
$_SESSION['error'] = 'Verbindung nicht gefunden';
|
||||
header('Location: ?module=connections&action=list');
|
||||
exit;
|
||||
}
|
||||
|
||||
$rows = $sql->set(
|
||||
"DELETE FROM connections WHERE id = ?",
|
||||
"i",
|
||||
[$connectionId]
|
||||
);
|
||||
|
||||
if ($rows > 0) {
|
||||
$_SESSION['success'] = 'Verbindung geloescht';
|
||||
} else {
|
||||
$_SESSION['error'] = 'Verbindung konnte nicht geloescht werden';
|
||||
}
|
||||
|
||||
header('Location: ?module=connections&action=list');
|
||||
exit;
|
||||
@@ -2,19 +2,13 @@
|
||||
/**
|
||||
* app/modules/connections/list.php
|
||||
*
|
||||
* Übersicht der Netzwerkverbindungen
|
||||
* - Tabellarische Liste aller Verbindungen
|
||||
* - Filter nach Geräten, VLANs, Status
|
||||
* - Später: Visuelle Netzwerk-Topologie
|
||||
* Uebersicht der Netzwerkverbindungen
|
||||
*/
|
||||
|
||||
// =========================
|
||||
// Filter einlesen
|
||||
// =========================
|
||||
$search = trim($_GET['search'] ?? '');
|
||||
$deviceId = (int)($_GET['device_id'] ?? 0);
|
||||
$selectedConnectionId = (int)($_GET['connection_id'] ?? 0);
|
||||
|
||||
// Einheitliche Endpunkt-Aufloesung fuer polymorphe Port-Typen.
|
||||
$endpointUnionSql = "
|
||||
SELECT
|
||||
'device' AS endpoint_type,
|
||||
@@ -60,16 +54,13 @@ $endpointUnionSql = "
|
||||
LEFT JOIN floors f ON f.id = fp.floor_id
|
||||
";
|
||||
|
||||
// =========================
|
||||
// WHERE-Clause bauen
|
||||
// =========================
|
||||
$where = [];
|
||||
$types = '';
|
||||
$params = [];
|
||||
|
||||
if ($search !== '') {
|
||||
$where[] = "(e1.owner_name LIKE ? OR e2.owner_name LIKE ? OR e1.port_name LIKE ? OR e2.port_name LIKE ?)";
|
||||
$types .= "ssss";
|
||||
$types .= 'ssss';
|
||||
$params[] = "%$search%";
|
||||
$params[] = "%$search%";
|
||||
$params[] = "%$search%";
|
||||
@@ -78,16 +69,13 @@ if ($search !== '') {
|
||||
|
||||
if ($deviceId > 0) {
|
||||
$where[] = "(e1.owner_device_id = ? OR e2.owner_device_id = ?)";
|
||||
$types .= "ii";
|
||||
$types .= 'ii';
|
||||
$params[] = $deviceId;
|
||||
$params[] = $deviceId;
|
||||
}
|
||||
|
||||
$whereSql = $where ? "WHERE " . implode(" AND ", $where) : "";
|
||||
$whereSql = $where ? 'WHERE ' . implode(' AND ', $where) : '';
|
||||
|
||||
// =========================
|
||||
// Verbindungen laden
|
||||
// =========================
|
||||
$connections = $sql->get(
|
||||
"SELECT
|
||||
c.id,
|
||||
@@ -125,10 +113,22 @@ $connections = $sql->get(
|
||||
$params
|
||||
);
|
||||
|
||||
// =========================
|
||||
// Filter-Daten
|
||||
// =========================
|
||||
$devices = $sql->get("SELECT id, name FROM devices ORDER BY name", "", []);
|
||||
$devices = $sql->get('SELECT id, name FROM devices ORDER BY name', '', []);
|
||||
|
||||
$selectedConnection = null;
|
||||
if ($selectedConnectionId > 0) {
|
||||
foreach ((array)$connections as $entry) {
|
||||
if ((int)($entry['id'] ?? 0) === $selectedConnectionId) {
|
||||
$selectedConnection = $entry;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($selectedConnection === null && !empty($connections)) {
|
||||
$selectedConnection = $connections[0];
|
||||
$selectedConnectionId = (int)($selectedConnection['id'] ?? 0);
|
||||
}
|
||||
|
||||
$selectedDevice = null;
|
||||
$selectedDevicePorts = [];
|
||||
@@ -140,7 +140,7 @@ if ($deviceId > 0) {
|
||||
FROM devices d
|
||||
LEFT JOIN device_types dt ON d.device_type_id = dt.id
|
||||
WHERE d.id = ?",
|
||||
"i",
|
||||
'i',
|
||||
[$deviceId]
|
||||
);
|
||||
|
||||
@@ -159,7 +159,7 @@ if ($deviceId > 0) {
|
||||
JOIN device_ports dp ON dp.id = dpm.device_port_id
|
||||
WHERE dp.device_id = ?
|
||||
) p",
|
||||
"ii",
|
||||
'ii',
|
||||
[$deviceId, $deviceId]
|
||||
)['cnt'] ?? 0);
|
||||
|
||||
@@ -187,7 +187,7 @@ if ($deviceId > 0) {
|
||||
OR (c.port_b_type = 'patchpanel' AND e2.endpoint_type = 'floor_patchpanel')
|
||||
)
|
||||
WHERE e1.owner_device_id = ? OR e2.owner_device_id = ?",
|
||||
"ii",
|
||||
'ii',
|
||||
[$deviceId, $deviceId]
|
||||
)['cnt'] ?? 0);
|
||||
|
||||
@@ -207,7 +207,7 @@ if ($deviceId > 0) {
|
||||
) p
|
||||
ORDER BY sort_id
|
||||
LIMIT 12",
|
||||
"ii",
|
||||
'ii',
|
||||
[$deviceId, $deviceId]
|
||||
);
|
||||
|
||||
@@ -230,159 +230,247 @@ if ($deviceId > 0) {
|
||||
}
|
||||
}
|
||||
|
||||
$buildListUrl = static function (array $extra = []) use ($search, $deviceId): string {
|
||||
$query = ['module' => 'connections', 'action' => 'list'];
|
||||
if ($search !== '') {
|
||||
$query['search'] = $search;
|
||||
}
|
||||
if ($deviceId > 0) {
|
||||
$query['device_id'] = $deviceId;
|
||||
}
|
||||
foreach ($extra as $key => $value) {
|
||||
if ($value === null || $value === '') {
|
||||
continue;
|
||||
}
|
||||
$query[$key] = $value;
|
||||
}
|
||||
return '?' . http_build_query($query);
|
||||
};
|
||||
?>
|
||||
|
||||
<div class="connections-container">
|
||||
<h1>Netzwerkverbindungen</h1>
|
||||
<div class="connections-layout">
|
||||
<div class="connections-container">
|
||||
<h1>Netzwerkverbindungen</h1>
|
||||
|
||||
<!-- =========================
|
||||
Filter-Toolbar
|
||||
========================= -->
|
||||
<div class="filter-form">
|
||||
<form method="GET">
|
||||
<input type="hidden" name="module" value="connections">
|
||||
<input type="hidden" name="action" value="list">
|
||||
<div class="filter-form">
|
||||
<form method="GET">
|
||||
<input type="hidden" name="module" value="connections">
|
||||
<input type="hidden" name="action" value="list">
|
||||
|
||||
<input type="text" name="search" placeholder="Suche nach Gerät oder Port…"
|
||||
value="<?php echo htmlspecialchars($search); ?>" class="search-input">
|
||||
<input type="text" name="search" placeholder="Suche nach Geraet oder Port..."
|
||||
value="<?php echo htmlspecialchars($search); ?>" class="search-input">
|
||||
|
||||
<select name="device_id">
|
||||
<option value="">- Alle Geräte -</option>
|
||||
<?php foreach ($devices as $device): ?>
|
||||
<option value="<?php echo $device['id']; ?>"
|
||||
<?php echo $device['id'] === $deviceId ? 'selected' : ''; ?>>
|
||||
<?php echo htmlspecialchars($device['name']); ?>
|
||||
</option>
|
||||
<select name="device_id">
|
||||
<option value="">- Alle Geraete -</option>
|
||||
<?php foreach ($devices as $device): ?>
|
||||
<option value="<?php echo (int)$device['id']; ?>"
|
||||
<?php echo ((int)$device['id'] === $deviceId) ? 'selected' : ''; ?>>
|
||||
<?php echo htmlspecialchars((string)$device['name']); ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
|
||||
<button type="submit" class="button">Filter</button>
|
||||
<a href="?module=connections&action=list" class="button">Reset</a>
|
||||
<a href="?module=connections&action=edit" class="button button-primary">+ Neue Verbindung</a>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<?php if (!empty($connections)): ?>
|
||||
<table class="connections-list">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Von (Geraet -> Port)</th>
|
||||
<th>Nach (Geraet -> Port)</th>
|
||||
<th>VLANs</th>
|
||||
<th>Beschreibung</th>
|
||||
<th>Status</th>
|
||||
<th>Aktionen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($connections as $conn): ?>
|
||||
<?php
|
||||
$connId = (int)($conn['id'] ?? 0);
|
||||
$comment = trim((string)($conn['comment'] ?? ''));
|
||||
$hasMissingInfo = empty($conn['endpoint_a_name']) || empty($conn['endpoint_b_name'])
|
||||
|| empty($conn['port_a_name']) || empty($conn['port_b_name']);
|
||||
$commentLower = mb_strtolower($comment, 'UTF-8');
|
||||
$warningFromComment = preg_match('/warn|achtung|critical/', $commentLower) === 1;
|
||||
$hasWarning = $hasMissingInfo || $warningFromComment;
|
||||
$rowClass = $connId === $selectedConnectionId ? 'connection-row-selected' : '';
|
||||
$vlanList = [];
|
||||
if (!empty($conn['vlan_config'])) {
|
||||
$vlanList = (array)json_decode((string)$conn['vlan_config'], true);
|
||||
}
|
||||
?>
|
||||
<tr class="<?php echo $rowClass; ?>">
|
||||
<td>
|
||||
<strong><?php echo htmlspecialchars((string)($conn['endpoint_a_name'] ?? 'N/A')); ?></strong><br>
|
||||
<small><?php echo htmlspecialchars((string)($conn['port_a_name'] ?? '-')); ?></small>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<strong><?php echo htmlspecialchars((string)($conn['endpoint_b_name'] ?? 'N/A')); ?></strong><br>
|
||||
<small><?php echo htmlspecialchars((string)($conn['port_b_name'] ?? '-')); ?></small>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<small><?php echo !empty($vlanList) ? htmlspecialchars(implode(', ', $vlanList)) : '-'; ?></small>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<small><?php echo htmlspecialchars($comment); ?></small>
|
||||
</td>
|
||||
|
||||
<td class="status-cell">
|
||||
<?php if ($hasWarning): ?>
|
||||
<span class="status-badge status-badge-warning" title="Unvollstaendige oder kritische Verbindung">Warnung</span>
|
||||
<?php else: ?>
|
||||
<span class="status-badge status-badge-ok" title="Verbindung vollstaendig">OK</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
|
||||
<td class="actions">
|
||||
<a href="<?php echo htmlspecialchars($buildListUrl(['connection_id' => $connId])); ?>" class="button button-small">Details</a>
|
||||
<a href="?module=connections&action=edit&id=<?php echo $connId; ?>" class="button button-small">Bearbeiten</a>
|
||||
<a href="?module=connections&action=swap&id=<?php echo $connId; ?>" class="button button-small" onclick="return confirm('Von/Nach fuer diese Verbindung vertauschen?');">Von/Nach tauschen</a>
|
||||
<a href="?module=connections&action=delete&id=<?php echo $connId; ?>" class="button button-small button-danger"
|
||||
data-confirm-delete="true"
|
||||
data-confirm-message="Diese Verbindung wirklich loeschen?">
|
||||
Loeschen
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
|
||||
<button type="submit" class="button">Filter</button>
|
||||
<a href="?module=connections&action=list" class="button">Reset</a>
|
||||
<a href="?module=connections&action=edit" class="button button-primary">+ Neue Verbindung</a>
|
||||
</form>
|
||||
</tbody>
|
||||
</table>
|
||||
<?php else: ?>
|
||||
<div class="empty-state">
|
||||
<p>Keine Verbindungen gefunden.</p>
|
||||
<p>
|
||||
<a href="?module=connections&action=edit" class="button button-primary">
|
||||
Erste Verbindung anlegen
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<!-- =========================
|
||||
Verbindungs-Tabelle
|
||||
========================= -->
|
||||
<?php if (!empty($connections)): ?>
|
||||
<table class="connections-list">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Von (Gerät → Port)</th>
|
||||
<th>Nach (Gerät → Port)</th>
|
||||
<th>VLANs</th>
|
||||
<th>Beschreibung</th>
|
||||
<th>Status</th>
|
||||
<th>Aktionen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($connections as $conn): ?>
|
||||
<?php
|
||||
$comment = trim($conn['comment'] ?? '');
|
||||
$hasMissingInfo = empty($conn['endpoint_a_name']) || empty($conn['endpoint_b_name'])
|
||||
|| empty($conn['port_a_name']) || empty($conn['port_b_name']);
|
||||
$commentLower = mb_strtolower($comment, 'UTF-8');
|
||||
$warningFromComment = preg_match('/warn|achtung|critical/', $commentLower);
|
||||
$hasWarning = $hasMissingInfo || $warningFromComment;
|
||||
?>
|
||||
<tr>
|
||||
<td>
|
||||
<strong><?php echo htmlspecialchars($conn['endpoint_a_name'] ?? 'N/A'); ?></strong><br>
|
||||
<small><?php echo htmlspecialchars($conn['port_a_name'] ?? '—'); ?></small>
|
||||
</td>
|
||||
<aside class="connections-sidebar">
|
||||
<section class="sidebar-card">
|
||||
<h3>Ausgewaehlte Verbindung</h3>
|
||||
<?php if ($selectedConnection): ?>
|
||||
<?php
|
||||
$selectedConnId = (int)($selectedConnection['id'] ?? 0);
|
||||
$selectedVlans = [];
|
||||
if (!empty($selectedConnection['vlan_config'])) {
|
||||
$selectedVlans = (array)json_decode((string)$selectedConnection['vlan_config'], true);
|
||||
}
|
||||
?>
|
||||
<p><strong>ID:</strong> #<?php echo $selectedConnId; ?></p>
|
||||
<p><strong>Von:</strong><br>
|
||||
<?php echo htmlspecialchars((string)($selectedConnection['endpoint_a_name'] ?? 'N/A')); ?><br>
|
||||
<small><?php echo htmlspecialchars((string)($selectedConnection['port_a_name'] ?? '-')); ?></small>
|
||||
</p>
|
||||
<p><strong>Nach:</strong><br>
|
||||
<?php echo htmlspecialchars((string)($selectedConnection['endpoint_b_name'] ?? 'N/A')); ?><br>
|
||||
<small><?php echo htmlspecialchars((string)($selectedConnection['port_b_name'] ?? '-')); ?></small>
|
||||
</p>
|
||||
<p><strong>VLANs:</strong> <?php echo !empty($selectedVlans) ? htmlspecialchars(implode(', ', $selectedVlans)) : '-'; ?></p>
|
||||
<p><strong>Kommentar:</strong> <?php echo htmlspecialchars((string)($selectedConnection['comment'] ?? '-')); ?></p>
|
||||
<div class="sidebar-actions">
|
||||
<a href="?module=connections&action=edit&id=<?php echo $selectedConnId; ?>" class="button button-small">Bearbeiten</a>
|
||||
<a href="?module=connections&action=swap&id=<?php echo $selectedConnId; ?>" class="button button-small" onclick="return confirm('Von/Nach fuer diese Verbindung vertauschen?');">Tauschen</a>
|
||||
<a href="?module=connections&action=delete&id=<?php echo $selectedConnId; ?>" class="button button-small button-danger" data-confirm-delete="true" data-confirm-message="Diese Verbindung wirklich loeschen?">Loeschen</a>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<p><em>Keine Verbindung ausgewaehlt.</em></p>
|
||||
<?php endif; ?>
|
||||
</section>
|
||||
|
||||
<td>
|
||||
<strong><?php echo htmlspecialchars($conn['endpoint_b_name'] ?? 'N/A'); ?></strong><br>
|
||||
<small><?php echo htmlspecialchars($conn['port_b_name'] ?? '—'); ?></small>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<small>
|
||||
<?php
|
||||
if ($conn['vlan_config']) {
|
||||
$vlan = json_decode($conn['vlan_config'], true);
|
||||
echo htmlspecialchars(implode(', ', (array)$vlan));
|
||||
} else {
|
||||
echo '—';
|
||||
}
|
||||
?>
|
||||
</small>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<small><?php echo htmlspecialchars($conn['comment'] ?? ''); ?></small>
|
||||
</td>
|
||||
<td class="status-cell">
|
||||
<?php if ($hasWarning): ?>
|
||||
<span class="status-badge status-badge-warning" title="Unvollständige oder kritische Verbindung">
|
||||
⚠️ Warnung
|
||||
</span>
|
||||
<section class="sidebar-card">
|
||||
<?php if ($selectedDevice): ?>
|
||||
<h3>Ausgewaehltes Geraet</h3>
|
||||
<p><strong><?php echo htmlspecialchars((string)$selectedDevice['name']); ?></strong></p>
|
||||
<p>Typ: <?php echo htmlspecialchars((string)($selectedDevice['type_name'] ?? '-')); ?></p>
|
||||
<p>Ports: <?php echo (int)$selectedDevice['port_count']; ?></p>
|
||||
<p>Verbindungen: <?php echo (int)$selectedDevice['connection_count']; ?></p>
|
||||
<p>
|
||||
VLANs:
|
||||
<?php if (!empty($selectedDeviceVlans)): ?>
|
||||
<?php echo htmlspecialchars(implode(', ', $selectedDeviceVlans)); ?>
|
||||
<?php else: ?>
|
||||
<span class="status-badge status-badge-ok" title="Verbindung vollständig">
|
||||
✔️ OK
|
||||
</span>
|
||||
-
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
|
||||
<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?"
|
||||
data-confirm-feedback="Löschen noch nicht implementiert">
|
||||
Löschen
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<?php else: ?>
|
||||
<div class="empty-state">
|
||||
<p>Keine Verbindungen gefunden.</p>
|
||||
<p>
|
||||
<a href="?module=connections&action=edit" class="button button-primary">
|
||||
Erste Verbindung anlegen
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</p>
|
||||
<?php if (!empty($selectedDevicePorts)): ?>
|
||||
<h4>Ports (max. 12)</h4>
|
||||
<ul>
|
||||
<?php foreach ($selectedDevicePorts as $port): ?>
|
||||
<li><?php echo htmlspecialchars((string)$port['name']); ?></li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
<?php endif; ?>
|
||||
<?php else: ?>
|
||||
<h3>Ausgewaehltes Geraet</h3>
|
||||
<p><em>Bitte ein Geraet im Filter auswaehlen.</em></p>
|
||||
<?php endif; ?>
|
||||
</section>
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
<!-- =========================
|
||||
Sidebar / Details
|
||||
========================= -->
|
||||
<style>
|
||||
.connections-layout {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 320px;
|
||||
gap: 20px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
<aside class="sidebar">
|
||||
<?php if ($selectedDevice): ?>
|
||||
<h3>Ausgewähltes Gerät</h3>
|
||||
<p><strong><?php echo htmlspecialchars($selectedDevice['name']); ?></strong></p>
|
||||
<p>Typ: <?php echo htmlspecialchars($selectedDevice['type_name'] ?? '—'); ?></p>
|
||||
<p>Ports: <?php echo (int)$selectedDevice['port_count']; ?></p>
|
||||
<p>Verbindungen: <?php echo (int)$selectedDevice['connection_count']; ?></p>
|
||||
<p>
|
||||
VLANs:
|
||||
<?php if (!empty($selectedDeviceVlans)): ?>
|
||||
<?php echo htmlspecialchars(implode(', ', $selectedDeviceVlans)); ?>
|
||||
<?php else: ?>
|
||||
—
|
||||
<?php endif; ?>
|
||||
</p>
|
||||
<?php if (!empty($selectedDevicePorts)): ?>
|
||||
<h4>Ports (max. 12)</h4>
|
||||
<ul>
|
||||
<?php foreach ($selectedDevicePorts as $port): ?>
|
||||
<li><?php echo htmlspecialchars($port['name']); ?></li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
<?php endif; ?>
|
||||
<?php else: ?>
|
||||
<p><em>Bitte ein Gerät im Filter auswählen.</em></p>
|
||||
<?php endif; ?>
|
||||
.connections-sidebar {
|
||||
position: sticky;
|
||||
top: 92px;
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
<!-- TODO: Verbindung bearbeiten / löschen -->
|
||||
</aside>
|
||||
.sidebar-card {
|
||||
background: #fff;
|
||||
border: 1px solid #e0e6ef;
|
||||
border-radius: 12px;
|
||||
padding: 14px;
|
||||
box-shadow: 0 8px 24px rgba(15, 26, 45, 0.08);
|
||||
}
|
||||
|
||||
.sidebar-card h3,
|
||||
.sidebar-card h4 {
|
||||
margin: 0 0 10px;
|
||||
}
|
||||
|
||||
.sidebar-card p {
|
||||
margin: 0 0 8px;
|
||||
}
|
||||
|
||||
.sidebar-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.connection-row-selected {
|
||||
background: #edf5ff;
|
||||
}
|
||||
|
||||
@media (max-width: 1100px) {
|
||||
.connections-layout {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.connections-sidebar {
|
||||
position: static;
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,15 +1,9 @@
|
||||
<?php
|
||||
/**
|
||||
* modules/dashboard/list.php
|
||||
* Dashboard / Startseite - Übersicht über alle Komponenten
|
||||
* Dashboard / Startseite - Uebersicht ueber alle Komponenten
|
||||
*/
|
||||
|
||||
// =========================
|
||||
// Statistiken aus DB laden
|
||||
// =========================
|
||||
|
||||
//TODO eine große zoombare verschiebbare svg wand machen, mit allen punkten drauf. anklicken der punkte erzeugt ein overlay, mit der reingezoomten ansicht zb einem rack
|
||||
|
||||
$stats = [
|
||||
'devices' => $sql->single("SELECT COUNT(*) as cnt FROM devices", "", [])['cnt'] ?? 0,
|
||||
'device_types' => $sql->single("SELECT COUNT(*) as cnt FROM device_types", "", [])['cnt'] ?? 0,
|
||||
@@ -18,7 +12,6 @@ $stats = [
|
||||
'locations' => $sql->single("SELECT COUNT(*) as cnt FROM locations", "", [])['cnt'] ?? 0,
|
||||
];
|
||||
|
||||
// Recent devices
|
||||
$recentDevices = $sql->get(
|
||||
"SELECT d.id, d.name, dt.name as type_name, r.name as rack_name, f.name as floor_name
|
||||
FROM devices d
|
||||
@@ -26,12 +19,41 @@ $recentDevices = $sql->get(
|
||||
LEFT JOIN racks r ON d.rack_id = r.id
|
||||
LEFT JOIN floors f ON r.floor_id = f.id
|
||||
ORDER BY d.id DESC LIMIT 5",
|
||||
"", []
|
||||
"",
|
||||
[]
|
||||
);
|
||||
|
||||
$topologyDevices = $sql->get(
|
||||
"SELECT
|
||||
d.id AS device_id,
|
||||
d.name AS device_name,
|
||||
dt.name AS device_type_name,
|
||||
r.id AS rack_id,
|
||||
r.name AS rack_name,
|
||||
f.id AS floor_id,
|
||||
f.name AS floor_name
|
||||
FROM devices d
|
||||
LEFT JOIN device_types dt ON dt.id = d.device_type_id
|
||||
LEFT JOIN racks r ON r.id = d.rack_id
|
||||
LEFT JOIN floors f ON f.id = r.floor_id
|
||||
ORDER BY floor_name, rack_name, device_name",
|
||||
"",
|
||||
[]
|
||||
);
|
||||
|
||||
$topologyPayload = array_map(static function (array $row): array {
|
||||
return [
|
||||
'device_id' => (int)($row['device_id'] ?? 0),
|
||||
'device_name' => (string)($row['device_name'] ?? ''),
|
||||
'device_type_name' => (string)($row['device_type_name'] ?? ''),
|
||||
'rack_id' => (int)($row['rack_id'] ?? 0),
|
||||
'rack_name' => (string)($row['rack_name'] ?? ''),
|
||||
'floor_id' => (int)($row['floor_id'] ?? 0),
|
||||
'floor_name' => (string)($row['floor_name'] ?? ''),
|
||||
];
|
||||
}, $topologyDevices);
|
||||
?>
|
||||
|
||||
<!-- Dashboard / Übersicht -->
|
||||
<div class="dashboard">
|
||||
<h1>Dashboard</h1>
|
||||
<div id="dashboard-modules" class="dashboard-modules"></div>
|
||||
@@ -39,35 +61,63 @@ $recentDevices = $sql->get(
|
||||
<p data-dashboard-warnings class="dashboard-inline-status"></p>
|
||||
<p data-dashboard-recent class="dashboard-inline-status"></p>
|
||||
|
||||
<!-- Statistik-Karten -->
|
||||
<section class="topology-wall" id="dashboard-topology-wall">
|
||||
<div class="topology-wall__header">
|
||||
<h2>Gesamt-Topologie-Wand</h2>
|
||||
<div class="topology-wall__tools">
|
||||
<button type="button" class="button button-small" data-topology-zoom="in">+</button>
|
||||
<button type="button" class="button button-small" data-topology-zoom="out">-</button>
|
||||
<button type="button" class="button button-small" data-topology-zoom="reset">Reset</button>
|
||||
</div>
|
||||
</div>
|
||||
<p class="topology-wall__hint">Mausrad zoomt, Ziehen verschiebt. Klick auf einen Punkt zoomt auf den Rack-Kontext und oeffnet die Detailkarte.</p>
|
||||
|
||||
<svg id="dashboard-topology-svg" viewBox="0 0 2400 1400" role="img" aria-label="Topologie-Wand">
|
||||
<rect x="0" y="0" width="2400" height="1400" class="topology-bg"></rect>
|
||||
<g id="dashboard-topology-grid"></g>
|
||||
<g id="dashboard-topology-layer"></g>
|
||||
</svg>
|
||||
|
||||
<div id="dashboard-topology-empty" class="topology-empty" hidden>Keine Geraete vorhanden. Bitte zuerst Geraete erfassen.</div>
|
||||
|
||||
<aside id="dashboard-topology-overlay" class="topology-overlay" hidden>
|
||||
<div class="topology-overlay__header">
|
||||
<h3 data-topology-title>Rack-Detail</h3>
|
||||
<button type="button" class="button button-small" data-topology-close>Schliessen</button>
|
||||
</div>
|
||||
<p data-topology-meta></p>
|
||||
<p data-topology-rack-link></p>
|
||||
<p data-topology-device-link></p>
|
||||
</aside>
|
||||
</section>
|
||||
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<h3><?php echo $stats['locations']; ?></h3>
|
||||
<h3><?php echo (int)$stats['locations']; ?></h3>
|
||||
<p>Standorte</p>
|
||||
<a href="?module=floors&action=list">Verwalten →</a>
|
||||
<a href="?module=floors&action=list">Verwalten -></a>
|
||||
</div>
|
||||
|
||||
<div class="stat-card">
|
||||
<h3><?php echo $stats['device_types']; ?></h3>
|
||||
<p>Gerätetypen</p>
|
||||
<a href="?module=device_types&action=list">Verwalten →</a>
|
||||
<h3><?php echo (int)$stats['device_types']; ?></h3>
|
||||
<p>Geraetetypen</p>
|
||||
<a href="?module=device_types&action=list">Verwalten -></a>
|
||||
</div>
|
||||
|
||||
<div class="stat-card">
|
||||
<h3><?php echo $stats['devices']; ?></h3>
|
||||
<p>Geräte</p>
|
||||
<a href="?module=devices&action=list">Verwalten →</a>
|
||||
<h3><?php echo (int)$stats['devices']; ?></h3>
|
||||
<p>Geraete</p>
|
||||
<a href="?module=devices&action=list">Verwalten -></a>
|
||||
</div>
|
||||
|
||||
<div class="stat-card">
|
||||
<h3><?php echo $stats['racks']; ?></h3>
|
||||
<h3><?php echo (int)$stats['racks']; ?></h3>
|
||||
<p>Racks</p>
|
||||
<a href="?module=racks&action=list">Verwalten →</a>
|
||||
<a href="?module=racks&action=list">Verwalten -></a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Zuletzt hinzugefügte Geräte -->
|
||||
<h2>Zuletzt hinzugefügt</h2>
|
||||
<h2>Zuletzt hinzugefuegt</h2>
|
||||
<?php if (!empty($recentDevices)): ?>
|
||||
<table class="recent-devices">
|
||||
<thead>
|
||||
@@ -82,20 +132,379 @@ $recentDevices = $sql->get(
|
||||
<tbody>
|
||||
<?php foreach ($recentDevices as $device): ?>
|
||||
<tr>
|
||||
<td><?php echo htmlspecialchars($device['name']); ?></td>
|
||||
<td><?php echo htmlspecialchars($device['type_name'] ?? '-'); ?></td>
|
||||
<td><?php echo htmlspecialchars($device['rack_name'] ?? '-'); ?></td>
|
||||
<td><?php echo htmlspecialchars($device['floor_name'] ?? '-'); ?></td>
|
||||
<td><a href="?module=devices&action=edit&id=<?php echo $device['id']; ?>">Bearbeiten</a></td>
|
||||
<td><?php echo htmlspecialchars((string)$device['name']); ?></td>
|
||||
<td><?php echo htmlspecialchars((string)($device['type_name'] ?? '-')); ?></td>
|
||||
<td><?php echo htmlspecialchars((string)($device['rack_name'] ?? '-')); ?></td>
|
||||
<td><?php echo htmlspecialchars((string)($device['floor_name'] ?? '-')); ?></td>
|
||||
<td><a href="?module=devices&action=edit&id=<?php echo (int)$device['id']; ?>">Bearbeiten</a></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<?php else: ?>
|
||||
<p><em>Noch keine Geräte vorhanden. <a href="?module=device_types&action=list">Starten Sie mit Gerätetypen</a>.</em></p>
|
||||
<p><em>Noch keine Geraete vorhanden. <a href="?module=device_types&action=list">Starten Sie mit Geraetetypen</a>.</em></p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<script id="dashboard-topology-data" type="application/json"><?php echo json_encode($topologyPayload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); ?></script>
|
||||
<script>
|
||||
(function () {
|
||||
const root = document.getElementById('dashboard-topology-wall');
|
||||
if (!root) {
|
||||
return;
|
||||
}
|
||||
|
||||
const svg = document.getElementById('dashboard-topology-svg');
|
||||
const gridLayer = document.getElementById('dashboard-topology-grid');
|
||||
const nodeLayer = document.getElementById('dashboard-topology-layer');
|
||||
const emptyNode = document.getElementById('dashboard-topology-empty');
|
||||
const overlay = document.getElementById('dashboard-topology-overlay');
|
||||
const overlayTitle = overlay ? overlay.querySelector('[data-topology-title]') : null;
|
||||
const overlayMeta = overlay ? overlay.querySelector('[data-topology-meta]') : null;
|
||||
const overlayRackLink = overlay ? overlay.querySelector('[data-topology-rack-link]') : null;
|
||||
const overlayDeviceLink = overlay ? overlay.querySelector('[data-topology-device-link]') : null;
|
||||
const closeButton = overlay ? overlay.querySelector('[data-topology-close]') : null;
|
||||
|
||||
const dataTag = document.getElementById('dashboard-topology-data');
|
||||
let nodes = [];
|
||||
try {
|
||||
nodes = JSON.parse((dataTag && dataTag.textContent) || '[]');
|
||||
} catch (error) {
|
||||
nodes = [];
|
||||
}
|
||||
|
||||
const scene = { width: 2400, height: 1400 };
|
||||
let view = { x: 0, y: 0, width: scene.width, height: scene.height };
|
||||
let drag = null;
|
||||
|
||||
function clampView() {
|
||||
view.width = Math.max(220, Math.min(scene.width, view.width));
|
||||
view.height = Math.max(180, Math.min(scene.height, view.height));
|
||||
view.x = Math.max(0, Math.min(scene.width - view.width, view.x));
|
||||
view.y = Math.max(0, Math.min(scene.height - view.height, view.y));
|
||||
}
|
||||
|
||||
function applyView() {
|
||||
clampView();
|
||||
svg.setAttribute('viewBox', `${view.x} ${view.y} ${view.width} ${view.height}`);
|
||||
}
|
||||
|
||||
function toSvgPoint(clientX, clientY) {
|
||||
const rect = svg.getBoundingClientRect();
|
||||
const x = view.x + ((clientX - rect.left) / rect.width) * view.width;
|
||||
const y = view.y + ((clientY - rect.top) / rect.height) * view.height;
|
||||
return { x, y };
|
||||
}
|
||||
|
||||
function zoomAt(anchorX, anchorY, factor) {
|
||||
const nextWidth = view.width * factor;
|
||||
const nextHeight = view.height * factor;
|
||||
|
||||
const ratioX = (anchorX - view.x) / view.width;
|
||||
const ratioY = (anchorY - view.y) / view.height;
|
||||
|
||||
view = {
|
||||
x: anchorX - ratioX * nextWidth,
|
||||
y: anchorY - ratioY * nextHeight,
|
||||
width: nextWidth,
|
||||
height: nextHeight
|
||||
};
|
||||
applyView();
|
||||
}
|
||||
|
||||
function resetView() {
|
||||
view = { x: 0, y: 0, width: scene.width, height: scene.height };
|
||||
applyView();
|
||||
}
|
||||
|
||||
function zoomToNode(x, y) {
|
||||
view = {
|
||||
x: x - 250,
|
||||
y: y - 170,
|
||||
width: 500,
|
||||
height: 340
|
||||
};
|
||||
applyView();
|
||||
}
|
||||
|
||||
function clearLayer(layer) {
|
||||
while (layer.firstChild) {
|
||||
layer.removeChild(layer.firstChild);
|
||||
}
|
||||
}
|
||||
|
||||
function svgElement(name) {
|
||||
return document.createElementNS('http://www.w3.org/2000/svg', name);
|
||||
}
|
||||
|
||||
function drawGrid() {
|
||||
clearLayer(gridLayer);
|
||||
for (let x = 0; x <= scene.width; x += 100) {
|
||||
const line = svgElement('line');
|
||||
line.setAttribute('x1', String(x));
|
||||
line.setAttribute('y1', '0');
|
||||
line.setAttribute('x2', String(x));
|
||||
line.setAttribute('y2', String(scene.height));
|
||||
line.setAttribute('class', 'topology-grid-line');
|
||||
gridLayer.appendChild(line);
|
||||
}
|
||||
|
||||
for (let y = 0; y <= scene.height; y += 100) {
|
||||
const line = svgElement('line');
|
||||
line.setAttribute('x1', '0');
|
||||
line.setAttribute('y1', String(y));
|
||||
line.setAttribute('x2', String(scene.width));
|
||||
line.setAttribute('y2', String(y));
|
||||
line.setAttribute('class', 'topology-grid-line');
|
||||
gridLayer.appendChild(line);
|
||||
}
|
||||
}
|
||||
|
||||
function buildLayout(data) {
|
||||
const floors = new Map();
|
||||
|
||||
data.forEach((entry) => {
|
||||
const floorName = (entry.floor_name || '').trim() || 'Ohne Stockwerk';
|
||||
const rackId = Number(entry.rack_id || 0);
|
||||
const rackName = (entry.rack_name || '').trim() || 'Ohne Rack';
|
||||
const rackKey = `${rackId}:${rackName}`;
|
||||
|
||||
if (!floors.has(floorName)) {
|
||||
floors.set(floorName, new Map());
|
||||
}
|
||||
const racks = floors.get(floorName);
|
||||
if (!racks.has(rackKey)) {
|
||||
racks.set(rackKey, []);
|
||||
}
|
||||
racks.get(rackKey).push(entry);
|
||||
});
|
||||
|
||||
const floorNames = Array.from(floors.keys()).sort((a, b) => a.localeCompare(b));
|
||||
const positioned = [];
|
||||
|
||||
floorNames.forEach((floorName, floorIndex) => {
|
||||
const rackMap = floors.get(floorName);
|
||||
const rackKeys = Array.from(rackMap.keys()).sort((a, b) => a.localeCompare(b));
|
||||
const floorX = 140 + floorIndex * 540;
|
||||
|
||||
positioned.push({ type: 'floor-label', x: floorX, y: 70, label: floorName });
|
||||
|
||||
rackKeys.forEach((rackKey, rackIndex) => {
|
||||
const devices = rackMap.get(rackKey);
|
||||
const [rackIdPart, rackNamePart] = rackKey.split(':');
|
||||
const rackId = Number(rackIdPart || 0);
|
||||
const rackName = rackNamePart || 'Ohne Rack';
|
||||
|
||||
const rackTop = 120 + rackIndex * 180;
|
||||
const rowCount = Math.ceil(Math.max(1, devices.length) / 4);
|
||||
const rackHeight = Math.max(90, 40 + rowCount * 45);
|
||||
|
||||
positioned.push({
|
||||
type: 'rack-box',
|
||||
x: floorX - 50,
|
||||
y: rackTop - 30,
|
||||
width: 380,
|
||||
height: rackHeight,
|
||||
label: rackName,
|
||||
rack_id: rackId
|
||||
});
|
||||
|
||||
devices.forEach((device, deviceIndex) => {
|
||||
const col = deviceIndex % 4;
|
||||
const row = Math.floor(deviceIndex / 4);
|
||||
const x = floorX + col * 85;
|
||||
const y = rackTop + row * 40;
|
||||
|
||||
positioned.push({
|
||||
type: 'node',
|
||||
x,
|
||||
y,
|
||||
rack_id: Number(device.rack_id || 0),
|
||||
rack_name: device.rack_name || 'Ohne Rack',
|
||||
floor_name: device.floor_name || 'Ohne Stockwerk',
|
||||
device_id: Number(device.device_id || 0),
|
||||
device_name: device.device_name || 'Unbenannt',
|
||||
device_type_name: device.device_type_name || ''
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
return positioned;
|
||||
}
|
||||
|
||||
function showOverlay(node) {
|
||||
if (!overlay || !overlayTitle || !overlayMeta || !overlayRackLink || !overlayDeviceLink) {
|
||||
return;
|
||||
}
|
||||
|
||||
overlayTitle.textContent = node.rack_name || 'Ohne Rack';
|
||||
const typeLabel = node.device_type_name ? ` (${node.device_type_name})` : '';
|
||||
overlayMeta.textContent = `Stockwerk: ${node.floor_name} | Geraet: ${node.device_name}${typeLabel}`;
|
||||
|
||||
if (node.rack_id > 0) {
|
||||
overlayRackLink.innerHTML = `<a href="?module=racks&action=edit&id=${node.rack_id}">Rack bearbeiten</a>`;
|
||||
} else {
|
||||
overlayRackLink.textContent = 'Kein Rack verknuepft';
|
||||
}
|
||||
|
||||
if (node.device_id > 0) {
|
||||
overlayDeviceLink.innerHTML = `<a href="?module=devices&action=edit&id=${node.device_id}">Geraet bearbeiten</a>`;
|
||||
} else {
|
||||
overlayDeviceLink.textContent = '';
|
||||
}
|
||||
|
||||
overlay.hidden = false;
|
||||
}
|
||||
|
||||
function renderTopology() {
|
||||
drawGrid();
|
||||
clearLayer(nodeLayer);
|
||||
|
||||
if (!nodes.length) {
|
||||
if (emptyNode) {
|
||||
emptyNode.hidden = false;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (emptyNode) {
|
||||
emptyNode.hidden = true;
|
||||
}
|
||||
|
||||
const entries = buildLayout(nodes);
|
||||
|
||||
entries.forEach((entry) => {
|
||||
if (entry.type === 'floor-label') {
|
||||
const text = svgElement('text');
|
||||
text.setAttribute('x', String(entry.x));
|
||||
text.setAttribute('y', String(entry.y));
|
||||
text.setAttribute('class', 'topology-floor-label');
|
||||
text.textContent = entry.label;
|
||||
nodeLayer.appendChild(text);
|
||||
return;
|
||||
}
|
||||
|
||||
if (entry.type === 'rack-box') {
|
||||
const rect = svgElement('rect');
|
||||
rect.setAttribute('x', String(entry.x));
|
||||
rect.setAttribute('y', String(entry.y));
|
||||
rect.setAttribute('width', String(entry.width));
|
||||
rect.setAttribute('height', String(entry.height));
|
||||
rect.setAttribute('rx', '10');
|
||||
rect.setAttribute('class', 'topology-rack-box');
|
||||
nodeLayer.appendChild(rect);
|
||||
|
||||
const label = svgElement('text');
|
||||
label.setAttribute('x', String(entry.x + 12));
|
||||
label.setAttribute('y', String(entry.y + 22));
|
||||
label.setAttribute('class', 'topology-rack-label');
|
||||
label.textContent = entry.label;
|
||||
nodeLayer.appendChild(label);
|
||||
return;
|
||||
}
|
||||
|
||||
const circle = svgElement('circle');
|
||||
circle.setAttribute('cx', String(entry.x));
|
||||
circle.setAttribute('cy', String(entry.y));
|
||||
circle.setAttribute('r', '10');
|
||||
circle.setAttribute('tabindex', '0');
|
||||
circle.setAttribute('class', 'topology-node');
|
||||
circle.setAttribute('data-device-id', String(entry.device_id));
|
||||
circle.setAttribute('data-rack-id', String(entry.rack_id));
|
||||
|
||||
const title = svgElement('title');
|
||||
title.textContent = `${entry.device_name} (${entry.floor_name} / ${entry.rack_name})`;
|
||||
circle.appendChild(title);
|
||||
|
||||
const activate = () => {
|
||||
nodeLayer.querySelectorAll('.topology-node.active').forEach((node) => {
|
||||
node.classList.remove('active');
|
||||
});
|
||||
circle.classList.add('active');
|
||||
zoomToNode(entry.x, entry.y);
|
||||
showOverlay(entry);
|
||||
};
|
||||
|
||||
circle.addEventListener('click', activate);
|
||||
circle.addEventListener('keydown', (event) => {
|
||||
if (event.key === 'Enter' || event.key === ' ') {
|
||||
event.preventDefault();
|
||||
activate();
|
||||
}
|
||||
});
|
||||
|
||||
nodeLayer.appendChild(circle);
|
||||
});
|
||||
}
|
||||
|
||||
svg.addEventListener('wheel', (event) => {
|
||||
event.preventDefault();
|
||||
const point = toSvgPoint(event.clientX, event.clientY);
|
||||
const factor = event.deltaY < 0 ? 0.9 : 1.1;
|
||||
zoomAt(point.x, point.y, factor);
|
||||
}, { passive: false });
|
||||
|
||||
svg.addEventListener('pointerdown', (event) => {
|
||||
drag = {
|
||||
startX: event.clientX,
|
||||
startY: event.clientY,
|
||||
baseX: view.x,
|
||||
baseY: view.y
|
||||
};
|
||||
svg.setPointerCapture(event.pointerId);
|
||||
});
|
||||
|
||||
svg.addEventListener('pointermove', (event) => {
|
||||
if (!drag) {
|
||||
return;
|
||||
}
|
||||
const rect = svg.getBoundingClientRect();
|
||||
const scaleX = view.width / rect.width;
|
||||
const scaleY = view.height / rect.height;
|
||||
const dx = (event.clientX - drag.startX) * scaleX;
|
||||
const dy = (event.clientY - drag.startY) * scaleY;
|
||||
view.x = drag.baseX - dx;
|
||||
view.y = drag.baseY - dy;
|
||||
applyView();
|
||||
});
|
||||
|
||||
svg.addEventListener('pointerup', (event) => {
|
||||
drag = null;
|
||||
svg.releasePointerCapture(event.pointerId);
|
||||
});
|
||||
|
||||
svg.addEventListener('pointercancel', (event) => {
|
||||
drag = null;
|
||||
svg.releasePointerCapture(event.pointerId);
|
||||
});
|
||||
|
||||
root.querySelectorAll('[data-topology-zoom]').forEach((button) => {
|
||||
button.addEventListener('click', () => {
|
||||
const action = button.getAttribute('data-topology-zoom');
|
||||
if (action === 'in') {
|
||||
zoomAt(view.x + view.width / 2, view.y + view.height / 2, 0.85);
|
||||
return;
|
||||
}
|
||||
if (action === 'out') {
|
||||
zoomAt(view.x + view.width / 2, view.y + view.height / 2, 1.15);
|
||||
return;
|
||||
}
|
||||
resetView();
|
||||
});
|
||||
});
|
||||
|
||||
if (closeButton && overlay) {
|
||||
closeButton.addEventListener('click', () => {
|
||||
overlay.hidden = true;
|
||||
});
|
||||
}
|
||||
|
||||
renderTopology();
|
||||
applyView();
|
||||
})();
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
@@ -186,7 +595,8 @@ $recentDevices = $sql->get(
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.recent-devices th, .recent-devices td {
|
||||
.recent-devices th,
|
||||
.recent-devices td {
|
||||
padding: 10px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid #ddd;
|
||||
@@ -195,4 +605,134 @@ $recentDevices = $sql->get(
|
||||
.recent-devices th {
|
||||
background: #f0f0f0;
|
||||
}
|
||||
|
||||
.topology-wall {
|
||||
margin: 18px 0 26px;
|
||||
border: 1px solid #d6e1f0;
|
||||
border-radius: 10px;
|
||||
background: #fff;
|
||||
padding: 14px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.topology-wall__header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.topology-wall__header h2 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.topology-wall__tools {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.topology-wall__hint {
|
||||
margin: 8px 0 10px;
|
||||
color: #445067;
|
||||
}
|
||||
|
||||
#dashboard-topology-svg {
|
||||
width: 100%;
|
||||
height: 460px;
|
||||
border: 1px solid #d7dee9;
|
||||
border-radius: 8px;
|
||||
background: #f8fbff;
|
||||
cursor: grab;
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
#dashboard-topology-svg:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.topology-bg {
|
||||
fill: #f7faff;
|
||||
}
|
||||
|
||||
.topology-grid-line {
|
||||
stroke: #e4ebf5;
|
||||
stroke-width: 1;
|
||||
}
|
||||
|
||||
.topology-floor-label {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
fill: #23304a;
|
||||
}
|
||||
|
||||
.topology-rack-box {
|
||||
fill: #ffffff;
|
||||
stroke: #bfd0e6;
|
||||
stroke-width: 2;
|
||||
}
|
||||
|
||||
.topology-rack-label {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
fill: #2a3d5c;
|
||||
}
|
||||
|
||||
.topology-node {
|
||||
fill: #1f73c9;
|
||||
stroke: #ffffff;
|
||||
stroke-width: 3;
|
||||
}
|
||||
|
||||
.topology-node:hover,
|
||||
.topology-node:focus,
|
||||
.topology-node.active {
|
||||
fill: #e5572e;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.topology-empty {
|
||||
margin-top: 12px;
|
||||
padding: 10px;
|
||||
background: #f4f7fb;
|
||||
border: 1px dashed #b8c7dd;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.topology-overlay {
|
||||
position: absolute;
|
||||
right: 16px;
|
||||
bottom: 16px;
|
||||
width: min(360px, calc(100% - 32px));
|
||||
border: 1px solid #cad8ea;
|
||||
border-radius: 10px;
|
||||
background: #ffffff;
|
||||
box-shadow: 0 12px 34px rgba(12, 42, 84, 0.2);
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.topology-overlay__header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.topology-overlay__header h3 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.topology-overlay p {
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
#dashboard-topology-svg {
|
||||
height: 360px;
|
||||
}
|
||||
|
||||
.topology-wall__header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
|
||||
<!-- JS -->
|
||||
<script src="/assets/js/app.js" defer></script>
|
||||
<script src="/assets/js/dashboard.js" defer></script>
|
||||
<script src="/assets/js/svg-editor.js" defer></script>
|
||||
<script src="/assets/js/network-view.js" defer></script>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user