1// ââ NETWORK DIAGRAM GENERATOR ââââââââââââââââââââââââââââââââââââââââââââââ 2 3function showDiagram() { 4 var wrap = document.getElementById('diagramSvgWrap'); 5 wrap.innerHTML = buildDiagramSVG(); 6 document.getElementById('diagramModal').removeAttribute('hidden'); 7} 8 9function closeDiagram() { 10 document.getElementById('diagramModal').setAttribute('hidden', ''); 11} 12 13function downloadDiagramSVG() { 14 var el = document.querySelector('#diagramSvgWrap svg'); 15 if (!el) return; 16 var xml = '<?xml version="1.0" encoding="UTF-8"?>\n' + el.outerHTML; 17 var blob = new Blob([xml], { type: 'image/svg+xml;charset=utf-8' }); 18 var url = URL.createObjectURL(blob); 19 var a = document.createElement('a'); 20 a.href = url; a.download = 'opengear-diagram.svg'; 21 document.body.appendChild(a); a.click(); 22 document.body.removeChild(a); URL.revokeObjectURL(url); 23} 24 25function downloadDiagramPNG() { 26 var svgEl = document.querySelector('#diagramSvgWrap svg'); 27 if (!svgEl) return; 28 var W = parseInt(svgEl.getAttribute('width')); 29 var H = parseInt(svgEl.getAttribute('height')); 30 var canvas = document.createElement('canvas'); 31 canvas.width = W * 2; 32 canvas.height = H * 2; 33 var ctx = canvas.getContext('2d'); 34 ctx.scale(2, 2); 35 var xml = '<?xml version="1.0" encoding="UTF-8"?>' + svgEl.outerHTML; 36 var blob = new Blob([xml], { type: 'image/svg+xml' }); 37 var url = URL.createObjectURL(blob); 38 var img = new Image(); 39 img.onload = function() { 40 ctx.drawImage(img, 0, 0); 41 URL.revokeObjectURL(url); 42 var a = document.createElement('a'); 43 a.href = canvas.toDataURL('image/png'); 44 a.download = 'opengear-diagram.png'; 45 document.body.appendChild(a); a.click(); document.body.removeChild(a); 46 }; 47 img.src = url; 48} 49 50function buildDiagramSVG() { 51 if (state.deploymentMode && state.configs && state.configs.some(function(s) { return s.racks && s.racks.length > 0; })) { 52 return _dgDeploy(); 53 } 54 return _dgSingle(); 55} 56 57// ââ Colors âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 58var _DG = { 59 bg: '#f8fafc', 60 titleBg: '#0f172a', 61 device: '#3a3a3a', deviceStroke: '#505050', deviceBezel: '#525252', 62 led: '#33ee55', ledGlow: '#22cc44', 63 ports: '#22c55e', 64 rack: '#3a3a3a', rackBezel: '#525252', rackHighlight: '#585858', 65 rackFrame: '#c8cdd4', rackFrameStroke: '#8a9199', rackFrameInner: '#d8dce2', 66 lh: '#3b0764', lhStroke: '#7c3aed', 67 site: '#e8ecf0', siteStroke: '#8a9199', siteHdr: '#c8cdd4', 68 acc: '#1e293b', accStroke: '#334155', 69 line: '#94a3b8', 70 badge: '#0ea5e9', 71 support: '#7c3aed', 72 text: '#0f172a', textMid: '#475569', textMut: '#94a3b8', 73}; 74 75// ââ SINGLE DEVICE DIAGRAM ââââââââââââââââââââââââââââââââââââââââââââââââââ 76function _dgSingle() { 77 var model = state.bomModel; 78 var hasLH = state.lighthouse === 'adding'; 79 var lhSku = hasLH ? getLighthouseSku() : null; 80 81 var accList = Object.keys(state.accAdded || {}).map(function(sku) { 82 var acc = ALL_ACCESSORIES.find(function(a) { return a.sku === sku; }); 83 return acc ? { title: acc.title, sku: sku, qty: state.accAdded[sku].qty || 1 } : null; 84 }).filter(Boolean); 85 86 var W = 780, PAD = 40, cx = W / 2; 87 var DW = 290, DH = 72; 88 var LHW = 260, LHH = 72; 89 var AW = 168, AH = 50, AGAP = 12, ACOLS = 4; 90 91 // Y layout 92 var y = 72; 93 var lhTop = 0, lhBtm = 0; 94 if (hasLH) { lhTop = y; lhBtm = lhTop + LHH; y = lhBtm + 44; } 95 var devTop = y, devBtm = devTop + DH; 96 y = devBtm; 97 98 var showAccs = accList.slice(0, 8); 99 var accCols = Math.min(ACOLS, showAccs.length); 100 var accRows = Math.ceil(showAccs.length / ACOLS); 101 var busY = 0, accTop = 0; 102 if (showAccs.length > 0) { 103 busY = devBtm + 38; 104 accTop = busY + 18; 105 y = accTop + accRows * (AH + AGAP) - AGAP; 106 } 107 var H = y + 56; 108 109 var out = []; 110 out.push(_dgOpen(W, H)); 111 out.push(_dgBg(W, H)); 112 out.push(_dgTitleBar(W, model ? 'Configuration Diagram' : 'Configuration Diagram', PAD)); 113
114 // Support badge 115 if (state.supportPlan) { 116 var sp = SUPPORT_PLANS.find(function(p) { return p.id === state.supportPlan; }); 117 var spLabel = sp ? sp.title : state.supportPlan; 118 out.push('<text x="' + (W - PAD) + '" y="38" font-size="11" fill="' + _DG.textMid + '" text-anchor="end">' + escHtml(spLabel + ' · ' + state.supportYears + 'yr') + '</text>'); 119 } 120 121 if (!model && !hasLH) { 122 out.push('<text x="' + cx + '" y="' + (H / 2 + 10) + '" font-size="14" fill="' + _DG.textMut + '" text-anchor="middle">Select a model to generate a diagram</text>'); 123 out.push('</svg>'); 124 return out.join('\n'); 125 } 126 127 // Lighthouse 128 if (hasLH) { 129 out.push(_dgLhBox(cx - LHW / 2, lhTop, LHW, LHH, lhSku)); 130 out.push(_dgLineV(cx, lhBtm, devTop)); 131 } 132 133 // Device 134 if (model) { 135 out.push(_dgDeviceBox(cx - DW / 2, devTop, DW, DH, model.title, model.sku)); 136 } 137 138 // Accessories 139 if (showAccs.length > 0) { 140 var totalW = accCols * AW + (accCols - 1) * AGAP; 141 var aStartX = cx - totalW / 2; 142 var busX1 = aStartX + AW / 2; 143 var busX2 = aStartX + (accCols - 1) * (AW + AGAP) + AW / 2; 144 145 out.push(_dgLineV(cx, devBtm, busY)); 146 if (accCols > 1) out.push(_dgLineH(busX1, busX2, busY)); 147 148 showAccs.forEach(function(acc, i) { 149 var col = i % ACOLS, row = Math.floor(i / ACOLS); 150 var ax = aStartX + col * (AW + AGAP); 151 var ay = accTop + row * (AH + AGAP); 152 var acx = ax + AW / 2; 153 out.push(_dgLineV(acx, busY, ay)); 154 out.push(_dgAccBox(ax, ay, AW, AH, acc.title, acc.sku, acc.qty)); 155 }); 156 157 if (accList.length > 8) { 158 out.push('<text x="' + cx + '" y="' + (H - 18) + '" font-size="11" fill="' + _DG.textMut + '" text-anchor="middle">+ ' + (accList.length - 8) + ' more accessories not shown</text>'); 159 } 160 } 161 162 out.push('</svg>'); 163 return out.join('\n'); 164} 165 166// ââ DEPLOYMENT DIAGRAM âââââââââââââââââââââââââââââââââââââââââââââââââââââ 167function _dgDeploy() { 168 var sites = state.configs.filter(function(s) { return s.racks && s.racks.length > 0; }); 169 var hasLH = state.lighthouse === 'adding'; 170 var lhSku = hasLH ? getLighthouseSku() : null; 171 172 var PAD = 48; 173 var SW = 214; // site box width 174 var SGAP = 24; // gap between sites 175 var SPAD = 14; // site inner padding 176 var RH = 54; // rack entry height 177 var RGAP = 8; // gap between racks 178 var SHDR = 36; // site header height 179 var LHW = 250, LHH = 72; 180 181 function siteH(site) { 182 var n = (site.racks || []).length; 183 return SHDR + SPAD + n * RH + (n - 1) * RGAP + SPAD; 184 } 185 var maxSiteH = Math.max.apply(null, sites.map(siteH).concat([100])); 186 187 var COLS = Math.min(4, sites.length); 188 var ROWS = Math.ceil(sites.length / COLS); 189 var contentW = COLS * SW + (COLS - 1) * SGAP; 190 var W = Math.max(760, contentW + PAD * 2); 191 var cx = W / 2; 192 193 var y = 72; 194 var lhTop = 0, lhBtm = 0; 195 if (hasLH) { lhTop = y; lhBtm = lhTop + LHH; y = lhBtm + 44; } 196 var sitesTopY = y; 197 var H = sitesTopY + ROWS * (maxSiteH + SGAP) - SGAP + PAD; 198 199 var sitesStartX = cx - contentW / 2; 200 201 var out = []; 202 out.push(_dgOpen(W, H)); 203 out.push(_dgBg(W, H)); 204 out.push(_dgTitleBar(W, 'Deployment Diagram', PAD)); 205 206 var totalRacks = sites.reduce(function(s, c) { return s + c.racks.length; }, 0); 207 var summary = sites.length + ' site' + (sites.length !== 1 ? 's' : '') + ' · ' + totalRacks + ' device' + (totalRacks !== 1 ? 's' : ''); 208 out.push('<text x="' + (W - PAD) + '" y="38" font-size="11" fill="' + _DG.textMid + '" text-anchor="end">' + escHtml(summary) + '</text>'); 209 210 if (hasLH) { 211 out.push(_dgLhBox(cx - LHW / 2, lhTop, LHW, LHH, lhSku)); 212 } 213 214 // Bus from LH to sites 215 if (hasLH && sites.length > 0) { 216 var busY = lhBtm + 22; 217 var firstCx = sitesStartX + SW / 2; 218 var lastCx = sitesStartX + (COLS - 1) * (SW + SGAP) + SW / 2; 219 out.push(_dgLineV(cx, lhBtm, busY)); 220 if (sites.length > 1) out.push(_dgLineH(firstCx, lastCx, busY)); 221
222 sites.slice(0, COLS).forEach(function(_, i) { 223 var scx = sitesStartX + i * (SW + SGAP) + SW / 2; 224 out.push(_dgLineV(scx, busY, sitesTopY)); 225 }); 226 // Additional rows: just a line from top of each site 227 if (ROWS > 1) { 228 sites.slice(COLS).forEach(function(_, i) { 229 var col = i % COLS, row = Math.floor(i / COLS) + 1; 230 var scx = sitesStartX + col * (SW + SGAP) + SW / 2; 231 var sy = sitesTopY + row * (maxSiteH + SGAP); 232 // no bus for rows > 0, just a subtle label 233 }); 234 } 235 } 236 237 sites.forEach(function(site, i) { 238 var col = i % COLS, row = Math.floor(i / COLS); 239 var sx = sitesStartX + col * (SW + SGAP); 240 var sy = sitesTopY + row * (maxSiteH + SGAP); 241 out.push(_dgSiteBox(sx, sy, SW, siteH(site), site.label || ('Site ' + (i + 1)), site.racks, SPAD, RH, RGAP, SHDR)); 242 }); 243 244 out.push('</svg>'); 245 return out.join('\n'); 246} 247 248// ââ SVG PRIMITIVES âââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 249function _dgOpen(w, h) { 250 return '<svg xmlns="http://www.w3.org/2000/svg" width="' + w + '" height="' + h + '" viewBox="0 0 ' + w + ' ' + h + '" font-family="-apple-system,BlinkMacSystemFont,\'Segoe UI\',Roboto,sans-serif">'; 251} 252 253function _dgBg(w, h) { 254 // Subtle dot grid 255 var dots = '<pattern id="dg" x="0" y="0" width="20" height="20" patternUnits="userSpaceOnUse"><circle cx="10" cy="10" r="0.8" fill="#cbd5e1"/></pattern>'; 256 return '<defs>' + dots + '</defs><rect width="' + w + '" height="' + h + '" fill="' + _DG.bg + '"/><rect width="' + w + '" height="' + h + '" fill="url(#dg)"/>'; 257} 258 259function _dgTitleBar(w, title, pad) { 260 return ( 261 '<rect width="' + w + '" height="52" fill="' + _DG.titleBg + '"/>' + 262 '<text x="' + pad + '" y="33" font-size="14" font-weight="700" fill="white" letter-spacing="-0.3">' + escHtml(title) + '</text>' + 263 '<text x="' + pad + '" y="46" font-size="10" fill="rgba(255,255,255,0.4)" letter-spacing="0.5">OPENGEAR PRODUCT CONFIGURATOR</text>' 264 ); 265} 266 267function _dgLineV(x, y1, y2) { 268 return '<line x1="' + x + '" y1="' + y1 + '" x2="' + x + '" y2="' + y2 + '" stroke="' + _DG.line + '" stroke-width="1.5"/>'; 269} 270 271function _dgLineH(x1, x2, y) { 272 return '<line x1="' + x1 + '" y1="' + y + '" x2="' + x2 + '" y2="' + y + '" stroke="' + _DG.line + '" stroke-width="1.5"/>'; 273} 274 275function _dgLhBox(x, y, w, h, sku) { 276 var out = []; 277 // Shadow 278 out.push('<rect x="' + (x+3) + '" y="' + (y+3) + '" width="' + w + '" height="' + h + '" rx="10" fill="rgba(0,0,0,0.12)"/>'); 279 // Body 280 out.push('<rect x="' + x + '" y="' + y + '" width="' + w + '" height="' + h + '" rx="10" fill="' + _DG.lh + '" stroke="' + _DG.lhStroke + '" stroke-width="1.5"/>'); 281 // Icon (server cluster) 282 var ix = x + 16, iy = y + h / 2; 283 out.push('<g opacity="0.85">'); 284 // Three server bars 285 [-10, 0, 10].forEach(function(dy, i) { 286 var barY = iy + dy - 4; 287 out.push('<rect x="' + ix + '" y="' + barY + '" width="30" height="8" rx="2" fill="none" stroke="rgba(255,255,255,0.4)" stroke-width="1.5"/>'); 288 out.push('<circle cx="' + (ix + 7) + '" cy="' + (barY + 4) + '" r="2.5" fill="' + (i === 0 ? '#22c55e' : i === 1 ? '#f59e0b' : '#94a3b8') + '"/>'); 289 out.push('<rect x="' + (ix + 14) + '" y="' + (barY + 2) + '" width="12" height="4" rx="1" fill="rgba(255,255,255,0.2)"/>'); 290 }); 291 out.push('</g>'); 292 // Text 293 var tx = ix + 42; 294 out.push('<text x="' + tx + '" y="' + (iy - 12) + '" font-size="9.5" font-weight="700" fill="rgba(255,255,255,0.55)" letter-spacing="1.2">LIGHTHOUSE</text>'); 295 out.push('<text x="' + tx + '" y="' + (iy + 7) + '" font-size="13.5" font-weight="700" fill="white">' + escHtml(sku || 'Opengear Lighthouse') + '</text>'); 296 out.push('<text x="' + tx + '" y="' + (iy + 22) + '" font-size="9.5" fill="rgba(255,255,255,0.45)">' + escHtml(state.lighthouseTier === 'CORE' ? 'Core Edition' : 'Enhance Edition') + ' · ' + escHtml(state.lighthouseNodes + ' nodes') + '</text>'); 297 return out.join('\n'); 298} 299 300function _dgDeviceBox(x, y, w, h, title, sku) { 301 var out = []; 302 var cy = y + h / 2; 303 304 // Drop shadow 305 out.push('<rect x="' + (x+3) + '" y="' + (y+3) + '" width="' + w + '" height="' + h + '" rx="6" fill="rgba(0,0,0,0.18)"/>'); 306 // Chassis body 307 out.push('<rect x="' + x + '" y="' + y + '" width="' + w + '" height="' + h + '" rx="6" fill="' + _DG.rack + '" stroke="' + _DG.rackBezel + '" stroke-width="1.5"/>'); 308 // Top bezel highlight strip 309 out.push('<rect x="' + (x+1) + '" y="' + (y+1) + '" width="' + (w-2) + '" height="5" rx="5" fill="' + _DG.rackHighlight + '"/>'); 310 // Bottom edge shadow line 311 out.push('<rect x="' + (x+1) + '" y="' + (y+h-4) + '" width="' + (w-2) + '" height="3" rx="2" fill="rgba(0,0,0,0.25)"/>'); 312 313 // Left panel section (LED area) 314 var panelW = 28; 315 out.push('<rect x="' + (x+1) + '" y="' + (y+1) + '" width="' + panelW + '" height="' + (h-2) + '" rx="5" fill="rgba(0,0,0,0.18)"/>'); 316 // 2 green LEDs stacked on left 317 var ledX = x + 10; 318 var leds = [cy - 8, cy + 4]; 319 leds.forEach(function(ly) { 320 out.push('<circle cx="' + ledX + '" cy="' + ly + '" r="5" fill="rgba(0,0,0,0.3)"/>'); 321 out.push('<circle cx="' + ledX + '" cy="' + ly + '" r="3.5" fill="' + _DG.led + '"/>'); 322 out.push('<circle cx="' + ledX + '" cy="' + ly + '" r="6" fill="' + _DG.ledGlow + '" opacity="0.22"/>'); 323 out.push('<circle cx="' + (ledX-1) + '" cy="' + (ly-1) + '" r="1.2" fill="rgba(255,255,255,0.7)"/>'); 324 }); 325 326 // Port row in center 327 var portStartX = x + panelW + 12; 328 var numPorts = 12, portW = 7, portH = 10, portGap = 3; 329 for (var p = 0; p < numPorts; p++) { 330 var lit = p < 8; 331 var px = portStartX + p * (portW + portGap); 332 out.push('<rect x="' + px + '" y="' + (cy - portH/2) + '" width="' + portW + '" height="' + portH + '" rx="1.5" fill="' + (lit ? '#1a1a1a' : '#222') + '" stroke="' + _DG.rackBezel + '" stroke-width="0.5"/>'); 333 if (lit) out.push('<rect x="' + (px+1.5) + '" y="' + (cy+portH/2-3) + '" width="' + (portW-3) + '" height="2" rx="1" fill="' + _DG.led + '" opacity="' + (p < 6 ? 0.85 : 0.35) + '"/>'); 334 } 335 336 // Divider before label section 337 var divX = portStartX + numPorts * (portW + portGap) + 8; 338 out.push('<line x1="' + divX + '" y1="' + (y+8) + '" x2="' + divX + '" y2="' + (y+h-8) + '" stroke="' + _DG.rackBezel + '" stroke-width="1"/>'); 339 340 // SKU / brand label 341 var tx = divX + 10; 342 if (tx + 60 < x + w - 8) { 343 out.push('<text x="' + tx + '" y="' + (cy - 7) + '" font-size="8.5" font-weight="700" fill="rgba(255,255,255,0.35)" letter-spacing="0.8">OPENGEAR</text>'); 344 var skuDisp = (sku || title || '').length > 18 ? (sku || title).slice(0, 18) + 'â¦' : (sku || title); 345 out.push('<text x="' + tx + '" y="' + (cy + 10) + '" font-size="12" font-weight="700" fill="white">' + escHtml(skuDisp) + '</text>'); 346 } 347 return out.join('\n'); 348} 349 350function _dgAccBox(x, y, w, h, title, sku, qty) { 351 var out = []; 352 out.push('<rect x="' + x + '" y="' + y + '" width="' + w + '" height="' + h + '" rx="6" fill="' + _DG.acc + '" stroke="' + _DG.accStroke + '" stroke-width="1"/>'); 353 // Cable icon
354 var ix = x + 10, iy = y + h / 2; 355 out.push('<rect x="' + ix + '" y="' + (iy - 7) + '" width="14" height="14" rx="3" fill="none" stroke="rgba(255,255,255,0.25)" stroke-width="1.5"/>'); 356 out.push('<line x1="' + (ix + 14) + '" y1="' + iy + '" x2="' + (ix + 20) + '" y2="' + iy + '" stroke="rgba(255,255,255,0.25)" stroke-width="1.5"/>'); 357 // Text 358 var tx = ix + 24; 359 var skuDisp = sku.length > 16 ? sku.slice(0, 16) + 'â¦' : sku; 360 var titleDisp = title.length > 20 ? title.slice(0, 20) + 'â¦' : title; 361 out.push('<text x="' + tx + '" y="' + (iy - 5) + '" font-size="9" fill="rgba(255,255,255,0.4)">' + escHtml(skuDisp) + '</text>'); 362 out.push('<text x="' + tx + '" y="' + (iy + 11) + '" font-size="11" fill="white">' + escHtml(titleDisp) + '</text>'); 363 if (qty > 1) { 364 out.push('<rect x="' + (x + w - 28) + '" y="' + (y + 10) + '" width="20" height="16" rx="8" fill="' + _DG.badge + '"/>'); 365 out.push('<text x="' + (x + w - 18) + '" y="' + (y + 22) + '" font-size="9" font-weight="700" fill="white" text-anchor="middle">Ã' + qty + '</text>'); 366 } 367 return out.join('\n'); 368} 369 370function _dgSiteBox(x, y, w, h, label, racks, spad, rh, rgap, shdr) { 371 var out = []; 372 373 // ââ Outer drop shadow ââââââââââââââââââââââââââââââââââââââââââââââââââââ 374 out.push('<rect x="' + (x+3) + '" y="' + (y+3) + '" width="' + w + '" height="' + h + '" rx="10" fill="rgba(0,0,0,0.10)"/>'); 375 376 // ââ Rack enclosure frame (metallic gray) âââââââââââââââââââââââââââââââââ 377 out.push('<rect x="' + x + '" y="' + y + '" width="' + w + '" height="' + h + '" rx="10" fill="' + _DG.rackFrame + '" stroke="' + _DG.rackFrameStroke + '" stroke-width="1.5"/>'); 378 // Inner inset â gives the recessed panel feel 379 var FP = 8; // frame padding 380 out.push('<rect x="' + (x+FP) + '" y="' + (y+shdr) + '" width="' + (w-FP*2) + '" height="' + (h-shdr-FP) + '" rx="4" fill="' + _DG.rackFrameInner + '" stroke="' + _DG.rackFrameStroke + '" stroke-width="0.75"/>'); 381 382 // ââ Site label header ââââââââââââââââââââââââââââââââââââââââââââââââââââ 383 out.push('<rect x="' + x + '" y="' + y + '" width="' + w + '" height="' + shdr + '" rx="10" fill="' + _DG.siteHdr + '"/>'); 384 out.push('<rect x="' + x + '" y="' + (y+shdr-10) + '" width="' + w + '" height="10" fill="' + _DG.siteHdr + '"/>'); 385 // Location pin emoji + label 386 out.push('<text x="' + (x+10) + '" y="' + (y+22) + '" font-size="12">ð</text>'); 387 var lbl = label.length > 15 ? label.slice(0, 15) + 'â¦' : label; 388 out.push('<text x="' + (x+27) + '" y="' + (y+22) + '" font-size="11.5" font-weight="700" fill="#374151">' + escHtml(lbl) + '</text>'); 389 // Rack unit count chip 390 var chipTxt = racks.length + 'U'; 391 out.push('<rect x="' + (x+w-36) + '" y="' + (y+8) + '" width="28" height="16" rx="8" fill="rgba(0,0,0,0.12)"/>'); 392 out.push('<text x="' + (x+w-22) + '" y="' + (y+20) + '" font-size="9" font-weight="700" fill="#4b5563" text-anchor="middle">' + chipTxt + '</text>'); 393 394 // ââ Rack units âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 395 var rw = w - (spad + FP) * 2 + FP; // width inside inner panel 396 var rxi = x + FP + spad; 397 var rty = y + shdr + spad; 398 399 racks.forEach(function(rack, i) { 400 var ry = rty + i * (rh + rgap); 401 var sku = rack.resolvedSku || rack.baseSku || 'Device'; 402 var cy = ry + rh / 2; 403 404 // Chassis drop shadow 405 out.push('<rect x="' + (rxi+2) + '" y="' + (ry+2) + '" width="' + rw + '" height="' + rh + '" rx="4" fill="rgba(0,0,0,0.22)"/>'); 406 // Chassis body 407 out.push('<rect x="' + rxi + '" y="' + ry + '" width="' + rw + '" height="' + rh + '" rx="4" fill="' + _DG.rack + '" stroke="' + _DG.rackBezel + '" stroke-width="1"/>'); 408 // Top bezel highlight 409 out.push('<rect x="' + (rxi+1) + '" y="' + (ry+1) + '" width="' + (rw-2) + '" height="4" rx="3" fill="' + _DG.rackHighlight + '"/>'); 410 // Bottom shadow edge 411 out.push('<rect x="' + (rxi+1) + '" y="' + (ry+rh-3) + '" width="' + (rw-2) + '" height="2" rx="1" fill="rgba(0,0,0,0.3)"/>'); 412 413 // Left LED panel recess 414 var panelW = 22; 415 out.push('<rect x="' + (rxi+1) + '" y="' + (ry+1) + '" width="' + panelW + '" height="' + (rh-2) + '" rx="3" fill="rgba(0,0,0,0.2)"/>'); 416 417 // 2 green LEDs (status indicators) on left panel 418 var ledX = rxi + 10; 419 var ledPositions = [cy - 7, cy + 5]; 420 ledPositions.forEach(function(ly) { 421 out.push('<circle cx="' + ledX + '" cy="' + ly + '" r="4.5" fill="rgba(0,0,0,0.4)"/>'); // bezel hole 422 out.push('<circle cx="' + ledX + '" cy="' + ly + '" r="3" fill="' + _DG.led + '"/>'); // LED 423 out.push('<circle cx="' + ledX + '" cy="' + ly + '" r="5.5" fill="' + _DG.ledGlow + '" opacity="0.2"/>'); // glow 424 out.push('<circle cx="' + (ledX-1) + '" cy="' + (ly-1) + '" r="1" fill="rgba(255,255,255,0.65)"/>'); // specular 425 }); 426 427 // Port connectors (small rectangles, 5 wide) 428 var portStartX = rxi + panelW + 8; 429 var numPorts = 5, portW = 7, portH = 12, portGap = 4; 430 for (var p = 0; p < numPorts; p++) { 431 var px = portStartX + p * (portW + portGap); 432 var active = p < 3; 433 out.push('<rect x="' + px + '" y="' + (cy-portH/2) + '" width="' + portW + '" height="' + portH + '" rx="1.5" fill="#1c1c1c" stroke="' + _DG.rackBezel + '" stroke-width="0.5"/>'); 434 if (active) out.push('<rect x="' + (px+1.5) + '" y="' + (cy+portH/2-3) + '" width="' + (portW-3) + '" height="2" rx="1" fill="' + _DG.led + '" opacity="0.8"/>'); 435 } 436 437 // Vertical divider before text 438 var dvx = portStartX + numPorts * (portW + portGap) + 8; 439 out.push('<line x1="' + dvx + '" y1="' + (ry+6) + '" x2="' + dvx + '" y2="' + (ry+rh-6) + '" stroke="' + _DG.rackBezel + '" stroke-width="0.75" stroke-dasharray="2,2"/>'); 440 441 // SKU label 442 var textX = dvx + 7; 443 var skuDisp = sku.length > 13 ? sku.slice(0, 13) + 'â¦' : sku; 444 out.push('<text x="' + textX + '" y="' + (cy+5) + '" font-size="10" font-weight="700" fill="white">' + escHtml(skuDisp) + '</text>'); 445 446 // Accessory count badge (right) 447 var accCt = Object.keys(rack.accAdded || {}).length; 448 if (accCt > 0) { 449 var bx = rxi + rw - 30; 450 out.push('<rect x="' + bx + '" y="' + (ry+10) + '" width="26" height="13" rx="6.5" fill="' + _DG.badge + '"/>'); 451 out.push('<text x="' + (bx+13) + '" y="' + (ry+21) + '" font-size="8" fill="white" text-anchor="middle">' + accCt + ' acc</text>'); 452 } 453
454 // Support plan badge (bottom-left of chassis) 455 if (rack.supportPlan) { 456 var sp = SUPPORT_PLANS.find(function(p) { return p.id === rack.supportPlan; }); 457 var spLbl = sp ? sp.title.slice(0, 8) : rack.supportPlan.slice(0, 8); 458 out.push('<rect x="' + (rxi+3) + '" y="' + (ry+rh-11) + '" width="34" height="8" rx="4" fill="' + _DG.support + '" opacity="0.8"/>'); 459 out.push('<text x="' + (rxi+20) + '" y="' + (ry+rh-5) + '" font-size="6.5" fill="white" text-anchor="middle">' + escHtml(spLbl) + '</text>'); 460 } 461 }); 462 463 return out.join('\n'); 464}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.