diff --git a/README.md b/README.md
index c5e714a..993e0f3 100644
--- a/README.md
+++ b/README.md
@@ -154,7 +154,11 @@ The rack front, drawn from the rack units set on cutsheet items, top first.
enp2s0 (left) and enp3s0 (right), and the status glow follows the host. The "ZimaBoard 832 +
ConnectX-3" drawing adds the single-port SFP+ card lying flat in the side PCIe slot, as in the rack,
with a DAC attached; its round dot is Gandalf's link mark for enp1s0 (the slot is PCI bus 1), not an
- LED on the card. Anything else is a labelled box.
+ LED on the card. UniFi desk switches (Flex Mini, Flex) on a shelf are drawn with their live faceplate
+ at their real width. Anything else is a labelled box.
+- **Gear resting on other gear**: a shelf item with Front drawing "No shelf: gear resting on what is
+ below" draws no tray, so a switch lying on top of the NVR is a "shelf" in the unit above the NVR whose
+ contents sit directly on it.
- **Racks**: admins name a rack and set its height with **Rack settings** on the page, and add more with
**+ Rack** (a tab per rack). Renaming a rack moves its items; an empty rack can be deleted. Each item
has a rack (empty means the first). Until a rack is saved, racks come from config:
diff --git a/app.py b/app.py
index b18fba1..4b49211 100644
--- a/app.py
+++ b/app.py
@@ -1210,7 +1210,8 @@ def api_rack():
'status': _device_status(dev(c), snapshot) if dev(c) in known else '',
'model': next((u.get('model') for u in snapshot.get('unifi') or [] if u.get('name') == dev(c)), '')
or '', 'notes': c.get('notes') or '', 'chassis': c.get('chassis') or '',
- 'slot': c.get('slot'), 'interfaces': _physical_interfaces(dev(c), snapshot)}
+ 'slot': c.get('slot'), 'interfaces': _physical_interfaces(dev(c), snapshot),
+ 'device': dev(c), 'ports': (switches.get(dev(c)) or {}).get('ports') or {}}
for c in sorted(on_shelf.get(name, []), key=lambda c: (c.get('slot') or 999, c['name'].lower()))
]
out.append(entry)
diff --git a/cutsheet.py b/cutsheet.py
index 3f13b1d..9111bfd 100644
--- a/cutsheet.py
+++ b/cutsheet.py
@@ -41,6 +41,7 @@ CHASSIS = {
'rosewill-rsv-l4500u': 'Rosewill RSV-L4500U 4U (15-bay)',
'zimaboard-832': 'IceWhale ZimaBoard 832 (shelf gear)',
'zimaboard-832-cx3': 'IceWhale ZimaBoard 832 + ConnectX-3 SFP+ in PCIe (shelf gear)',
+ 'resting': 'No shelf: gear resting on what is below (for a shelf item)',
'server-1u': 'Generic 1U server',
'server-2u': 'Generic 2U server',
'server-4u': 'Generic 4U server',
diff --git a/static/faceplates.js b/static/faceplates.js
index fc33088..3f04150 100644
--- a/static/faceplates.js
+++ b/static/faceplates.js
@@ -595,6 +595,15 @@ function faceplateLegendHtml(spec, marks) {
// ctx: { swName, portMap, outlets, online, titleFor(port, idx, role), cutsheetFor?(idx) -> {mark, text},
// rack? (draw only the front, spec.rackView, as mounted in a rack) }
function renderFaceplate(model, ctx) {
+ const { svg, marks } = faceplateSvg(model, ctx);
+ return `
+
${svg}
+
${faceplateLegendHtml(FACEPLATES[model], marks)}`;
+}
+
+// The faceplate's SVG on its own, for drawing inside another picture (a shelf). `attrs` adds
+// attributes to the `;
+ return { svg, marks };
}
// Where an fp-scroll panel should start: its port area
diff --git a/static/fronts.js b/static/fronts.js
index c068120..f7a38b6 100644
--- a/static/fronts.js
+++ b/static/fronts.js
@@ -1,5 +1,5 @@
'use strict';
-/* exported RACK_FRONTS, CHASSIS_FACEPLATES, rackFrontSvg, shelfSvg, SHELF_GEAR */
+/* 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.
//
@@ -256,42 +256,70 @@ const SHELF_GEAR = {
},
};
+// 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 is drawn to its real width; anything else is a labelled box.
-function shelfSvg(item, esc) {
+// 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 floor = H - 20;
+ 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 w = gear ? gear.mm * perMm : 150;
- const k = gear ? w / gear.width : 1;
- const h = gear ? gear.height * k : 58;
+ 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 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)}
+ 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}
-
-
- ${esc(c.name)}
- `;
+ ${desk ? '' : ``}
+
+ ${esc(c.name)}
+ ${desk ? '' : ''}`;
x += w + 26;
return out;
}).join('');
+ const tray = resting ? '' : `${ears(W, H, 'rf-ear-dark')}
+
+
+ `;
return `${esc(item.name)}
- ${ears(W, H, 'rf-ear-dark')}
-
-
-
- ${things || `empty shelf`}
+ ${tray}
+ ${things || `${resting ? 'nothing here' : 'empty shelf'}`}
${esc(item.name)}`;
}
diff --git a/static/rack.js b/static/rack.js
index 55aa62e..0453185 100644
--- a/static/rack.js
+++ b/static/rack.js
@@ -1,5 +1,5 @@
'use strict';
-/* global FACEPLATES, renderFaceplate, RACK_CAN_EDIT, RACK_FRONTS, CHASSIS_FACEPLATES, rackFrontSvg, shelfSvg */
+/* global FACEPLATES, renderFaceplate, faceplateSvg, RACK_CAN_EDIT, RACK_FRONTS, CHASSIS_FACEPLATES, rackFrontSvg, shelfSvg */
// 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.
@@ -145,6 +145,15 @@ function plateHtml(item) {
`;
}
+// Live faceplates for UniFi desk gear sitting on a shelf (a Flex Mini on top of the NVR)
+const shelfFaceplates = {
+ spec: model => FACEPLATES[model],
+ svg: (c, attrs) => faceplateSvg(c.model, {
+ swName: c.device || c.name, portMap: portMapOf(c.ports), outlets: [], online: c.status === 'up',
+ titleFor: (port, idx) => portTitle(port, idx),
+ }, attrs).svg,
+};
+
function backHtml(item) {
// A full-depth item seen from the rear: its back, not its faceplate
return `
@@ -161,7 +170,7 @@ function itemHtml(item) {
else if (item.chassis && RACK_FRONTS[item.chassis]) body = rackFrontSvg(item.chassis, item);
else if (item.kind === 'patch_panel' && item.chassis === 'uacc-patch-24') body = uaccPanelSvg(item);
else if (item.kind === 'patch_panel') body = panelSvg(item);
- else if (item.kind === 'shelf') body = shelfSvg(item, esc);
+ else if (item.kind === 'shelf') body = shelfSvg(item, esc, shelfFaceplates);
else body = plateHtml(item);
return `