'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 ``;
}
// ── 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 ``;
}