'use strict'; /* global FACEPLATES, renderFaceplate */ // Rack page: draws the rack front from /api/rack. Switches use their real faceplates, // patch panels a generic panel; hovering a port lights up every port on the same cable run. const esc = s => lt.escHtml(s == null ? '' : String(s)); const RATIO = 10.5; // a 19" rack's width to one unit's height const KIND_TEXT = { patch_panel: 'Patch panel', wall_jack: 'Wall jack', device: 'Device', shelf: 'Shelf', other: 'Other' }; let _rack = null; function speedText(mbps) { if (!mbps) return ''; return mbps >= 1000 ? `${mbps / 1000}G` : `${mbps}M`; } function portMapOf(ports) { const map = {}; Object.entries(ports || {}).forEach(([name, p]) => { if (p.port_idx != null) map[p.port_idx] = Object.assign({ name }, p); }); return map; } function portTitle(port, idx) { const parts = [port && port.name && port.name !== `Port ${idx}` ? `Port ${idx} "${port.name}"` : `Port ${idx}`]; if (port) { parts.push(port.up ? (speedText(port.speed_mbps) || 'up') : 'down'); const nb = port.lldp && (port.lldp.system_name || port.lldp.chassis_id); if (nb) parts.push(`→ ${nb}`); } return esc(parts.join(' · ')); } function isRackFaceplate(model) { const spec = FACEPLATES[model]; return !!spec && !/fp-desk/.test(spec.frameClass || ''); } // ── Item drawings ─────────────────────────────────────────────────────── function switchHtml(item) { const sw = item.switch; return renderFaceplate(sw.model, { swName: item.name, portMap: portMapOf(sw.ports), outlets: sw.outlets, online: sw.online, titleFor: (port, idx) => portTitle(port, idx), cutsheetFor: idx => { const m = item.ports[String(idx)]; return m ? { mark: m.mark, text: m.text } : null; }, }); } function panelSvg(item) { const count = Math.max(1, item.port_count || 24); const units = item.rack_height || 1; const rows = Math.max(1, Math.min(units * 2, Math.ceil(count / 24))); const perRow = Math.ceil(count / rows); const W = 1100, H = 105 * units, ear = 70; const pitch = Math.min(38, (W - ear * 2 - 40) / perRow); const left = (W - pitch * perRow) / 2; const rowH = (H - 20) / rows; // Label characters that fit under one port at the label's font size (9px, ~5.6px a character) const fit = Math.max(3, Math.floor((pitch - 2) / 5.6)); const short = t => (t.length > fit ? `${t.slice(0, fit - 1)}…` : t); const ports = []; for (let n = 1; n <= count; n++) { const r = Math.floor((n - 1) / perRow), c = (n - 1) % perRow; const x = left + c * pitch + pitch / 2, y = 10 + r * rowH + rowH / 2; const m = item.ports[String(n)]; const label = item.labels[String(n)] || ''; const title = [`Port ${n}`, label && `"${label}"`, m ? m.text : 'not on the cutsheet'].filter(Boolean).join(' · '); const attrs = m ? ` data-run="${m.run.join(' ')}" data-cable="${m.cable_id}" tabindex="0" role="link"` : ''; ports.push(` ${esc(title)} ${n} ${label ? `${esc(short(label))}` : ''} `); } return ` ${esc(item.name)} ${ports.join('')} `; } function plateHtml(item) { const sub = [KIND_TEXT[item.kind] || item.kind, item.location].filter(Boolean).join(' · '); return `
${esc(item.name)} ${esc(sub)}
`; } function itemHtml(item) { let body; if (item.switch && isRackFaceplate(item.switch.model)) body = switchHtml(item); else if (item.kind === 'patch_panel') body = panelSvg(item); else body = plateHtml(item); return `
${body}
`; } function uLabels(top, bottom) { const spans = []; for (let u = top; u >= bottom; u--) spans.push(`${u}`); return ``; } function row(top, bottom, inner, cls) { const n = top - bottom + 1; return `
${uLabels(top, bottom)}
${inner}
`; } // ── Page ──────────────────────────────────────────────────────────────── function render(data) { _rack = data; const byTop = Object.fromEntries(data.items.map(i => [i.rack_top, i])); const rows = []; let u = data.units; while (u >= 1) { const item = byTop[u]; if (item) { rows.push(row(u, item.rack_u, itemHtml(item), 'rack-row-item')); u = item.rack_u - 1; continue; } let low = u; while (low - 1 >= 1 && !byTop[low - 1]) low--; rows.push(row(u, low, '', 'rack-row-empty')); u = low - 1; } document.getElementById('rack').innerHTML = rows.join(''); document.getElementById('rack-count').textContent = data.items.length; // Faceplate ports carry their cable run too, for hover and click document.querySelectorAll('.rack-item .fp-port').forEach(g => { const item = data.items.find(i => i.name === g.dataset.switch); const m = item && item.ports[g.dataset.portIdx]; if (m) { g.dataset.run = m.run.join(' '); g.dataset.cable = m.cable_id; } }); const notes = []; data.overlaps.forEach(o => notes.push(`${esc(o.name)} (${esc(o.units)}) overlaps ${esc(o.with.join(', '))} and is not drawn.`)); data.outside.forEach(o => notes.push(`${esc(o.name)} (${esc(o.units)}) is above the top of a ${data.units}U rack and is not drawn.`)); const warn = document.getElementById('rack-warnings'); warn.hidden = !notes.length; warn.innerHTML = notes.length ? `

Fix the rack unit on the cutsheet.

` : ''; const unplaced = document.getElementById('rack-unplaced'); unplaced.innerHTML = data.unplaced.length ? `Not in the drawing (no rack unit set): ${data.unplaced.map(n => `${esc(n)}`).join(' ')}` : ''; if (!data.items.length) { document.getElementById('rack').insertAdjacentHTML('afterbegin', `
Nothing placed yet
Give panels, shelves and devices a rack unit on the cutsheet and they appear here.
`); } } function readout(el) { const out = document.getElementById('rack-readout'); if (!el) { out.textContent = 'Hover a port to follow its cable.'; return; } const title = el.querySelector('title'); out.textContent = title ? title.textContent : ''; } function highlight(el) { const ids = el && el.dataset.run ? el.dataset.run.split(' ') : []; document.querySelectorAll('#rack [data-run]').forEach(x => { x.classList.toggle('rack-hl', ids.length > 0 && x.dataset.run.split(' ').some(i => ids.includes(i))); }); } function portAt(target) { return target.closest && target.closest('#rack .fp-port, #rack .rack-pp-port'); } function onEnter(e) { const el = portAt(e.target); if (!el) return; highlight(el); readout(el); } function onLeave(e) { const el = portAt(e.target); if (el && !el.contains(e.relatedTarget)) { highlight(null); readout(null); } } function openPort(el) { if (el.classList.contains('fp-port')) { location.href = `/device/${encodeURIComponent(el.dataset.switch)}#port-${encodeURIComponent(el.dataset.portIdx)}`; } else if (el.dataset.cable) { location.href = `/cutsheet#cable-${encodeURIComponent(el.dataset.cable)}`; } } const rackEl = document.getElementById('rack'); rackEl.addEventListener('mouseover', onEnter); rackEl.addEventListener('mouseout', onLeave); rackEl.addEventListener('focusin', onEnter); rackEl.addEventListener('focusout', onLeave); rackEl.addEventListener('click', e => { const el = portAt(e.target); if (el) openPort(el); }); rackEl.addEventListener('keydown', e => { const el = portAt(e.target); if (el && (e.key === 'Enter' || e.key === ' ')) { e.preventDefault(); openPort(el); } }); async function load() { try { render(await lt.api.get('/api/rack')); } catch (err) { if (_rack) return; // keep the last drawing on a failed refresh document.getElementById('rack').innerHTML = `
Could not load the rack
${esc(err.message || err)}
`; } } readout(null); load(); setInterval(() => { if (!document.hidden && !document.querySelector('#rack .rack-hl')) load(); }, 60000);