Quelltext des aktuellen Standes
/**
* Infrastruktur-Modul – Rack-Elevation: Verkabelungs-Overlay und Bedienung.
*
* Die Seite liefert ihre Daten als JSON-Insel `#inf-rack-data` aus; dieses
* Skript liest sie einmal nach `window.INFRA_RACK` und arbeitet damit. Alle
* aus dem Markup heraus aufgerufenen Funktionen hängen an `window`.
*/
(function () {
'use strict';
var el = document.getElementById('inf-rack-data');
if (!el) { return; }
try {
window.INFRA_RACK = JSON.parse(el.textContent);
} catch (e) {
window.INFRA_RACK = null;
}
})();
/**
* Verkabelungs-Overlay der Rack-Elevation.
*
* Zeichnet Verbindungen als 90°-geführte Leitungen im linken Kabelkanal, damit
* die Ports frei und anklickbar bleiben (Patchdocs-Muster). Rack-interne
* Verbindungen laufen als Schleife von Port zu Port; Verbindungen zu anderen
* Racks/Standorten oder externen Zielen enden an einer Austrittsmarke oben.
* Beschriftungen erscheinen erst bei Klick auf Port, Leitung oder Marke.
*/
(function () {
'use strict';
var data = window.INFRA_RACK;
if (!data || !Array.isArray(data.connections)) {
return;
}
var SVGNS = 'http://www.w3.org/2000/svg';
// Farbe je Kabeltyp kommt zentral aus CSS-Tokens (--inf-cc); hier nur die Klasse.
var CLASSES = {
copper: 'inf-cbl--copper', fiber: 'inf-cbl--fiber', dac: 'inf-cbl--dac',
wan: 'inf-cbl--wan', power: 'inf-cbl--power', other: 'inf-cbl--other'
};
function typeClass(t) { return CLASSES[t] || CLASSES.other; }
var TYPE_LABEL = {
copper: 'Kupfer', fiber: 'Fiber', dac: 'DAC', wan: 'WAN/VPN', power: 'Strom', other: 'Sonstige'
};
function slug(s) { return String(s || '').toLowerCase().replace(/[^a-z0-9]/g, ''); }
function stage() { return document.getElementById('inf-stage'); }
function svg() { return document.getElementById('inf-cables'); }
/** Aktueller Zoom der Bühne (transform: scale) – Koordinaten müssen darauf normiert werden. */
function zoomOf() { var st = stage(); return (st && parseFloat(st.dataset.zoom)) || 1; }
/** Port-Element eines Endpunkts, sonst dessen Geräte-Face als Fallback. */
function endpointEl(mountId, port) {
if (mountId == null) { return null; }
return document.getElementById('inf-pt-' + mountId + '-' + slug(port))
|| document.getElementById('inf-face-' + mountId);
}
/** Mittelpunkt/Kanten eines Elements relativ zur Bühne (auf Zoom normiert). */
function centerOf(el) {
var z = zoomOf();
var s = stage().getBoundingClientRect(), r = el.getBoundingClientRect();
return {
x: (r.left - s.left) / z + (r.width / z) / 2,
y: (r.top - s.top) / z + (r.height / z) / 2,
left: (r.left - s.left) / z,
right: (r.right - s.left) / z
};
}
/** Endpunkt inkl. gapY: die Kante des Geräte-Faces (dort liegen keine Ports). */
function endpt(mountId, port) {
var el = endpointEl(mountId, port);
if (!el) { return null; }
var z = zoomOf();
var p = centerOf(el);
var face = el.closest('.inf-face') || el;
var s = stage().getBoundingClientRect(), r = face.getBoundingClientRect();
var top = (r.top - s.top) / z, bot = (r.bottom - s.top) / z;
p.gapY = (p.y < (top + bot) / 2) ? top - 3 : bot + 3;
return p;
}
function laneBounds() {
var lane = document.querySelector('#inf-stage .inf-cable-lane');
if (!lane) { return { left: 14, right: 90 }; }
var z = zoomOf();
var s = stage().getBoundingClientRect(), r = lane.getBoundingClientRect();
return { left: (r.left - s.left) / z, right: (r.right - s.left) / z };
}
function dist(a, b) { return Math.hypot(a[0] - b[0], a[1] - b[1]); }
/** Polyline mit abgerundeten 90°-Ecken. */
function roundedPath(pts, radius) {
if (pts.length < 3) { return 'M ' + pts[0][0] + ' ' + pts[0][1] + ' L ' + pts[1][0] + ' ' + pts[1][1]; }
var d = 'M ' + pts[0][0] + ' ' + pts[0][1];
for (var i = 1; i < pts.length - 1; i++) {
var p0 = pts[i - 1], p1 = pts[i], p2 = pts[i + 1];
var rr = Math.min(radius, dist(p0, p1) / 2, dist(p1, p2) / 2);
var d1 = [p1[0] - p0[0], p1[1] - p0[1]], l1 = Math.hypot(d1[0], d1[1]) || 1;
var d2 = [p2[0] - p1[0], p2[1] - p1[1]], l2 = Math.hypot(d2[0], d2[1]) || 1;
var a = [p1[0] - d1[0] / l1 * rr, p1[1] - d1[1] / l1 * rr];
var b = [p1[0] + d2[0] / l2 * rr, p1[1] + d2[1] / l2 * rr];
d += ' L ' + a[0] + ' ' + a[1] + ' Q ' + p1[0] + ' ' + p1[1] + ' ' + b[0] + ' ' + b[1];
}
var last = pts[pts.length - 1];
return d + ' L ' + last[0] + ' ' + last[1];
}
// Highlight-Register: Verbindung → beteiligte Elemente (Leitung, Punkte,
// Ports, Austrittsmarke); Port-Element-ID → Verbindungs-IDs; ID → Verbindung.
var byConn = {}, byPort = {}, connById = {};
function portEl(mountId, port) {
return mountId != null ? document.getElementById('inf-pt-' + mountId + '-' + slug(port)) : null;
}
function register(connId, el) {
if (!el) { return; }
(byConn[connId] = byConn[connId] || []).push(el);
}
function highlight(connId, on) {
(byConn[connId] || []).forEach(function (el) {
if (el.classList) { el.classList.toggle('inf-hl', on); }
});
}
// von außen (Verkabelungsliste rechts) aufrufbar: Verbindung im Rack hervorheben
window.infHighlightConnection = function (connId, on) { highlight(connId, on); };
function stroke(d, klass, connId) {
var s = svg();
var hit = document.createElementNS(SVGNS, 'path'); // breite, unsichtbare Trefferfläche
hit.setAttribute('d', d); hit.setAttribute('fill', 'none');
hit.setAttribute('stroke', 'transparent'); hit.setAttribute('stroke-width', '14');
hit.style.pointerEvents = 'stroke'; hit.style.cursor = 'pointer';
s.appendChild(hit);
var vis = document.createElementNS(SVGNS, 'path');
vis.setAttribute('d', d); vis.setAttribute('fill', 'none');
vis.setAttribute('class', 'inf-cbl-vis ' + klass); // Strichfarbe via CSS-Token (--inf-cc)
vis.setAttribute('stroke-width', '2.6');
vis.setAttribute('stroke-linecap', 'round'); vis.setAttribute('opacity', '.92');
s.appendChild(vis);
register(connId, vis);
return hit;
}
function dot(x, y, klass, connId) {
var c = document.createElementNS(SVGNS, 'circle');
c.setAttribute('cx', x); c.setAttribute('cy', y); c.setAttribute('r', '3.2');
c.setAttribute('class', 'inf-cdot ' + klass);
svg().appendChild(c);
register(connId, c);
}
function esc(s) { return String(s == null ? '' : s).replace(/[&<>"]/g, function (m) { return { '&': '&', '<': '<', '>': '>', '"': '"' }[m]; }); }
function tipHtml(c) {
var type = TYPE_LABEL[c.cable_type] || c.cable_type;
var near = esc(c.near.dev) + (c.near.port ? ' · <span class="k">' + esc(c.near.port) + '</span>' : '');
var far;
if (c.far.external) {
far = '<b>' + esc(c.far.dev) + '</b>';
} else {
var loc = c.far.location ? '<b>📍 ' + esc(c.far.location) + '</b> · ' : '';
far = loc + esc(c.far.rack ? c.far.rack + ' · ' : '') + esc(c.far.dev) + (c.far.port ? ' · <span class="k">' + esc(c.far.port) + '</span>' : '');
}
var lbl = c.label ? '<br>' + esc(c.label) : '';
return near + ' ↔ ' + far + '<br><span class="k">' + esc(type) + '</span>' + lbl;
}
// Hover: Trio (Quellport, Zielport, Leitung) hervorheben + Tooltip; Klick: bearbeiten
function bindConnHover(el, connId, tipPos) {
el.addEventListener('mouseenter', function () { highlight(connId, true); showTip(tipHtml(connById[connId]), tipPos()); });
el.addEventListener('mouseleave', function () { highlight(connId, false); hideTip(); });
el.addEventListener('click', function (e) { e.stopPropagation(); if (window.infEditConnection) { window.infEditConnection(connId); } });
}
function draw() {
var st = stage(), s = svg();
if (!st || !s || !st.clientWidth) { return; }
s.setAttribute('viewBox', '0 0 ' + st.clientWidth + ' ' + st.clientHeight);
s.innerHTML = '';
Array.prototype.forEach.call(document.querySelectorAll('.inf-exit-mark'), function (m) { m.remove(); });
byConn = {};
var lane = laneBounds();
var within = [], exits = [];
// Marker-Verbindungen (AP über Patchpanel): das Patchkabel Patchpanel↔Switch
// wird normal gezeichnet; der Anschluss zum Gerät selbst (AP) wird separat
// grau/dezent als kurzer Stich an der Patchpanel-Seite dargestellt.
var markerLinks = [];
data.connections.forEach(function (c) {
if (c.from_marker) {
markerLinks.push(c);
if (c.far && !c.far.external) { (c.within ? within : exits).push(c); }
return;
}
(c.within ? within : exits).push(c);
});
within.forEach(function (c, i) {
var a = endpt(c.near.mount_id, c.near.port);
var b = endpt(c.far.mount_id, c.far.port);
if (!a || !b) { return; }
var laneX = lane.right - 12 - i * 14;
var klass = typeClass(c.cable_type);
var pts = [[a.x, a.y], [a.x, a.gapY], [laneX, a.gapY], [laneX, b.gapY], [b.x, b.gapY], [b.x, b.y]];
var hit = stroke(roundedPath(pts, 9), klass, c.id);
dot(a.x, a.y, klass, c.id); dot(b.x, b.y, klass, c.id);
register(c.id, portEl(c.near.mount_id, c.near.port));
register(c.id, portEl(c.far.mount_id, c.far.port));
bindConnHover(hit, c.id, function () { return { x: laneX, y: (a.gapY + b.gapY) / 2 }; });
});
exits.forEach(function (c, i) {
var a = endpt(c.near.mount_id, c.near.port);
if (!a) { return; }
var laneX = lane.left + 10 + i * 14, topY = 6;
var klass = typeClass(c.cable_type);
var pts = [[a.x, a.y], [a.x, a.gapY], [laneX, a.gapY], [laneX, topY]];
var hit = stroke(roundedPath(pts, 9), klass, c.id);
dot(a.x, a.y, klass, c.id);
register(c.id, portEl(c.near.mount_id, c.near.port));
var mk = document.createElement('div');
mk.className = 'inf-exit-mark ' + klass;
mk.style.left = laneX + 'px'; mk.style.top = topY + 'px';
mk.textContent = '↗';
st.appendChild(mk);
register(c.id, mk);
bindConnHover(mk, c.id, function () { return { x: laneX, y: topY + 12 }; });
});
// Geräte-Anschluss (AP): läuft wie ein externes Gerät vom Patchpanel-Port
// nach oben zu einer Austrittsmarke – aber grau und nicht anklickbar
// (Festverkabelung, nicht patchbar). Hover zeigt das Gerät.
markerLinks.forEach(function (c, i) {
var a = endpt(c.near.mount_id, c.near.port);
if (!a) { return; }
var laneX = lane.left + 10 + (exits.length + i) * 14, topY = 6;
var pts = [[a.x, a.y], [a.x, a.gapY], [laneX, a.gapY], [laneX, topY]];
var d = roundedPath(pts, 9);
var apName = c.label || (c.far && c.far.dev) || 'Gerät';
var tip = '<b>' + esc(apName) + '</b><br><span class="k">Anschluss am Patchpanel</span>';
var vis = document.createElementNS(SVGNS, 'path');
vis.setAttribute('d', d); vis.setAttribute('fill', 'none');
vis.setAttribute('class', 'inf-cbl-vis inf-cbl--muted');
vis.setAttribute('stroke-width', '2.2'); vis.setAttribute('stroke-linecap', 'round');
vis.setAttribute('opacity', '.5');
svg().appendChild(vis);
var dotc = document.createElementNS(SVGNS, 'circle');
dotc.setAttribute('cx', a.x); dotc.setAttribute('cy', a.y); dotc.setAttribute('r', '3.2');
dotc.setAttribute('class', 'inf-cdot inf-cbl--muted'); dotc.setAttribute('opacity', '.5');
svg().appendChild(dotc);
var hit = document.createElementNS(SVGNS, 'path');
hit.setAttribute('d', d); hit.setAttribute('fill', 'none');
hit.setAttribute('stroke', 'transparent'); hit.setAttribute('stroke-width', '12');
hit.style.pointerEvents = 'stroke';
svg().appendChild(hit);
hit.addEventListener('mouseenter', function () { showTip(tip, { x: laneX, y: (a.gapY + topY) / 2 }); });
hit.addEventListener('mouseleave', hideTip);
var mk = document.createElement('div');
mk.className = 'inf-exit-mark inf-cbl--muted';
mk.style.left = laneX + 'px'; mk.style.top = topY + 'px'; mk.style.opacity = '.6';
mk.textContent = '↗';
st.appendChild(mk);
mk.addEventListener('mouseenter', function () { showTip(tip, { x: laneX, y: topY + 12 }); });
mk.addEventListener('mouseleave', hideTip);
});
}
// Ports einmalig verdrahten (statisches DOM). Belegt vs. frei wird zur
// Klickzeit entschieden (byPort ist erst nach draw() gefüllt).
function wirePorts() {
data.connections.forEach(function (c) {
connById[c.id] = c;
var kn = 'inf-pt-' + c.near.mount_id + '-' + slug(c.near.port);
(byPort[kn] = byPort[kn] || []).push(c.id);
if (c.within && c.far.mount_id != null) {
var kf = 'inf-pt-' + c.far.mount_id + '-' + slug(c.far.port);
(byPort[kf] = byPort[kf] || []).push(c.id);
}
});
Array.prototype.forEach.call(document.querySelectorAll('.inf-devcol .inf-pt[id]'), function (el) {
el.style.cursor = 'pointer';
el.addEventListener('mouseenter', function () {
var cids = byPort[el.id];
if (!cids || !cids.length) { return; }
cids.forEach(function (cid) { highlight(cid, true); });
showTip(cids.map(function (cid) { return tipHtml(connById[cid]); }).join('<hr class="inf-tip-sep">'), centerOf(el));
});
el.addEventListener('mouseleave', function () {
var cids = byPort[el.id];
if (cids) { cids.forEach(function (cid) { highlight(cid, false); }); }
hideTip();
});
el.addEventListener('click', function (e) {
e.stopPropagation();
var cids = byPort[el.id];
if (cids && cids.length) {
if (window.infEditConnection) { window.infEditConnection(cids[0]); }
} else if (window.infAddConnectionAt) {
window.infAddConnectionAt(el.getAttribute('data-mount'), el.getAttribute('data-port'));
}
});
});
}
function showTip(html, pos) {
var t = document.getElementById('inf-tip'), s = stage().getBoundingClientRect(), z = zoomOf();
if (!t) { return; }
// position:fixed → Viewport-Koordinaten; Bühnen-lokale Koordinaten mit Zoom skalieren
t.innerHTML = html; t.style.display = 'block';
t.style.left = (s.left + pos.x * z) + 'px';
t.style.top = (s.top + pos.y * z) + 'px';
}
function hideTip() { var t = document.getElementById('inf-tip'); if (t) { t.style.display = 'none'; } }
// Von außen (Zoom-Steuerung) aufrufbar: Kabel neu zeichnen
window.infRedrawCables = function () { draw(); };
function init() {
wirePorts();
requestAnimationFrame(function () { requestAnimationFrame(draw); });
window.addEventListener('resize', function () { hideTip(); draw(); });
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();
/**
* Ansicht der Rack-Elevation: helle/dunkle Darstellung und Zoom.
*
* Beide Einstellungen gelten pro Browser (localStorage). Der Zoom passt sich
* beim Öffnen an die verfügbare Breite an, bis der Nutzer selbst zoomt.
*/
(function () {
'use strict';
var stage = document.getElementById('inf-stage');
if (!stage) { return; }
window.infToggleRackTheme = function () {
var light = stage.classList.toggle('inf-light');
try { localStorage.setItem('inf-rack-theme', light ? 'light' : 'dark'); } catch (e) {}
};
var KEY = 'inf-rack-zoom';
var MIN = 0.3, MAX = 2, STEP = 0.15;
// Ziel-Zoom beim Öffnen auf breiten Schirmen: kompakte 65 %, damit das Rack
// mit Umgebung sichtbar ist. Schmale Schirme verkleinern bei Bedarf weiter.
var FIT = 0.65;
var curZoom = 1;
var userSet = false; // hat der Nutzer manuell gezoomt?
function els() {
var sz = document.getElementById('inf-sizer');
return sz ? { st: stage, sz: sz } : null;
}
function clamp(z) { return Math.max(MIN, Math.min(MAX, z)); }
// Skalierung, bei der das (unskalierte) Rack in seinen Scroll-Container passt.
// Auf breiten Schirmen wird höchstens auf FIT (65 %) vergrößert; passt das Rack
// dort nicht in die Breite, wird weiter verkleinert (schmale Schirme/Handy).
function fitZoom(e) {
var scroll = e.st.closest('.inf-rack-scroll');
var avail = scroll ? scroll.clientWidth : (e.st.parentElement ? e.st.parentElement.clientWidth : 0);
var natural = e.st.offsetWidth; // transform-unabhängig
if (!avail || !natural) { return FIT; }
return Math.max(MIN, Math.min(FIT, avail / natural));
}
function render(e) {
var z = curZoom;
e.st.dataset.zoom = z;
e.st.style.transform = z === 1 ? '' : 'scale(' + z + ')';
// offsetWidth/Height sind unskaliert → skalierten Platz reservieren
e.sz.style.width = (e.st.offsetWidth * z) + 'px';
e.sz.style.height = (e.st.offsetHeight * z) + 'px';
var lvl = document.getElementById('inf-zoom-lvl');
if (lvl) { lvl.textContent = Math.round(z * 100) + '%'; }
if (window.infRedrawCables) { window.infRedrawCables(); }
}
window.infZoom = function (dir) {
var e = els(); if (!e) { return; }
if (dir === 0) {
// Zurücksetzen = wieder an den Bildschirm anpassen
userSet = false;
try { localStorage.removeItem(KEY); } catch (err) {}
curZoom = fitZoom(e);
} else {
userSet = true;
curZoom = clamp(curZoom + dir * STEP);
try { localStorage.setItem(KEY, curZoom); } catch (err) {}
}
render(e);
};
function boot() {
var e = els(); if (!e) { return; }
var stored = NaN;
try { stored = parseFloat(localStorage.getItem(KEY)); } catch (err) {}
if (!isNaN(stored) && stored > 0) { userSet = true; curZoom = clamp(stored); }
else { userSet = false; curZoom = fitZoom(e); }
render(e);
}
// Bei Größen-/Ausrichtungsänderung neu einpassen – solange nicht manuell gezoomt.
var rt;
window.addEventListener('resize', function () {
if (userSet) { return; }
clearTimeout(rt);
rt = setTimeout(function () { var e = els(); if (e) { curZoom = fitZoom(e); render(e); } }, 150);
});
setTimeout(boot, 80);
})();
/**
* Dialoge der Rack-Seite: Einbauen, Bearbeiten, Verbinden, Ports beschriften.
*
* Welche Felder ein Dialog zeigt, hängt am Gerätetyp: nur Typen mit Ports
* bekommen Portzahlen, nur netzwerkfähige zusätzlich SFP/QSFP, und die
* Rückseiten-Felder erscheinen erst bei voller Einbautiefe.
*/
(function () {
'use strict';
var D = window.INFRA_RACK;
if (!D || !D.aPorts) { return; }
var A_PORTS = D.aPorts, B_PORTS = D.bPorts, CONNS = D.conns;
var PORT_LABELS = D.portLabels, MOUNTS = D.mounts;
function byId(id) { return document.getElementById(id); }
function show(id, on) { var e = byId(id); if (e) { e.style.display = on ? '' : 'none'; } }
/**
* Fuellt die Auswahllisten eines Dialogs aus den Seitendaten.
*
* Die langen Listen (Dokuseiten, Inventar, UniFi) stehen einmal als Daten
* im Seitenkopf statt als Markup in jedem Dialog. Gefuellt wird beim ersten
* Oeffnen: Ein ungeoeffneter Dialog kostet damit weder Quelltext noch
* Auswahlfelder im Dokument. Ein bereits gesetzter Wert bleibt erhalten.
*
* @param {ParentNode} root Dialog oder ein anderer Bereich mit Auswahlfeldern.
*/
function fillSelects(root) {
var lists = D.options || {};
Array.prototype.forEach.call(root.querySelectorAll('select[data-inf-fill]'), function (sel) {
if (sel.dataset.infFilled) { return; }
var rows = lists[sel.dataset.infFill];
if (!rows) { return; }
var vorhanden = {};
Array.prototype.forEach.call(sel.options, function (o) { vorhanden[o.value] = true; });
var frag = document.createDocumentFragment();
rows.forEach(function (row) {
if (vorhanden[String(row[0])]) { return; }
var o = document.createElement('option');
o.value = row[0];
o.textContent = row[1];
if (row[2]) { o.setAttribute('data-model', row[2]); }
frag.appendChild(o);
});
sel.appendChild(frag);
sel.dataset.infFilled = '1';
// Das durchsuchbare Dropdown baut seine Liste erst beim Aufklappen aus
// dem Select; hier genuegt der Abgleich der Beschriftung.
if (window.ssSync) { window.ssSync(sel); }
});
}
/** Oeffnet einen Dialog und stellt sicher, dass seine Auswahllisten gefuellt sind. */
function openModal(id) {
var m = byId(id);
if (!m) { return null; }
fillSelects(m);
m.style.display = 'flex';
return m;
}
window.infFillSelects = fillSelects;
window.infOpenModal = openModal;
window.infSource = function (v) {
show('inf-f-asset', v === 'asset');
show('inf-f-unifi', v === 'unifi');
show('inf-f-label', v === 'passive');
};
/** Beschriftung der Portzahl – ein Server hat NICs, eine PDU Ausgänge. */
function countLabel(v) {
return v === 'server' ? 'NICs'
: ((v === 'pdu' || v === 'ups') ? window.onitText('Ausgänge')
: (v === 'pbx' ? window.onitText('Anschlüsse') : window.onitText('Ports')));
}
function hasCount(v) { return ['switch', 'firewall', 'patchpanel', 'gateway', 'pdu', 'ups', 'server', 'pbx'].indexOf(v) >= 0; }
function hasUplink(v) { return ['switch', 'firewall', 'gateway', 'server'].indexOf(v) >= 0; }
window.infCountLabel = countLabel;
window.infHasCount = hasCount;
window.infHasUplink = hasUplink;
/** Sichtbarkeit der Portfelder eines Dialogs; `pfx` ist 'add' oder 'edit'. */
function portKind(pfx, v) {
var cnt = hasCount(v), up = hasUplink(v);
var fd = (byId('inf-' + pfx + '-fulldepth') || {}).checked;
show('inf-' + pfx + '-portcounts', cnt);
show('inf-' + pfx + '-sfp-wrap', up);
show('inf-' + pfx + '-qsfp-wrap', up);
show('inf-' + pfx + '-layout-wrap', v === 'switch');
show('inf-' + pfx + '-cap-wrap', v === 'ups' || v === 'pdu');
show('inf-' + pfx + '-rear-wrap', cnt && fd);
show('inf-' + pfx + '-rsfp-wrap', up);
show('inf-' + pfx + '-rqsfp-wrap', up);
var l = byId('inf-' + pfx + '-pc-label'); if (l) { l.textContent = countLabel(v); }
var rl = byId('inf-' + pfx + '-rpc-label'); if (rl) { rl.textContent = countLabel(v) + ' hinten'; }
}
window.infPortKind = function (v) { portKind('add', v); };
window.infEditPortKind = function (v) { portKind('edit', v); };
window.infAddDepth = function () { var k = byId('inf-portkind'); portKind('add', k ? k.value : 'none'); };
window.infEditDepth = function () { var k = document.querySelector('#inf-edit-form [name=port_kind]'); portKind('edit', k ? k.value : 'none'); };
// Gerätefarbe: Farb-Input nur aktiv (und damit absendbar), wenn die Checkbox gesetzt ist.
window.infToggleColor = function (mode) {
var cb = byId('inf-' + mode + '-usecolor');
var ci = byId('inf-' + mode + '-color');
if (cb && ci) { ci.disabled = !cb.checked; }
};
// Kabelfarbe nur senden, wenn sie bewusst gesetzt wurde – ein deaktiviertes
// Feld überträgt nichts, die Verbindung bleibt damit ohne Farbe.
window.infToggleCableColor = function () {
var cb = byId('inf-conn-usecolor');
var inp = byId('inf-conn-color');
if (cb && inp) { inp.disabled = !cb.checked; }
};
window.infBKind = function (v) {
show('inf-b-mount-wrap', v !== 'external');
show('inf-b-ext-wrap', v === 'external');
};
// nach programmatischem Setzen von <select>-Werten die durchsuchbaren Wrapper aktualisieren
function syncSelects(root) {
if (!window.ssSync) { return; }
Array.prototype.forEach.call(root.querySelectorAll('select'), function (s) { window.ssSync(s); });
}
window.infSyncSelects = syncSelects;
window.infOpenAdd = function () {
var cb = byId('inf-add-usecolor'); if (cb) { cb.checked = false; }
window.infToggleColor('add');
openModal('inf-mount-modal');
};
window.infAddAt = function (u, slot) {
byId('inf-add-start').value = u;
var side = byId('inf-add-side');
side.value = D.side;
if (window.ssSync) { window.ssSync(side); }
var sl = byId('inf-add-slot');
if (sl) { sl.value = slot || 'full'; if (window.ssSync) { window.ssSync(sl); } }
openModal('inf-mount-modal');
};
/**
* Fuellt den Bearbeiten-Dialog mit dem Stand eines eingebauten Geraets.
*
* Der Stand kommt aus den Seitendaten – dieselbe Quelle, aus der auch das
* Verschieben per Ziehen liest. So beschreibt genau eine Stelle das Geraet.
*
* @param {number} mountId Kennung des Einbaus.
*/
window.infEditMount = function (mountId) {
var m = MOUNTS[mountId]; if (!m) { return; }
var f = byId('inf-edit-form');
f.action = '/infrastructure/mounts/' + mountId + '/update';
f.elements['label'].value = m.label != null ? m.label : '';
f.elements['start_u'].value = m.start_u;
f.elements['height_u'].value = m.height_u;
f.elements['side'].value = m.side;
f.elements['port_kind'].value = m.port_kind;
f.elements['port_count'].value = m.port_count;
f.elements['sfp_count'].value = m.sfp_count;
f.elements['qsfp_count'].value = m.qsfp_count != null ? m.qsfp_count : 0;
if (f.elements['port_layout']) { f.elements['port_layout'].value = m.port_layout || 'zebra'; }
f.elements['power_watt'].value = m.power_watt != null ? m.power_watt : '';
f.elements['weight_kg'].value = m.weight_kg != null ? m.weight_kg : '';
if (f.elements['capacity_watt']) { f.elements['capacity_watt'].value = m.capacity_watt != null ? m.capacity_watt : ''; }
if (f.elements['slot']) { f.elements['slot'].value = m.slot || 'full'; }
var ecb = byId('inf-edit-usecolor'), eci = byId('inf-edit-color');
if (ecb && eci) {
if (m.color) { ecb.checked = true; eci.value = m.color; } else { ecb.checked = false; }
window.infToggleColor('edit');
}
if (f.elements['full_depth']) { f.elements['full_depth'].checked = !!(m.full_depth === 1 || m.full_depth === true); }
if (f.elements['port_count_rear']) { f.elements['port_count_rear'].value = m.port_count_rear != null ? m.port_count_rear : 0; }
if (f.elements['sfp_count_rear']) { f.elements['sfp_count_rear'].value = m.sfp_count_rear != null ? m.sfp_count_rear : 0; }
if (f.elements['qsfp_count_rear']) { f.elements['qsfp_count_rear'].value = m.qsfp_count_rear != null ? m.qsfp_count_rear : 0; }
portKind('edit', m.port_kind);
// Die Auswahllisten stehen erst nach dem Fuellen zur Verfuegung; der
// hinterlegte Dokuverweis wird danach gesetzt und bleibt so erhalten.
fillSelects(byId('inf-edit-modal'));
if (f.elements['doc_page_id']) { f.elements['doc_page_id'].value = m.doc_page_id != null ? m.doc_page_id : ''; }
syncSelects(f);
openModal('inf-edit-modal');
};
// Bekannte UniFi-Modelle → Port-Profil (Autofill beim Einbauen); spezifische zuerst
var UNIFI_PORTS = [
{ re: /EnterpriseCampus|Campus/i, kind: 'switch', pc: 0, sfp: 48 },
{ re: /Pro.?Aggregation/i, kind: 'switch', pc: 0, sfp: 28 },
{ re: /Aggregation/i, kind: 'switch', pc: 0, sfp: 8 },
{ re: /(Pro.?)?48|48.?PoE/i, kind: 'switch', pc: 48, sfp: 4 },
{ re: /(Pro.?)?24|24.?PoE/i, kind: 'switch', pc: 24, sfp: 2 },
{ re: /16/i, kind: 'switch', pc: 16, sfp: 2 },
{ re: /Flex|Lite.?8|USW.?8|Ultra/i, kind: 'switch', pc: 8, sfp: 0 },
{ re: /UDM.?(Pro|SE)|UXG/i, kind: 'gateway', pc: 8, sfp: 2 },
{ re: /UDM|USG/i, kind: 'gateway', pc: 4, sfp: 0 }
];
window.infUnifiAutofill = function (sel) {
var opt = sel.options[sel.selectedIndex];
var model = opt ? (opt.getAttribute('data-model') || '') : '';
if (!model) { return; }
for (var i = 0; i < UNIFI_PORTS.length; i++) {
if (UNIFI_PORTS[i].re.test(model)) {
var pk = byId('inf-portkind');
pk.value = UNIFI_PORTS[i].kind;
portKind('add', pk.value);
if (window.ssSync) { window.ssSync(pk); }
byId('inf-add-pc').value = UNIFI_PORTS[i].pc;
byId('inf-add-sfp').value = UNIFI_PORTS[i].sfp;
break;
}
}
};
// Port-Vorschläge (datalist) je Gerätetyp – Rückseiten-Ports mit Präfix „R"
function portsFor(kind, pc, sfp, qsfp, pfx) {
var out = []; var i, s, x;
pfx = pfx || ''; pc = pc || 0; sfp = sfp || 0; qsfp = qsfp || 0;
if (kind === 'switch' || kind === 'firewall') {
for (i = 1; i <= pc; i++) { out.push(pfx + i); }
for (s = 1; s <= sfp; s++) { out.push(pfx + 'SFP' + s); }
for (x = 1; x <= qsfp; x++) { out.push(pfx + 'QSFP' + x); }
} else if (kind === 'patchpanel' || kind === 'pbx') {
for (i = 1; i <= pc; i++) { out.push(pfx + i); }
} else if (kind === 'gateway') {
out.push(pfx + 'WAN1');
for (i = 1; i <= Math.max(1, pc); i++) { out.push(pfx + 'LAN' + i); }
for (s = 1; s <= sfp; s++) { out.push(pfx + 'SFP' + s); }
for (x = 1; x <= qsfp; x++) { out.push(pfx + 'QSFP' + x); }
} else if (kind === 'server') {
for (i = 1; i <= pc; i++) { out.push(pfx + 'NIC' + i); }
for (s = 1; s <= sfp; s++) { out.push(pfx + 'SFP' + s); }
for (x = 1; x <= qsfp; x++) { out.push(pfx + 'QSFP' + x); }
} else if (kind === 'ups') {
var un = pc > 0 ? pc : 8;
for (i = 1; i <= un; i++) { out.push(pfx + 'OUT' + i); }
}
return out;
}
function suggestPorts(p) {
if (!p) { return []; }
var out = portsFor(p.kind, p.pc, p.sfp, p.qsfp || 0, '');
if ((p.pcR || 0) + (p.sfpR || 0) + (p.qsfpR || 0) > 0) {
out = out.concat(portsFor(p.kind, p.pcR, p.sfpR, p.qsfpR, 'R'));
}
return out;
}
function fillDatalist(dlId, profile) {
var dl = byId(dlId); if (!dl) { return; }
dl.innerHTML = '';
suggestPorts(profile).forEach(function (v) {
var o = document.createElement('option'); o.value = v; dl.appendChild(o);
});
}
window.infPortsFor = portsFor;
window.infSuggestPorts = suggestPorts;
window.infFillDatalist = fillDatalist;
window.infBPorts = function (id) { fillDatalist('inf-b-portlist', B_PORTS[id]); };
// Ports beschriften: Dialog mit einem Eingabefeld je Port des Geräts
window.infLabelPorts = function (mountId) {
var p = A_PORTS[mountId]; if (!p) { return; }
var refs = suggestPorts(p);
var labels = PORT_LABELS[mountId] || {};
var body = byId('inf-portlabel-body');
body.innerHTML = '';
refs.forEach(function (ref) {
var row = document.createElement('div'); row.className = 'form-group inf-pl-row';
var lab = document.createElement('label'); lab.textContent = 'Port ' + ref;
var inp = document.createElement('input');
inp.type = 'text'; inp.className = 'form-control form-control-sm';
inp.name = 'label[' + ref + ']'; inp.value = labels[ref] || '';
inp.placeholder = window.onitText('z. B. Büro 1.02');
row.appendChild(lab); row.appendChild(inp); body.appendChild(row);
});
byId('inf-portlabel-form').action = '/infrastructure/mounts/' + mountId + '/port-labels';
openModal('inf-portlabel-modal');
};
// Verbindungs-Dialog: Anlegen / Bearbeiten (ein wiederverwendetes Modal)
function openConnAdd() {
var f = byId('inf-conn-form');
f.reset();
f.action = D.connAddAction;
byId('inf-conn-title').textContent = 'Verbindung anlegen';
window.infBKind('mount');
fillDatalist('inf-a-portlist', null);
fillDatalist('inf-b-portlist', null);
syncSelects(f);
openModal('inf-conn-modal');
}
window.infOpenConnAdd = openConnAdd;
// von der Elevation: Klick auf freien Port → neue Verbindung mit vorbelegtem Endpunkt A
window.infAddConnectionAt = function (mountId, portRef) {
openConnAdd();
var f = byId('inf-conn-form');
if (mountId != null && mountId !== '') { f.elements['a_mount_id'].value = mountId; }
f.elements['a_port'].value = portRef || '';
fillDatalist('inf-a-portlist', A_PORTS[mountId]);
syncSelects(f);
};
// aus der Liste oder per Klick auf belegten Port/Leitung → Verbindung bearbeiten
window.infEditConnection = function (id) {
var c = CONNS[id]; if (!c) { return; }
var f = byId('inf-conn-form');
f.reset();
f.action = '/infrastructure/connections/' + id + '/update';
byId('inf-conn-title').textContent = 'Verbindung bearbeiten';
f.elements['a_mount_id'].value = c.a_mount_id != null ? c.a_mount_id : '';
f.elements['a_port'].value = c.a_port || '';
f.elements['b_kind'].value = c.b_kind;
window.infBKind(c.b_kind);
f.elements['b_mount_id'].value = c.b_mount_id != null ? c.b_mount_id : '';
f.elements['b_external'].value = c.b_external || '';
f.elements['b_port'].value = c.b_port || '';
f.elements['cable_type'].value = c.cable_type;
f.elements['label'].value = c.label || '';
fillDatalist('inf-a-portlist', A_PORTS[c.a_mount_id]);
fillDatalist('inf-b-portlist', B_PORTS[c.b_mount_id]);
syncSelects(f);
openModal('inf-conn-modal');
};
var aSel = byId('inf-a-mount');
if (aSel) {
aSel.addEventListener('change', function () { fillDatalist('inf-a-portlist', A_PORTS[this.value]); });
}
// Drag&Drop: Gerät im Rack auf eine andere HE verschieben (Server validiert Überlappung/Höhe)
var devcol = byId('inf-devcol');
if (devcol) {
var dragId = null;
Array.prototype.forEach.call(document.querySelectorAll('.inf-face.inf-draggable'), function (f) {
f.addEventListener('dragstart', function (e) {
dragId = f.getAttribute('data-mount-id');
e.dataTransfer.setData('text/plain', dragId);
e.dataTransfer.effectAllowed = 'move';
f.classList.add('inf-dragging');
});
f.addEventListener('dragend', function () {
f.classList.remove('inf-dragging');
devcol.classList.remove('inf-droptarget');
});
});
devcol.addEventListener('dragover', function (e) {
if (dragId !== null) { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; devcol.classList.add('inf-droptarget'); }
});
devcol.addEventListener('dragleave', function (e) {
if (e.target === devcol) { devcol.classList.remove('inf-droptarget'); }
});
devcol.addEventListener('drop', function (e) {
e.preventDefault(); devcol.classList.remove('inf-droptarget');
var id = e.dataTransfer.getData('text/plain') || dragId; if (!id) { return; }
var m = MOUNTS[id]; if (!m) { return; }
var uEl = document.querySelector('.inf-uscale .inf-u');
var uPx = uEl ? uEl.getBoundingClientRect().height : 34;
var rect = devcol.getBoundingClientRect();
var idx = Math.floor((e.clientY - rect.top) / uPx);
if (idx < 0) { idx = 0; }
if (idx >= D.rowUs.length) { idx = D.rowUs.length - 1; }
var physTopU = D.rowUs[idx];
var newStart = (D.numbering === 'top_down') ? physTopU : (physTopU - m.height_u + 1);
if (newStart < 1) { newStart = 1; }
if (newStart === m.start_u) { return; }
var f = document.createElement('form');
f.method = 'POST';
f.action = '/infrastructure/mounts/' + id + '/update';
function add(n, v) {
var i = document.createElement('input');
i.type = 'hidden'; i.name = n; i.value = (v == null ? '' : v);
f.appendChild(i);
}
add('_csrf', D.csrf); add('start_u', newStart); add('height_u', m.height_u); add('side', m.side);
add('slot', m.slot); add('full_depth', m.full_depth ? 1 : '');
add('label', m.label); add('port_kind', m.port_kind); add('port_count', m.port_count); add('sfp_count', m.sfp_count);
add('qsfp_count', m.qsfp_count); add('port_layout', m.port_layout);
add('port_count_rear', m.port_count_rear); add('sfp_count_rear', m.sfp_count_rear); add('qsfp_count_rear', m.qsfp_count_rear);
add('power_watt', m.power_watt); add('weight_kg', m.weight_kg);
add('capacity_watt', m.capacity_watt);
add('color', m.color);
document.body.appendChild(f); f.submit();
});
}
// Escape schließt offene Dialoge
document.addEventListener('keydown', function (e) {
if (e.key === 'Escape') {
document.querySelectorAll('.modal-overlay').forEach(function (m) { m.style.display = 'none'; });
}
});
})();