diff --git a/README.md b/README.md index 41c91bb..58a605c 100644 --- a/README.md +++ b/README.md @@ -134,9 +134,10 @@ The rack front, drawn from the rack units set on cutsheet items, top first. live status. Cables may use either name: checks, suggestions, tickets and the device page treat an item and its device as the same thing. - **Drawn fronts**: gear without a live faceplate can pick a **Front drawing** on its cutsheet item: - UniFi NVR Pro (from the CC0 NetBox device library image), Sliger CX4712 4U (10 bays), Rosewill - RSV-L4500U 4U (15 bays behind the fan door), or a generic 1U/2U/4U server. The power light shows the - linked device's live status. Drawings live in `static/fronts.js`. + UniFi NVR Pro (a full faceplate, `UNVRPRO` in `static/faceplates.js`, laid out from the CC0 NetBox + device library image), Sliger CX4712 4U (10 numbered bays), Rosewill RSV-L4500U 4U (perforated fan + door), or a generic 1U/2U/4U server. The power light shows the linked device's live status. Case + drawings live in `static/fronts.js`. - **Shelves**: an item can sit on a shelf item instead of having a rack unit. The shelf draws what is on it side by side (mini PCs, desk switches), each with its status and a link to its page. - **Front and rear**: each item is mounted from the front, the rear, or full depth (both sides). On a diff --git a/static/faceplates.js b/static/faceplates.js index 4974235..8529031 100644 --- a/static/faceplates.js +++ b/static/faceplates.js @@ -284,6 +284,35 @@ const FACEPLATES = { }, }, + // UniFi NVR Pro, 2U. Layout from the CC0 NetBox devicetype-library front image + // (elevation-images/Ubiquiti/ubiquiti-unifi-protect-network-video-recorder-pro.front.png): + // touchscreen top left, bays 1-3 on top and 4-7 below, RJ45 (1) and SFP+ (2) top right. + // UniFi Network doesn't list a Protect NVR, so on the rack it is picked by Front drawing; + // if it is ever reported as UNVRPRO its ports go live like any other. + UNVRPRO: { + width: 1167, height: 230, frameClass: 'fp-scroll', scrollX: 700, + label: 'UniFi NVR Pro', + legend: [...LEGEND_RJ45, ['white', '10G']], + roles: { 1: 'RJ45 1G', 2: 'SFP+ 10G' }, + ports: [ + { idx: 1, type: 'rj45', x: 1040, y: 74, scale: 1.3, label: [1040, 44] }, + { idx: 2, type: 'sfp', x: 1072, y: 61, w: 40, h: 27, led: [1092, 96], label: [1092, 44] }, + ], + decor() { + const spans = [[97, 293], [302, 498], [507, 703], [712, 908], [916, 1112]]; + const bay = (x, y, n) => ` + + + ${n}`; + return `${fpVents(spans, 8, 4)}${fpVents(spans, 216, 4)} + ${fpQuadScreen(14, 33, 62)}${fpBrand(26, 112, 'NVR Pro')} + ${[97, 384, 669].map((x, i) => bay(x, 27, i + 1)).join('')} + ${[12, 299, 585, 871].map((x, i) => bay(x, 125, i + 4)).join('')} + + ${fpReset(1124, 219)}`; + }, + }, + // Port layout and LED colors: https://dl.ui.com/qsg/USW-Pro-24/USW-Pro-24_EN.html // (the non-PoE sibling). PoE draw has no documented LED, so it is a bolt by the number. US24PRO: { diff --git a/static/fronts.js b/static/fronts.js index 6fda2db..e8af622 100644 --- a/static/fronts.js +++ b/static/fronts.js @@ -1,7 +1,6 @@ 'use strict'; -/* exported RACK_FRONTS, rackFrontSvg */ -// Drawn fronts for rack gear Gandalf can't read a faceplate from: an NVR UniFi doesn't -// report, and custom-built servers. Chosen per cutsheet item ("Front drawing"). Each one +/* 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 }. @@ -59,39 +58,14 @@ function driveBay(x, y, w, h, vertical) { } const RACK_FRONTS = { - // Ubiquiti UNVR Pro: drawn from the CC0 NetBox devicetype-library front image. - // 2U; touchscreen top left, bays 1-3 on top and 4-7 below, RJ45 + SFP+ top right. - 'unvr-pro': { - label: 'UniFi NVR Pro (UNVR Pro)', units: 2, width: 1167, height: 230, - draw({ status }) { - const bay = (x, y, w, n) => ` - - - ${n}`; - const top = [97, 384, 669].map((x, i) => bay(x, 27, 283, i + 1)).join(''); - const bottom = [12, 299, 585, 871].map((x, i) => bay(x, 125, 283, i + 4)).join(''); - const strip = (y) => [97, 302, 507, 712, 916].map(x => ``).join(''); - return ` - ${strip(9)}${strip(216)} - - - NVR Pro - ${top}${bottom} - - - - 1 - 2 - RESET`; - }, - }, - // 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 }) { - const bays = Array.from({ length: 10 }, (_, i) => driveBay(78 + i * 86, 150, 78, 240, true)).join(''); + // 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')} @@ -116,9 +90,12 @@ const RACK_FRONTS = { 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)} @@ -156,6 +133,9 @@ function genericServer(units, bays) { }; } +// 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]; diff --git a/static/rack.js b/static/rack.js index c5d4a80..aa17ba4 100644 --- a/static/rack.js +++ b/static/rack.js @@ -1,5 +1,5 @@ 'use strict'; -/* global FACEPLATES, renderFaceplate, RACK_CAN_EDIT, RACK_FRONTS, rackFrontSvg */ +/* global FACEPLATES, renderFaceplate, RACK_CAN_EDIT, RACK_FRONTS, CHASSIS_FACEPLATES, rackFrontSvg */ // Rack page: draws the rack front from /api/rack. Switches use their real faceplates, // patch panels a generic panel; hovering a port lights up every port on the same cable run. @@ -38,9 +38,10 @@ function isRackFaceplate(model) { } // ── Item drawings ─────────────────────────────────────────────────────── -function switchHtml(item) { - const sw = item.switch; - return renderFaceplate(sw.model, { +function switchHtml(item, model) { + // A live switch, or a faceplate picked by Front drawing (no port data: drawn unlinked) + const sw = item.switch || { model, ports: {}, outlets: [], online: item.status === 'up' }; + return renderFaceplate(model || sw.model, { swName: item.device, portMap: portMapOf(sw.ports), outlets: sw.outlets, online: sw.online, rack: true, titleFor: (port, idx) => portTitle(port, idx), cutsheetFor: idx => { @@ -126,6 +127,7 @@ function itemHtml(item) { let body; if (item.through) body = backHtml(item); else if (item.switch && isRackFaceplate(item.switch.model)) body = switchHtml(item); + else if (CHASSIS_FACEPLATES[item.chassis]) body = switchHtml(item, CHASSIS_FACEPLATES[item.chassis]); else if (item.chassis && RACK_FRONTS[item.chassis]) body = rackFrontSvg(item.chassis, item); else if (item.kind === 'patch_panel') body = panelSvg(item); else if (item.kind === 'shelf') body = shelfHtml(item); diff --git a/static/style.css b/static/style.css index 8bc443f..fed6043 100644 --- a/static/style.css +++ b/static/style.css @@ -1545,11 +1545,14 @@ body.fp-marks-off .fp-svg .fp-mark { display: none; } .rf-lock-slot { fill: #2b3037; } .rf-led { fill: #6b737d; } .rf-reset { fill: #0b0d10; stroke: #6b737d; } -.rf-brand { fill: #9aa7b4; font-size: 13px; font-weight: 700; letter-spacing: .12em; font-family: Helvetica, Arial, sans-serif; } +.rf-brand { fill: #c3ccd5; font-size: 12px; font-weight: 700; letter-spacing: .14em; font-family: Helvetica, Arial, sans-serif; } .rf-small { fill: #4a4f57; font-size: 10px; font-family: Helvetica, Arial, sans-serif; } .rf-dark { fill: #1b1e22; font-weight: 700; } .rf-door { fill: #121519; stroke: #3a424c; stroke-width: 2; } -.rf-mesh { fill: #0b0d10; stroke: #262c33; } +.rf-mesh { stroke: #262c33; stroke-width: 2; opacity: .55; } +.rf-perf-hole { fill: #000; } +.rf-door-latch { fill: #3a424c; stroke: #59636f; } +.rf-bay-num { fill: #8a96a3; font-size: 11px; font-family: Helvetica, Arial, sans-serif; } .rf-fan { fill: #15191e; stroke: #2e353e; stroke-width: 6; } .rf-fan-hub { fill: #262c33; } .rf-fan-blade { fill: #1f252c; stroke: #2e353e; }