Merge pull request 'todos' (#21) from todos into main
Reviewed-on: #21
This commit was merged in pull request #21.
This commit is contained in:
124
AGENTS.md
124
AGENTS.md
@@ -1,88 +1,58 @@
|
|||||||
# AGENTS.md
|
# AGENTS.md
|
||||||
|
|
||||||
## Ziel der Datei
|
|
||||||
Dieses Dokument beschreibt, welche Informationen ich als Agent für `p:\netwatch` erwarten würde: Projektziel, Setup, Regeln, Skills, bekannte Issues, Kontext & Einschränkungen.
|
|
||||||
|
|
||||||
## Ziel
|
## Ziel
|
||||||
Codex soll bei der Pflege von `NEXT_STEPS.md` immer die zugehörigen Gitea-Issues berücksichtigen und erledigte Aufgaben über Commit-Messages schließen.
|
Codex arbeitet pragmatisch bei Aufgaben aus `NEXT.md` und User-Requests.
|
||||||
|
Ein Gitea-Issue ist optional.
|
||||||
|
|
||||||
## Projektüberblick
|
## Kernregeln
|
||||||
- Name: **netwatch** – ein Netzwerk-Dokumentations- und Verkabelungsverwaltungs-Tool (Alpha v0.2, Core-Module funktionsfähig, Stand: 13. Februar 2026).
|
|
||||||
- Features: Dashboard, Gerätetypen-/Geräteverwaltung, Racks/Floors mit SVG-Planung, Verbindungen inkl. VLANs, Module, grafische Ansichten (Rack, Netzwerkgraph, Stockwerke/Räume).
|
|
||||||
- Datenmodell: zentrales SQL-Schema (`locations`, `device_types`, `devices`, `connections` etc.) mit JSON-Erweiterungsmöglichkeiten.
|
|
||||||
- Projektphasen (Phase 1–4) sind im README gelistet, siehe letzte Abschnitte.
|
|
||||||
|
|
||||||
## Schneller Projektstart
|
1. Ein Issue ist **nicht erforderlich**, um eine Aufgabe umzusetzen.
|
||||||
```powershell
|
2. Skills duerfen jederzeit verwendet werden (z. B. `gitea-issues`).
|
||||||
docker-compose up -d --build
|
3. Ein `NEXT.md`-Punkt darf erst auf erledigt (`[x]`) gesetzt werden, wenn die Umsetzung im Code erfolgt ist.
|
||||||
# danach: http://localhost
|
4. Nur wenn ein Gitea-Issue konkret referenziert ist **und** durch die Aenderung abgeschlossen wird, muss die Commit-Message `closes #<id>` enthalten.
|
||||||
```
|
5. Jede `closes`-Referenz steht in einer **eigenen Zeile**.
|
||||||
Das Docker-Setup (Compose + Portainer) liegt in `docker-compose.yml` und `docker-portainer.yml`, ergänzende Infos in `Dockerfile`.
|
6. Kein `closes #<id>`, wenn das Issue nicht tatsaechlich abgeschlossen ist.
|
||||||
|
7. `git push` nur auf explizite Aufforderung; standardmaessig nur committen.
|
||||||
|
|
||||||
## Verbindliche Regeln
|
## Verbindlicher Ablauf
|
||||||
1. Vor jeder Änderung an `NEXT_STEPS.md` müssen offene Issues geladen werden.
|
|
||||||
2. Jeder umsetzbare Punkt in `NEXT_STEPS.md` muss eine Issue-Referenz im Format `[#<id>]` enthalten.
|
|
||||||
3. Ein Punkt darf nur als erledigt markiert werden, wenn:
|
|
||||||
- die Umsetzung im Code erfolgt ist, und
|
|
||||||
- ein Commit mit `closes #<id>` erstellt wird.
|
|
||||||
4. Kein „done“ ohne Issue-ID und kein Commit ohne passende `closes #<id>`-Referenz.
|
|
||||||
5. Wenn mehrere Issues betroffen sind, alle in der Commit-Message aufführen (z. B. `closes #12, closes #18`).
|
|
||||||
6. Beim Erstellen neuer NEXT_STEPS-Punkte sollen möglichst bestehende offene Issues verlinkt statt Duplikate erzeugt werden.
|
|
||||||
|
|
||||||
## Workflow für Codex
|
1. Aufgabe umsetzen (aus `NEXT.md` oder User-Anfrage).
|
||||||
1. Offene Issues abrufen (Skill `gitea-issues`):
|
2. Optional Issues laden, wenn Kontext/Zuordnung noetig ist:
|
||||||
- `python C:/Users/s.titz/.codex/skills/gitea-issues/scripts/list_issues.py <owner> <repo> --state open --limit 100 --json`
|
- `python C:/Users/s.titz/.codex/skills/gitea-issues/scripts/list_issues.py <owner> <repo> --state open --limit 100 --json`
|
||||||
2. `NEXT_STEPS.md` aktualisieren:
|
3. `NEXT.md` bei Bedarf aktualisieren (mit oder ohne `[#<id>]`).
|
||||||
- Punkte mit `[#<id>]` ergänzen oder korrigieren.
|
4. Commit erstellen.
|
||||||
3. Umsetzung durchführen.
|
5. Wenn Issue abgeschlossen wird, Commit-Message mit eigener `closes`-Zeile schreiben.
|
||||||
4. Commit mit Schließ-Referenz erstellen:
|
|
||||||
- `git commit -m "Kurzbeschreibung der Änderung; closes #<id>"`
|
|
||||||
5. Prüfen, dass jede als erledigt markierte Aufgabe eine geschlossene Issue-Referenz hat.
|
|
||||||
|
|
||||||
## Formatvorgabe für NEXT_STEPS.md
|
## Commit-Format bei Issue-Abschluss
|
||||||
- Beispiel offen:
|
|
||||||
- `- [ ] [#42] Backup-Runbook erstellen`
|
Beispiel mit einem Issue:
|
||||||
- Beispiel erledigt:
|
|
||||||
|
```text
|
||||||
|
Kurzbeschreibung der Aenderung
|
||||||
|
|
||||||
|
closes #42
|
||||||
|
```
|
||||||
|
|
||||||
|
Beispiel mit mehreren Issues:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Kurzbeschreibung der Aenderung
|
||||||
|
|
||||||
|
closes #12
|
||||||
|
closes #18
|
||||||
|
```
|
||||||
|
|
||||||
|
## Format fuer NEXT.md
|
||||||
|
|
||||||
|
- Offen ohne Issue:
|
||||||
|
- `- [ ] //TODO Backup-Runbook erstellen`
|
||||||
|
- Offen mit Issue:
|
||||||
|
- `- [ ] [#42] //TODO Backup-Runbook erstellen`
|
||||||
|
- Erledigt mit Issue:
|
||||||
- `- [x] [#42] Backup-Runbook erstellen`
|
- `- [x] [#42] Backup-Runbook erstellen`
|
||||||
|
- Erledigt ohne Issue:
|
||||||
|
- `- [x] Backup-Runbook erstellen`
|
||||||
|
|
||||||
## Annahmen
|
## Annahme
|
||||||
- Gitea ist so konfiguriert, dass `closes #<id>` in Commit-Messages das Issue schließt.
|
|
||||||
- `GITEA_TOKEN` ist gesetzt, damit Issue-Abfragen funktionieren.
|
|
||||||
|
|
||||||
## Skills & Nutzungshinweise
|
- Gitea ist so konfiguriert, dass `closes #<id>` in Commit-Messages das Issue schliesst.
|
||||||
- **skill-creator** – Anleitung zum Erstellen bzw. Erweitern eigener Skills. Pfad: `C:/Users/s.titz/.codex/skills/.system/skill-creator/SKILL.md`.
|
|
||||||
- **skill-installer** – Anleitung zum Installieren zusätzlicher Skills aus Kurationslisten oder GitHub. Pfad: `C:/Users/s.titz/.codex/skills/.system/skill-installer/SKILL.md`.
|
|
||||||
|
|
||||||
Wenn ein Skill genannt wird (z. B. `$skill-creator`) oder die Aufgabe exakt zur Beschreibung passt, muss dieser Skill in dem Turn verwendet werden. Skills immer erst öffnen (`SKILL.md`), nur nötige Teile lesen, relative Pfade innerhalb des Skill-Verzeichnisses auflösen. Bei mehreren Skills: minimaler Satz in sinnvoller Reihenfolge, kurz ankündigen, warum welche Skills genutzt wurden.
|
|
||||||
|
|
||||||
## Lokale Arbeitsregeln
|
|
||||||
- Arbeitsumgebung: Windows, Pfad `P:\netwatch`, Shell `powershell`. Schreibzugriff für mich hier ist verboten; Änderungen müssen vom Nutzer übernommen werden.
|
|
||||||
- Suche: Nutze `rg`/`rg --files` statt `grep`/`find` für Geschwindigkeit.
|
|
||||||
- Codeänderungen: Nur ASCII-Zeichen einführen (außer bestehende Dateien nutzen Unicode); Formate ohne `apply_patch` nur wenn nötig; bevorzuge `apply_patch`.
|
|
||||||
- Keine destruktiven Git-Befehle ohne ausdrückliche Aufforderung (z. B. keinen `reset --hard`).
|
|
||||||
- Tests/Builds: Wenn nötig, nenne passende Tests/Prüfmethoden als nächsten Schritt.
|
|
||||||
- Kommunikation: Verwende beim Antworten absolute Datumsangaben (z. B. „13. Februar 2026“), wenn sich jemand auf „heute/morgen“ bezieht, um Missverständnisse zu vermeiden.
|
|
||||||
|
|
||||||
## Bekannte Bugs (aus `BUGS.md`)
|
|
||||||
- Gerät löschen funktioniert nicht (Status unklar).
|
|
||||||
- Gerätetypen SVG-Modul: Malfunktion.
|
|
||||||
- Ports Drag & Drop (Funktion unklar).
|
|
||||||
- Beim Erstellen von Gerätetypen soll ein voreingestelltes Rechteck basierend auf 19-Zoll & HE-Größe erzeugt werden, das als Grundgerüst dient.
|
|
||||||
- Device-Typ-Erstellung: Klick auf Objekt-Typ-Button, dann Drag-Drop für Diagonale und Loslassen fixiert Position.
|
|
||||||
|
|
||||||
## Weitere Ressourcen
|
|
||||||
- `NEXT_STEPS.md` (aktuelles ToDo/Roadmap).
|
|
||||||
- `IMPLEMENTATION_STATUS.md` (Status-Tracking).
|
|
||||||
- `README.md` (Feature- und Architekturübersicht).
|
|
||||||
|
|
||||||
## Besonderheiten / Kommunikation
|
|
||||||
- Aktuelles Datum: Freitag, 13. Februar 2026 (nicht überschreiben).
|
|
||||||
- Keine Netzwerkanfragen möglich; Referenzen nur lokal nutzen.
|
|
||||||
- Wenn ein Agent spezielle Instruktionen benötigt (z. B. Skill-Anwendung), immer darauf hinweisen und ggf. den Nutzer nach Bestätigung fragen.
|
|
||||||
|
|
||||||
## Einschränkungen
|
|
||||||
- Sandbox ist lesend; bitte selbst `AGENTS.md` anlegen.
|
|
||||||
- Jegliche Ausgaben/Antworten sollten den Developer-Guidelines folgen (kurz, teamorientiert, klare nächste Schritte).
|
|
||||||
|
|
||||||
## Wichtig
|
|
||||||
- Nutze UTF-8, wenn nicht anders angegeben.
|
|
||||||
|
|||||||
3
BUGS.md
3
BUGS.md
@@ -1,3 +0,0 @@
|
|||||||
# gefundene bugs
|
|
||||||
- [ ] device löschen geht nicht
|
|
||||||
- [ ] TODO Design vereinheitlichen
|
|
||||||
9
NEXT.md
Normal file
9
NEXT.md
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# NEXT_STEPS
|
||||||
|
|
||||||
|
## Aktive Aufgaben (priorisiert)
|
||||||
|
|
||||||
|
## Verifikation (Status unklar, nicht als erledigt markieren ohne Reproduktion + Commit)
|
||||||
|
- [x] [#15] Neue Verbindung: Netzwerkdose auswählbar (Regressionstest in UI durchgeführt)
|
||||||
|
|
||||||
|
## gefundene bugs
|
||||||
|
- [x] Design vereinheitlichen
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
# NEXT_STEPS
|
|
||||||
|
|
||||||
## Stand
|
|
||||||
- Letzte Pflege: 18. Februar 2026
|
|
||||||
- Quelle für Issues: lokale Referenzen aus Repository (`NEXT_STEPS.md`, `BUGS.md`, Code-Check)
|
|
||||||
- Hinweis: Live-Abruf via `gitea-issues` war am 18. Februar 2026 nicht möglich (Verbindung zu Gitea verweigert).
|
|
||||||
|
|
||||||
## Aktive Aufgaben (priorisiert)
|
|
||||||
- [ ] [#10] Dashboard-Grafik erzeugen (Location/Building/Floor/Verbindungen als Hierarchie)
|
|
||||||
- [ ] [#5] Dashboard als zoombare und verschiebbare SVG-Fläche umsetzen (interaktive Geräte/Ports/Verbindungen)
|
|
||||||
- [ ] [#14] Hilfslinien der Stockwerkskarten nur im Edit-Mode anzeigen, im Anzeige-Mode ausblenden
|
|
||||||
- [ ] [#11] Encoding- und Umlautfehler bereinigen (inkl. Anzeige in UI-Dateien und Markdown-Dokumenten)
|
|
||||||
- [ ] [#4] `device_types/edit`: Option "Ports automatisch erstellen" nur beim Erstellen anzeigen, nicht beim Editieren
|
|
||||||
|
|
||||||
## Verifikation (Status unklar, nicht als erledigt markieren ohne Reproduktion + Commit)
|
|
||||||
- [ ] [#15] Neue Verbindung: Netzwerkdose auswählbar (Regressionstest in UI durchführen)
|
|
||||||
|
|
||||||
## Hinweise zur Abarbeitung
|
|
||||||
- Vor jeder Änderung an dieser Datei offene Issues erneut laden (`gitea-issues`-Skill).
|
|
||||||
- Aufgaben hier nur mit Issue-Referenz `[#<id>]` führen.
|
|
||||||
- Aufgabe erst auf erledigt setzen, wenn Code umgesetzt und Commit mit `closes #<id>` erstellt wurde.
|
|
||||||
36
TODO.md
36
TODO.md
@@ -1,36 +0,0 @@
|
|||||||
# TODO
|
|
||||||
|
|
||||||
Bereinigte und aktuelle TODO-Liste (Stand: 18. Februar 2026).
|
|
||||||
Quelle: vorhandene `TODO`-Marker im Repository plus offene Architekturpunkte.
|
|
||||||
|
|
||||||
## Erledigt (bereits umgesetzt)
|
|
||||||
|
|
||||||
- [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.
|
|
||||||
|
|
||||||
## Offen (direkt im Code markiert)
|
|
||||||
|
|
||||||
- [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`).
|
|
||||||
|
|
||||||
## Offen (Bugs / Doku / Statusdateien)
|
|
||||||
|
|
||||||
- [ ] `BUGS.md`: Design vereinheitlichen.
|
|
||||||
- [x] `IMPLEMENTATION_STATUS.md`: Delete-Funktionen-Status aktualisiert.
|
|
||||||
- [x] `IMPLEMENTATION_STATUS.md`: Auth-Status aktualisiert.
|
|
||||||
- [x] `README.md`: Patchpanel-Infrastruktur-Status nachgezogen.
|
|
||||||
- [x] `README.md`: SVG-Editor-Status fuer Floor-Infrastruktur nachgezogen.
|
|
||||||
- [x] `doc/DATABASE.md`: Statusabschnitt fuer Patchpanel/Floorplan finalisiert.
|
|
||||||
- [x] `init.sql`: Port-Konfigurationsregeln konkretisiert.
|
|
||||||
|
|
||||||
## Topologie-Backlog (ausstehend)
|
|
||||||
|
|
||||||
- [x] `connections.port_a_type` / `connections.port_b_type` um `patchpanel` erweitert und auf `floor_patchpanel_ports.id` nutzbar gemacht.
|
|
||||||
- [ ] Validierungsregeln fuer Topologie fest verdrahten (Patchpanel-Port nur mit Patchpanel-Port oder Netzwerkbuchsen-Port).
|
|
||||||
- [x] Port-CRUD fuer Patchpanels: `floor_patchpanel_ports` wird aus `port_count` erzeugt/synchronisiert.
|
|
||||||
- [x] Port-CRUD fuer Netzwerkbuchsen: `network_outlet_ports` wird gepflegt (mindestens ein Port je Buchse) und ist in Verbindungen nutzbar.
|
|
||||||
@@ -77,6 +77,19 @@ function endpointExists($sql, string $type, int $id): bool
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isTopologyPairAllowed(string $typeA, string $typeB): bool
|
||||||
|
{
|
||||||
|
$allowed = ['device' => true, 'module' => true, 'outlet' => true, 'patchpanel' => true];
|
||||||
|
if (!isset($allowed[$typeA]) || !isset($allowed[$typeB])) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if ($typeA === 'patchpanel' || $typeB === 'patchpanel') {
|
||||||
|
return ($typeA === 'patchpanel' && in_array($typeB, ['patchpanel', 'outlet'], true))
|
||||||
|
|| ($typeB === 'patchpanel' && in_array($typeA, ['patchpanel', 'outlet'], true));
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
function loadConnections($sql): void
|
function loadConnections($sql): void
|
||||||
{
|
{
|
||||||
$contextType = strtolower(trim((string)($_GET['context_type'] ?? 'all')));
|
$contextType = strtolower(trim((string)($_GET['context_type'] ?? 'all')));
|
||||||
@@ -189,6 +202,9 @@ function saveConnection($sql): void
|
|||||||
if ($portAId <= 0 || $portBId <= 0) {
|
if ($portAId <= 0 || $portBId <= 0) {
|
||||||
jsonError('port_a_id und port_b_id sind erforderlich', 400);
|
jsonError('port_a_id und port_b_id sind erforderlich', 400);
|
||||||
}
|
}
|
||||||
|
if (!isTopologyPairAllowed($portAType, $portBType)) {
|
||||||
|
jsonError('Patchpanel-Ports duerfen nur mit Patchpanel-Ports oder Netzwerkdosen-Ports verbunden werden', 400);
|
||||||
|
}
|
||||||
|
|
||||||
if ($portAType === $portBType && $portAId === $portBId) {
|
if ($portAType === $portBType && $portAId === $portBId) {
|
||||||
jsonError('Port A und Port B duerfen nicht identisch sein', 400);
|
jsonError('Port A und Port B duerfen nicht identisch sein', 400);
|
||||||
|
|||||||
@@ -208,6 +208,48 @@ main {
|
|||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.connections-layout {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) 320px;
|
||||||
|
gap: 20px;
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connections-sidebar {
|
||||||
|
position: sticky;
|
||||||
|
top: 92px;
|
||||||
|
display: grid;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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: 900px) {
|
@media (max-width: 900px) {
|
||||||
.connections-list th,
|
.connections-list th,
|
||||||
.connections-list td {
|
.connections-list td {
|
||||||
@@ -215,6 +257,17 @@ main {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1100px) {
|
||||||
|
.connections-layout {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connections-sidebar {
|
||||||
|
position: static;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 900px) {
|
@media (max-width: 900px) {
|
||||||
.app-header {
|
.app-header {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@@ -51,8 +51,49 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function enforceTopologyTypeRules(typeA, typeB) {
|
||||||
|
const allowWithPatchpanel = { patchpanel: true, outlet: true };
|
||||||
|
const selectedA = typeA.value;
|
||||||
|
const selectedB = typeB.value;
|
||||||
|
|
||||||
|
const applyRules = (sourceType, targetSelect) => {
|
||||||
|
for (const option of targetSelect.options) {
|
||||||
|
const value = option.value;
|
||||||
|
if (!value) {
|
||||||
|
option.disabled = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (sourceType === 'patchpanel') {
|
||||||
|
option.disabled = !allowWithPatchpanel[value];
|
||||||
|
} else {
|
||||||
|
option.disabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (targetSelect.selectedOptions.length > 0 && targetSelect.selectedOptions[0].disabled) {
|
||||||
|
targetSelect.value = '';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
applyRules(selectedA, typeB);
|
||||||
|
applyRules(selectedB, typeA);
|
||||||
|
}
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
bindPair('port_a_type', 'port_a_id');
|
bindPair('port_a_type', 'port_a_id');
|
||||||
bindPair('port_b_type', 'port_b_id');
|
bindPair('port_b_type', 'port_b_id');
|
||||||
|
|
||||||
|
const typeA = document.getElementById('port_a_type');
|
||||||
|
const typeB = document.getElementById('port_b_type');
|
||||||
|
if (!typeA || !typeB) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const syncRules = () => {
|
||||||
|
enforceTopologyTypeRules(typeA, typeB);
|
||||||
|
};
|
||||||
|
|
||||||
|
syncRules();
|
||||||
|
typeA.addEventListener('change', syncRules);
|
||||||
|
typeB.addEventListener('change', syncRules);
|
||||||
});
|
});
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -254,6 +254,10 @@
|
|||||||
background.setAttribute('stroke-width', '1');
|
background.setAttribute('stroke-width', '1');
|
||||||
svg.appendChild(background);
|
svg.appendChild(background);
|
||||||
|
|
||||||
|
const style = createSvgElement('style');
|
||||||
|
style.textContent = '.floor-guide{display:none;}';
|
||||||
|
svg.appendChild(style);
|
||||||
|
|
||||||
state.guides.forEach((guide) => {
|
state.guides.forEach((guide) => {
|
||||||
const line = createSvgElement('line');
|
const line = createSvgElement('line');
|
||||||
if (guide.orientation === 'horizontal') {
|
if (guide.orientation === 'horizontal') {
|
||||||
|
|||||||
@@ -77,6 +77,9 @@ $isEndpointAllowed = static function (string $type, int $id) use ($occupiedByTyp
|
|||||||
if ($id <= 0) {
|
if ($id <= 0) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if ($type === 'outlet') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
if ($type === $portAType && $id === $portAId) {
|
if ($type === $portAType && $id === $portAId) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -426,61 +426,6 @@ $buildListUrl = static function (array $extra = []) use ($search, $deviceId): st
|
|||||||
</aside>
|
</aside>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
|
||||||
.connections-layout {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: minmax(0, 1fr) 320px;
|
|
||||||
gap: 20px;
|
|
||||||
align-items: start;
|
|
||||||
}
|
|
||||||
|
|
||||||
.connections-sidebar {
|
|
||||||
position: sticky;
|
|
||||||
top: 92px;
|
|
||||||
display: grid;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.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>
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
document.querySelectorAll('.js-connection-delete').forEach((button) => {
|
document.querySelectorAll('.js-connection-delete').forEach((button) => {
|
||||||
|
|||||||
@@ -42,6 +42,18 @@ $normalizePortType = static function (string $value): string {
|
|||||||
$portAType = $normalizePortType((string)$portAType);
|
$portAType = $normalizePortType((string)$portAType);
|
||||||
$portBType = $normalizePortType((string)$portBType);
|
$portBType = $normalizePortType((string)$portBType);
|
||||||
|
|
||||||
|
$isTopologyPairAllowed = static function (string $typeA, string $typeB): bool {
|
||||||
|
$allowed = ['device' => true, 'module' => true, 'outlet' => true, 'patchpanel' => true];
|
||||||
|
if (!isset($allowed[$typeA]) || !isset($allowed[$typeB])) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if ($typeA === 'patchpanel' || $typeB === 'patchpanel') {
|
||||||
|
return ($typeA === 'patchpanel' && in_array($typeB, ['patchpanel', 'outlet'], true))
|
||||||
|
|| ($typeB === 'patchpanel' && in_array($typeA, ['patchpanel', 'outlet'], true));
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
// =========================
|
// =========================
|
||||||
// Validierung (einfach)
|
// Validierung (einfach)
|
||||||
// =========================
|
// =========================
|
||||||
@@ -50,6 +62,9 @@ $errors = [];
|
|||||||
if ($portAId <= 0 || $portBId <= 0) {
|
if ($portAId <= 0 || $portBId <= 0) {
|
||||||
$errors[] = "Beide Ports sind erforderlich";
|
$errors[] = "Beide Ports sind erforderlich";
|
||||||
}
|
}
|
||||||
|
if (!$isTopologyPairAllowed($portAType, $portBType)) {
|
||||||
|
$errors[] = "Patchpanel-Ports duerfen nur mit Patchpanel-Ports oder Netzwerkdosen-Ports verbunden werden";
|
||||||
|
}
|
||||||
|
|
||||||
$otherConnections = $sql->get(
|
$otherConnections = $sql->get(
|
||||||
"SELECT id, port_a_type, port_a_id, port_b_type, port_b_id
|
"SELECT id, port_a_type, port_a_id, port_b_type, port_b_id
|
||||||
@@ -59,31 +74,73 @@ $otherConnections = $sql->get(
|
|||||||
[$connId]
|
[$connId]
|
||||||
);
|
);
|
||||||
|
|
||||||
$isEndpointUsed = static function (string $endpointType, int $endpointId) use ($otherConnections, $normalizePortType): bool {
|
$endpointUsage = [];
|
||||||
|
$trackUsage = static function (string $endpointType, int $endpointId, string $otherType) use (&$endpointUsage): void {
|
||||||
if ($endpointId <= 0) {
|
if ($endpointId <= 0) {
|
||||||
return false;
|
return;
|
||||||
}
|
}
|
||||||
foreach ((array)$otherConnections as $row) {
|
if (!isset($endpointUsage[$endpointType][$endpointId])) {
|
||||||
$typeA = $normalizePortType((string)($row['port_a_type'] ?? ''));
|
$endpointUsage[$endpointType][$endpointId] = [
|
||||||
$idA = (int)($row['port_a_id'] ?? 0);
|
'total' => 0,
|
||||||
if ($typeA === $endpointType && $idA === $endpointId) {
|
'patchpanel' => 0,
|
||||||
return true;
|
'other' => 0,
|
||||||
}
|
];
|
||||||
|
}
|
||||||
$typeB = $normalizePortType((string)($row['port_b_type'] ?? ''));
|
$endpointUsage[$endpointType][$endpointId]['total']++;
|
||||||
$idB = (int)($row['port_b_id'] ?? 0);
|
if ($endpointType === 'outlet') {
|
||||||
if ($typeB === $endpointType && $idB === $endpointId) {
|
if ($otherType === 'patchpanel') {
|
||||||
return true;
|
$endpointUsage[$endpointType][$endpointId]['patchpanel']++;
|
||||||
|
} else {
|
||||||
|
$endpointUsage[$endpointType][$endpointId]['other']++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if ($isEndpointUsed($portAType, $portAId)) {
|
foreach ((array)$otherConnections as $row) {
|
||||||
$errors[] = "Port an Endpunkt A ist bereits in Verwendung";
|
$typeA = $normalizePortType((string)($row['port_a_type'] ?? ''));
|
||||||
|
$typeB = $normalizePortType((string)($row['port_b_type'] ?? ''));
|
||||||
|
$idA = (int)($row['port_a_id'] ?? 0);
|
||||||
|
$idB = (int)($row['port_b_id'] ?? 0);
|
||||||
|
|
||||||
|
$trackUsage($typeA, $idA, $typeB);
|
||||||
|
$trackUsage($typeB, $idB, $typeA);
|
||||||
}
|
}
|
||||||
if ($isEndpointUsed($portBType, $portBId)) {
|
|
||||||
$errors[] = "Port an Endpunkt B ist bereits in Verwendung";
|
$validateEndpointUsage = static function (string $endpointType, int $endpointId, string $otherType, string $label) use ($endpointUsage): ?string {
|
||||||
|
if ($endpointId <= 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$stats = $endpointUsage[$endpointType][$endpointId] ?? ['total' => 0, 'patchpanel' => 0, 'other' => 0];
|
||||||
|
if ((int)$stats['total'] <= 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($endpointType !== 'outlet') {
|
||||||
|
return $label . " ist bereits in Verwendung";
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($otherType === 'patchpanel') {
|
||||||
|
if ((int)$stats['patchpanel'] > 0) {
|
||||||
|
return $label . " hat bereits eine Patchpanel-Verbindung";
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((int)$stats['other'] > 0) {
|
||||||
|
return $label . " hat bereits eine Endgeraete-Verbindung";
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
$errorA = $validateEndpointUsage($portAType, $portAId, $portBType, 'Port an Endpunkt A');
|
||||||
|
if ($errorA !== null) {
|
||||||
|
$errors[] = $errorA;
|
||||||
|
}
|
||||||
|
|
||||||
|
$errorB = $validateEndpointUsage($portBType, $portBId, $portAType, 'Port an Endpunkt B');
|
||||||
|
if ($errorB !== null) {
|
||||||
|
$errors[] = $errorB;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!empty($errors)) {
|
if (!empty($errors)) {
|
||||||
|
|||||||
@@ -86,6 +86,7 @@ $pageTitle = $isEdit ? "Gerätetyp bearbeiten: " . htmlspecialchars($deviceType[
|
|||||||
<textarea id="comment" name="comment" rows="3"
|
<textarea id="comment" name="comment" rows="3"
|
||||||
placeholder="z.B. Rack-Mount, 48 RJ45 + 4 SFP"><?php echo htmlspecialchars($deviceType['comment'] ?? ''); ?></textarea>
|
placeholder="z.B. Rack-Mount, 48 RJ45 + 4 SFP"><?php echo htmlspecialchars($deviceType['comment'] ?? ''); ?></textarea>
|
||||||
</div>
|
</div>
|
||||||
|
<?php if (!$isEdit): ?>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="seed_ports">Ports automatisch anlegen</label>
|
<label for="seed_ports">Ports automatisch anlegen</label>
|
||||||
<input type="number" id="seed_ports" name="seed_ports" min="0" step="1"
|
<input type="number" id="seed_ports" name="seed_ports" min="0" step="1"
|
||||||
@@ -106,6 +107,7 @@ $pageTitle = $isEdit ? "Gerätetyp bearbeiten: " . htmlspecialchars($deviceType[
|
|||||||
</select>
|
</select>
|
||||||
<small>Wird beim automatischen Erstellen neuer Ports als Startwert gesetzt.</small>
|
<small>Wird beim automatischen Erstellen neuer Ports als Startwert gesetzt.</small>
|
||||||
</div>
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
<!-- =========================
|
<!-- =========================
|
||||||
|
|||||||
@@ -21,7 +21,8 @@ $deviceTypeId = (int)($_POST['id'] ?? 0);
|
|||||||
$name = trim($_POST['name'] ?? '');
|
$name = trim($_POST['name'] ?? '');
|
||||||
$category = $_POST['category'] ?? 'other';
|
$category = $_POST['category'] ?? 'other';
|
||||||
$comment = trim($_POST['comment'] ?? '');
|
$comment = trim($_POST['comment'] ?? '');
|
||||||
$seedPortCount = max(0, (int)($_POST['seed_ports'] ?? 0));
|
$isCreate = $deviceTypeId <= 0;
|
||||||
|
$seedPortCount = $isCreate ? max(0, (int)($_POST['seed_ports'] ?? 0)) : 0;
|
||||||
$defaultPortTypeId = normalizeNullableInt($_POST['default_port_type_id'] ?? null);
|
$defaultPortTypeId = normalizeNullableInt($_POST['default_port_type_id'] ?? null);
|
||||||
$portRows = is_array($_POST['port_rows'] ?? null) ? $_POST['port_rows'] : [];
|
$portRows = is_array($_POST['port_rows'] ?? null) ? $_POST['port_rows'] : [];
|
||||||
$rawShapes = trim($_POST['shape_definition'] ?? '');
|
$rawShapes = trim($_POST['shape_definition'] ?? '');
|
||||||
@@ -131,7 +132,9 @@ if ($deviceTypeId > 0) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
seedDeviceTypePorts($sql, $deviceTypeId, $seedPortCount, $defaultPortTypeId);
|
if ($isCreate) {
|
||||||
|
seedDeviceTypePorts($sql, $deviceTypeId, $seedPortCount, $defaultPortTypeId);
|
||||||
|
}
|
||||||
syncDeviceTypePorts($sql, $deviceTypeId, $portRows);
|
syncDeviceTypePorts($sql, $deviceTypeId, $portRows);
|
||||||
|
|
||||||
$_SESSION['success'] = $deviceTypeId ? "Gerätetyp gespeichert" : "Fehler beim Speichern";
|
$_SESSION['success'] = $deviceTypeId ? "Gerätetyp gespeichert" : "Fehler beim Speichern";
|
||||||
|
|||||||
@@ -56,10 +56,10 @@ $dependencies = $sql->single(
|
|||||||
(
|
(
|
||||||
SELECT COUNT(*)
|
SELECT COUNT(*)
|
||||||
FROM connections c
|
FROM connections c
|
||||||
WHERE (c.port_a_type = 'device' AND c.port_a_id IN (
|
WHERE ((c.port_a_type = 'device' OR c.port_a_type = 'device_ports') AND c.port_a_id IN (
|
||||||
SELECT dp3.id FROM device_ports dp3 WHERE dp3.device_id = ?
|
SELECT dp3.id FROM device_ports dp3 WHERE dp3.device_id = ?
|
||||||
))
|
))
|
||||||
OR (c.port_b_type = 'device' AND c.port_b_id IN (
|
OR ((c.port_b_type = 'device' OR c.port_b_type = 'device_ports') AND c.port_b_id IN (
|
||||||
SELECT dp4.id FROM device_ports dp4 WHERE dp4.device_id = ?
|
SELECT dp4.id FROM device_ports dp4 WHERE dp4.device_id = ?
|
||||||
))
|
))
|
||||||
) AS connection_count",
|
) AS connection_count",
|
||||||
@@ -108,8 +108,8 @@ if ($hasDependencies && !$forceDelete) {
|
|||||||
// Connections referenzieren device_ports nur logisch, daher manuell entfernen.
|
// Connections referenzieren device_ports nur logisch, daher manuell entfernen.
|
||||||
$sql->set(
|
$sql->set(
|
||||||
"DELETE FROM connections
|
"DELETE FROM connections
|
||||||
WHERE (port_a_type = 'device' AND port_a_id IN (SELECT id FROM device_ports WHERE device_id = ?))
|
WHERE ((port_a_type = 'device' OR port_a_type = 'device_ports') AND port_a_id IN (SELECT id FROM device_ports WHERE device_id = ?))
|
||||||
OR (port_b_type = 'device' AND port_b_id IN (SELECT id FROM device_ports WHERE device_id = ?))",
|
OR ((port_b_type = 'device' OR port_b_type = 'device_ports') AND port_b_id IN (SELECT id FROM device_ports WHERE device_id = ?))",
|
||||||
"ii",
|
"ii",
|
||||||
[$deviceId, $deviceId]
|
[$deviceId, $deviceId]
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -47,10 +47,10 @@ if ($isEdit) {
|
|||||||
(
|
(
|
||||||
SELECT COUNT(*)
|
SELECT COUNT(*)
|
||||||
FROM connections c
|
FROM connections c
|
||||||
WHERE (c.port_a_type = 'device' AND c.port_a_id IN (
|
WHERE ((c.port_a_type = 'device' OR c.port_a_type = 'device_ports') AND c.port_a_id IN (
|
||||||
SELECT dp3.id FROM device_ports dp3 WHERE dp3.device_id = ?
|
SELECT dp3.id FROM device_ports dp3 WHERE dp3.device_id = ?
|
||||||
))
|
))
|
||||||
OR (c.port_b_type = 'device' AND c.port_b_id IN (
|
OR ((c.port_b_type = 'device' OR c.port_b_type = 'device_ports') AND c.port_b_id IN (
|
||||||
SELECT dp4.id FROM device_ports dp4 WHERE dp4.device_id = ?
|
SELECT dp4.id FROM device_ports dp4 WHERE dp4.device_id = ?
|
||||||
))
|
))
|
||||||
) AS connection_count",
|
) AS connection_count",
|
||||||
|
|||||||
@@ -97,10 +97,10 @@ $devices = $sql->get(
|
|||||||
(
|
(
|
||||||
SELECT COUNT(*)
|
SELECT COUNT(*)
|
||||||
FROM connections c
|
FROM connections c
|
||||||
WHERE (c.port_a_type = 'device' AND c.port_a_id IN (
|
WHERE ((c.port_a_type = 'device' OR c.port_a_type = 'device_ports') AND c.port_a_id IN (
|
||||||
SELECT dp3.id FROM device_ports dp3 WHERE dp3.device_id = d.id
|
SELECT dp3.id FROM device_ports dp3 WHERE dp3.device_id = d.id
|
||||||
))
|
))
|
||||||
OR (c.port_b_type = 'device' AND c.port_b_id IN (
|
OR ((c.port_b_type = 'device' OR c.port_b_type = 'device_ports') AND c.port_b_id IN (
|
||||||
SELECT dp4.id FROM device_ports dp4 WHERE dp4.device_id = d.id
|
SELECT dp4.id FROM device_ports dp4 WHERE dp4.device_id = d.id
|
||||||
))
|
))
|
||||||
) AS connection_count
|
) AS connection_count
|
||||||
|
|||||||
Reference in New Issue
Block a user