Files
gandalf/static/fronts.js
T
jaredandClaude Opus 5.5 ce12dede5f
Lint / JS (eslint) (push) Successful in 12s
Lint / Python (flake8) (push) Successful in 1m10s
Lint / Notify on failure (push) Skipped
Lint / Deploy (push) Skipped
Security / Python Security (bandit) (push) Successful in 1m11s
Test / Python Tests (pytest) (push) Successful in 1m42s
Lint / Python (flake8) (pull_request) Successful in 2m28s
Lint / JS (eslint) (pull_request) Successful in 14s
Lint / Notify on failure (pull_request) Skipped
Lint / Deploy (pull_request) Skipped
Security / Python Security (bandit) (pull_request) Successful in 1m10s
Test / Python Tests (pytest) (pull_request) Successful in 1m16s
Faceplates: USW Pro Max 24 PoE and 48 PoE
Both laid out from the CC0 NetBox devicetype-library images, with jack
centres measured from them: the 24 has one row of 24 RJ45 in groups of 8
(17-24 at 2.5G) and 2 SFP+; the 48 has two rows, odd ports on top, in
groups of 16 (33-48 at 2.5G) and 4 SFP+ (49-52). The "2.5G" and "10G" bars
under the faster ports are drawn too (fpSpeedBar).

Registered as USPM24P/USPM48P with USPM24/USPM48 aliases, and as Front
drawing options so the new switch draws on the rack before it is adopted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 00:01:12 -05:00

154 lines
7.7 KiB
JavaScript

'use strict';
/* exported RACK_FRONTS, CHASSIS_FACEPLATES, rackFrontSvg */
// 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 => `<rect class="rf-slot" x="${x}" y="${y - 9}" width="9" height="18" rx="4"/>`).join('');
return `<rect class="${cls}" x="0" y="0" width="34" height="${h}" rx="3"/>${holes(12)}
<rect class="${cls}" x="${w - 34}" y="0" width="34" height="${h}" rx="3"/>${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(`<rect class="rf-vent" x="${x}" y="${y}" width="${seg}" height="${h}" rx="${h / 2}"/>`);
return out.join('');
}
function powerButton(cx, cy, r, status) {
return `<circle class="rf-power-ring" cx="${cx}" cy="${cy}" r="${r}" style="stroke:${frontLed(status)}"/>
<path class="rf-power-icon" d="M${cx - r * 0.35} ${cy - r * 0.3} a${r * 0.48} ${r * 0.48} 0 1 0 ${r * 0.7} 0 M${cx} ${cy - r * 0.55} v${r * 0.5}"/>`;
}
function usbA(x, y) {
return `<rect class="rf-port" x="${x}" y="${y}" width="22" height="9" rx="1"/><rect class="rf-port-tongue" x="${x + 3}" y="${y + 2}" width="16" height="3"/>`;
}
function usbC(x, y) {
return `<rect class="rf-port" x="${x}" y="${y}" width="16" height="6" rx="3"/>`;
}
function keyLock(cx, cy) {
return `<circle class="rf-lock" cx="${cx}" cy="${cy}" r="9"/><rect class="rf-lock-slot" x="${cx - 1.5}" y="${cy - 6}" width="3" height="12" rx="1"/>`;
}
function brand(x, y, text, anchor = 'start') {
return `<text class="rf-brand" x="${x}" y="${y}" text-anchor="${anchor}">${text}</text>`;
}
function driveBay(x, y, w, h, vertical) {
// A hot-swap bay: tray face, latch and activity light
const latch = vertical
? `<rect class="rf-latch" x="${x + w / 2 - 6}" y="${y + h - 26}" width="12" height="18" rx="2"/>`
: `<rect class="rf-latch" x="${x + w - 26}" y="${y + h / 2 - 6}" width="18" height="12" rx="2"/>`;
const led = vertical ? `<circle class="rf-bay-led" cx="${x + w / 2}" cy="${y + 10}" r="2.5"/>`
: `<circle class="rf-bay-led" cx="${x + 10}" cy="${y + h / 2}" r="2.5"/>`;
return `<rect class="rf-bay" x="${x}" y="${y}" width="${w}" height="${h}" rx="3"/>${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)}
<text class="rf-bay-num" x="${117 + i * 86}" y="396" text-anchor="middle">${i + 1}</text>`).join('');
return `<rect class="rf-body-dark" width="1100" height="420" rx="3"/>${ears(1100, 420, 'rf-ear-dark')}
<rect class="rf-blank" x="78" y="30" width="418" height="98" rx="3"/>
<rect class="rf-blank" x="510" y="30" width="418" height="98" rx="3"/>
${vents(98, 480, 74, 7, 8, 40)}${vents(530, 912, 74, 7, 8, 40)}
${bays}
<rect class="rf-io" x="950" y="30" width="104" height="360" rx="4"/>
${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 => `<circle class="rf-fan" cx="${cx}" cy="210" r="112"/>
<circle class="rf-fan-hub" cx="${cx}" cy="210" r="30"/>
${[0, 72, 144, 216, 288].map(a => `<path class="rf-fan-blade" transform="rotate(${a} ${cx} 210)" d="M${cx} 180 q40 -40 70 -10 q-30 10 -70 40 z"/>`).join('')}`;
const handle = x => `<rect class="rf-handle" x="${x}" y="60" width="16" height="300" rx="8"/>`;
return `<rect class="rf-body-dark" width="1100" height="420" rx="3"/>${ears(1100, 420, 'rf-ear-dark')}
${handle(36)}${handle(1048)}
<defs><pattern id="rf-perf" width="9" height="9" patternUnits="userSpaceOnUse">
<circle class="rf-perf-hole" cx="4.5" cy="4.5" r="2.4"/></pattern></defs>
<rect class="rf-door" x="64" y="16" width="880" height="388" rx="6"/>
${fan(225)}${fan(504)}${fan(783)}
<rect class="rf-mesh" x="80" y="32" width="848" height="356" rx="4" fill="url(#rf-perf)"/>
<rect class="rf-door-latch" x="900" y="180" width="26" height="60" rx="5"/>
<rect class="rf-io" x="954" y="16" width="86" height="388" rx="4"/>
${powerButton(997, 62, 18, status)}
<circle class="rf-reset" cx="997" cy="104" r="5"/>
<circle class="rf-led" cx="985" cy="130" r="3.5" style="fill:${frontLed(status)}"/><circle class="rf-led" cx="1009" cy="130" r="3.5"/>
${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 `<rect class="rf-body-dark" width="1100" height="${H}" rx="3"/>${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 `<svg class="rack-svg rack-front" viewBox="0 0 ${spec.width} ${spec.height}" preserveAspectRatio="none"
role="img" aria-label="${lt.escHtml(`${ctx.name}: ${spec.label}${ctx.status ? `, ${ctx.status}` : ''}`)}">
<title>${lt.escHtml(`${ctx.name} · ${spec.label}${ctx.status ? ` · ${ctx.status}` : ''}`)}</title>
${spec.draw(ctx)}
</svg>`;
}