'use strict'; /* exported RACK_FRONTS, CHASSIS_FACEPLATES, rackFrontSvg, shelfSvg, SHELF_GEAR */ // Drawn fronts for rack gear Gandalf can't read a faceplate from: custom-built servers. Chosen per cutsheet item ("Front drawing"). Each one // shows the device's live status on its power light when the item is linked to a device. // // Spec: { label, units, width, height, draw(ctx) -> SVG markup }, ctx: { status, name }. // Width:height should be about 10.5:1 per rack unit so the drawing fills its slot. const FRONT_STATUS = { up: '#2fd36b', degraded: '#ffb300', down: '#e0312b' }; function frontLed(status) { return FRONT_STATUS[status] || '#6b737d'; } function ears(w, h, cls = 'rf-ear') { // Rack ears with their mounting slots, both sides const slots = Array.from({ length: Math.round(h / 105) }, (_, i) => 105 * i + 52); const holes = x => slots.map(y => ``).join(''); return `${holes(12)} ${holes(w - 21)}`; } function vents(x0, x1, y, h = 8, gap = 6, seg = 120) { const out = []; for (let x = x0; x + seg <= x1; x += seg + gap) out.push(``); return out.join(''); } function powerButton(cx, cy, r, status) { return ` `; } function usbA(x, y) { return ``; } function usbC(x, y) { return ``; } function keyLock(cx, cy) { return ``; } function brand(x, y, text, anchor = 'start') { return `${text}`; } function driveBay(x, y, w, h, vertical) { // A hot-swap bay: tray face, latch and activity light const latch = vertical ? `` : ``; const led = vertical ? `` : ``; return `${latch}${led}`; } const RACK_FRONTS = { // Sliger CX4712 4U with its faceplate off (as driveAtlas draws it): two 5.25" blanks side by // side, ten 3.5" hot-swap bays standing on edge in one row, a narrow I/O strip on the right. 'sliger-cx4712': { label: 'Sliger CX4712 4U (10-bay)', units: 4, width: 1100, height: 420, draw({ status }) { // Bays numbered left to right, the local convention driveAtlas uses for this case const bays = Array.from({ length: 10 }, (_, i) => `${driveBay(78 + i * 86, 150, 78, 228, true)} ${i + 1}`).join(''); return `${ears(1100, 420, 'rf-ear-dark')} ${vents(98, 480, 74, 7, 8, 40)}${vents(530, 912, 74, 7, 8, 40)} ${bays} ${powerButton(1002, 78, 20, status)} ${usbA(991, 130)}${usbA(991, 150)}${usbC(995, 176)} ${keyLock(1002, 220)} ${brand(1002, 372, 'SLIGER', 'middle')}`; }, }, // Rosewill RSV-L4500U 4U: the perforated, lockable drop-down door over the fan wall // (15 bays sit behind it), D-handles on the ears, I/O strip with power and USB on the right. 'rosewill-rsv-l4500u': { label: 'Rosewill RSV-L4500U 4U (15-bay)', units: 4, width: 1100, height: 420, draw({ status }) { const fan = cx => ` ${[0, 72, 144, 216, 288].map(a => ``).join('')}`; const handle = x => ``; return `${ears(1100, 420, 'rf-ear-dark')} ${handle(36)}${handle(1048)} ${fan(225)}${fan(504)}${fan(783)} ${powerButton(997, 62, 18, status)} ${usbA(986, 160)}${usbA(986, 180)} ${keyLock(997, 232)} ${brand(997, 388, 'ROSEWILL', 'middle')}`; }, }, // Generic servers, for gear without a specific drawing 'server-1u': genericServer(1, 4), 'server-2u': genericServer(2, 12), 'server-4u': genericServer(4, 24), }; function genericServer(units, bays) { const H = 105 * units; return { label: `Generic ${units}U server`, units, width: 1100, height: H, draw({ status }) { const rows = Math.max(1, Math.round(units * 0.75)); const cols = Math.ceil(bays / rows); const bw = Math.min(118, 720 / cols - 8); const bh = (H - 30 - (rows - 1) * 8) / rows; const out = []; for (let r = 0; r < rows; r++) { for (let c = 0; c < cols && r * cols + c < bays; c++) out.push(driveBay(60 + c * (bw + 8), 15 + r * (bh + 8), bw, bh, false)); } return `${ears(1100, H, 'rf-ear-dark')} ${out.join('')} ${vents(810, 1040, 18, 7, 6, 36)}${units > 1 ? vents(810, 1040, H - 26, 7, 6, 36) : ''} ${powerButton(1020, H / 2, Math.min(18, H / 5), status)}`; }, }; } // Front drawings that are full faceplates in faceplates.js (same parts as the UniFi gear) const CHASSIS_FACEPLATES = { 'unvr-pro': 'UNVRPRO', // For a new switch before UniFi reports it (or under a model code not mapped yet) 'usw-pro-max-24-poe': 'USPM24P', 'usw-pro-max-48-poe': 'USPM48P', }; // The markup for one item's drawn front, or '' when the key is unknown function rackFrontSvg(key, ctx) { const spec = RACK_FRONTS[key]; if (!spec) return ''; return ` ${lt.escHtml(`${ctx.name} · ${spec.label}${ctx.status ? ` · ${ctx.status}` : ''}`)} ${spec.draw(ctx)} `; } // ── IceWhale ZimaBoard 832 (shelf gear) ───────────────────────────────── // The port end, traced from IceWhale's labelled photo in ZimaDocs // (source/images/Hardware-Interface-Introduction-/hardware-interface-introduction.webp), // in that photo's pixel coordinates (the port end spans 862 x 415 px; 81.4 mm wide). // Left to right: Mini-DisplayPort 1.2, two stacks of GbE RJ45 over USB 3.0, DC 12V 5.5x2.5mm, // on a gunmetal case over a thin gold PCB edge and the black polycarbonate base. const ZIMA = { width: 862, height: 415, mm: 81.4 }; function zimaRj45(x, y, link) { const contacts = Array.from({ length: 8 }, (_, i) => ``).join(''); return ` ${contacts} `; } function zimaUsb(x, y) { return ` ${Array.from({ length: 5 }, (_, i) => ``).join('')}`; } // Which NIC is which on a ZimaBoard: the onboard Realtek GbE ports enumerate on PCI buses 2 and 3 // (enp2s0 left jack, enp3s0 right), and a card in the PCIe slot on bus 1 (enp1s0, enp1s0d1). const ZIMA_NICS = { rj45: ['enp2s0', 'enp3s0'], slot: /^enp1s0/ }; function zimaNic(ctx, name) { const n = (ctx.interfaces || []).find(i => i.name === name); return n ? n.up : ctx.status === 'up'; } function zimaFront(ctx) { // ctx: { status, interfaces: [{name, up}] } const link = i => zimaNic(ctx, ZIMA_NICS.rj45[i]); const on = ctx.status === 'up' || ctx.status === 'degraded'; const pads = [60, 105, 420, 470, 640, 700, 790, 835] .map(x => ``).join(''); return ` ${on ? '' : ''} ${pads} ${zimaRj45(213, 47, link(0))}${zimaRj45(443, 47, link(1))} ${zimaUsb(216, 205)}${zimaUsb(443, 205)} `; } // A Mellanox ConnectX-3 single-port SFP+ card (MCX311A) in the ZimaBoard's side PCIe slot, lying flat // beside the board as in the rack photo (wiki.lotusguild.org/zimaboards.jpg): its front edge is about // 54 mm (572 px at the ZimaBoard photo's 10.59 px/mm), PCB at the board's PCB height, heatsink on the // chip, and the SFP+ cage toward the board with a DAC plugged in and its pull tab hanging down. // The round dot on the cage is Gandalf's link mark (like the cutsheet marks), not an LED on the card. const CX3_PX = 572; function cx3Card(up) { return ` ${Array.from({ length: 9 }, (_, i) => ``).join('')} Gandalf: SFP+ link ${up ? 'up' : 'down'}`; } function zimaCx3Front(ctx) { return `${cx3Card(zimaNic(ctx, (ctx.interfaces || []).map(i => i.name).find(n => ZIMA_NICS.slot.test(n)) || 'enp1s0'))} ${zimaFront(ctx)}`; } const ZIMA_SPECS = 'Intel Celeron N3450, 8GB LPDDR4, 32GB eMMC, 2x GbE, 2x USB 3.0, 2x SATA, PCIe 2.0 x4, 6W'; const SHELF_GEAR = { 'zimaboard-832': { label: 'IceWhale ZimaBoard 832', mm: ZIMA.mm, width: ZIMA.width, height: ZIMA.height, draw: zimaFront, specs: ZIMA_SPECS, }, 'zimaboard-832-cx3': { label: 'IceWhale ZimaBoard 832 + ConnectX-3 SFP+', mm: ZIMA.mm * (ZIMA.width + CX3_PX) / ZIMA.width, width: ZIMA.width + CX3_PX, height: ZIMA.height, draw: zimaCx3Front, specs: `${ZIMA_SPECS}; Mellanox ConnectX-3 10G SFP+ (enp1s0) in the PCIe slot, DAC attached`, }, }; // A shelf at true scale (1100 units = 482.6 mm, a 19" panel) with what sits on it, left to right. // Gear with a SHELF_GEAR drawing is drawn to its real width; anything else is a labelled box. function shelfSvg(item, esc) { const units = item.rack_height || 1; const W = 1100, H = 105 * units, perMm = W / 482.6; const floor = H - 20; let x = 64; const things = (item.contents || []).map(c => { const gear = SHELF_GEAR[c.chassis]; const w = gear ? gear.mm * perMm : 150; const k = gear ? w / gear.width : 1; const h = gear ? gear.height * k : 58; const top = floor - h; const nics = (c.interfaces || []).map(n => `${n.name} ${n.up ? 'up' : 'down'}`).join(', '); const tip = [c.name, gear ? gear.label : c.kind, c.status, nics, gear && gear.specs].filter(Boolean).join(' · '); const body = gear ? `${gear.draw(c)}` : ` ${esc(c.name)}`; const out = `${esc(tip)} ${body} ${esc(c.name)} `; x += w + 26; return out; }).join(''); return ` ${esc(item.name)} ${ears(W, H, 'rf-ear-dark')} ${things || `empty shelf`} ${esc(item.name)} `; }