'use strict'; /* exported RACK_FRONTS, CHASSIS_FACEPLATES, rackFrontSvg, shelfSvg, SHELF_GEAR, DESK_FACEPLATES */ // 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`, }, }; // UniFi desk gear drawn from its live faceplate when it sits on a shelf: the real width of the // face (Ubiquiti tech specs) and the span of that face in the faceplate drawing, to scale it. const DESK_FACEPLATES = { USMINI: { mm: 107, body: [2, 661], label: 'UniFi Switch Flex Mini' }, // 107 x 70 x 21 mm USF5P: { mm: 122.5, body: [66, 488], label: 'UniFi Switch Flex' }, // 122.5 x 107.1 x 28 mm }; // 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, or UniFi desk gear with a live faceplate, is drawn to its real // width; anything else is a labelled box. A shelf with Front drawing "resting" has no tray: its gear // lies directly on whatever is below it (a switch on top of the NVR). function shelfSvg(item, esc, faceplate) { const units = item.rack_height || 1; const W = 1100, H = 105 * units, perMm = W / 482.6; const resting = item.chassis === 'resting'; const floor = resting ? H - 3 : H - 20; let x = 64; const things = (item.contents || []).map(c => { const gear = SHELF_GEAR[c.chassis]; const desk = !gear && faceplate && DESK_FACEPLATES[c.model]; let w, h, body; if (gear) { w = gear.mm * perMm; const k = w / gear.width; h = gear.height * k; body = `${gear.draw(c)}`; } else if (desk) { // Scale the faceplate so its body spans the switch's real width, then place the whole drawing const spec = faceplate.spec(c.model); const k = (desk.mm * perMm) / (desk.body[1] - desk.body[0]); w = desk.mm * perMm; h = spec.height * k; body = faceplate.svg(c, `x="${x - desk.body[0] * k}" y="${floor - h}" width="${spec.width * k}" height="${h}"`); } else { w = 150; h = 58; body = ` ${esc(c.name)}`; } const top = floor - h; const nics = (c.interfaces || []).map(n => `${n.name} ${n.up ? 'up' : 'down'}`).join(', '); const label = gear ? gear.label : (desk ? desk.label : c.kind); const tip = [c.name, label, c.status, nics, gear && gear.specs].filter(Boolean).join(' · '); // A live faceplate's ports are links of their own, so it isn't wrapped in one const open = desk ? '' : ``; const tagY = resting ? top - 16 : floor + 5; const out = `${open}${esc(tip)} ${body} ${desk ? '' : ``} ${esc(c.name)} ${desk ? '' : ''}`; x += w + 26; return out; }).join(''); const tray = resting ? '' : `${ears(W, H, 'rf-ear-dark')} `; return ` ${esc(item.name)} ${tray} ${things || `${resting ? 'nothing here' : 'empty shelf'}`} ${esc(item.name)} `; }