'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' };
// 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('')}`;
}
function zimaFront(ctx) {
// ctx: { status, interfaces: [{name, up}] } - one RJ45 per NIC, left to right
const nics = ctx.interfaces || [];
const link = i => (nics[i] ? nics[i].up : ctx.status === 'up');
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)}
`;
}
const SHELF_GEAR = {
'zimaboard-832': {
label: 'IceWhale ZimaBoard 832', mm: ZIMA.mm, width: ZIMA.width, height: ZIMA.height, draw: zimaFront,
specs: 'Intel Celeron N3450, 8GB LPDDR4, 32GB eMMC, 2x GbE, 2x USB 3.0, 2x SATA, PCIe 2.0 x4, 6W',
},
};
// 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 ``;
}