diff --git a/README.md b/README.md
index 43906fa..b41cd1f 100644
--- a/README.md
+++ b/README.md
@@ -136,7 +136,8 @@ The rack front, drawn from the rack units set on cutsheet items, top first.
- **Drawn fronts**: gear without a live faceplate can pick a **Front drawing** on its cutsheet item:
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
+ door), the UniFi blank 24-port keystone patch panel (UACC-Rack-Panel-Patch-Blank-24: keystone jacks
+ appear where cables are documented), 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.
diff --git a/cutsheet.py b/cutsheet.py
index 645b121..cd9be22 100644
--- a/cutsheet.py
+++ b/cutsheet.py
@@ -29,6 +29,7 @@ RACK_UNITS_MAX = 60
# Drawn fronts for gear without a live faceplate (keys of RACK_FRONTS in static/fronts.js)
CHASSIS = {
'unvr-pro': 'UniFi NVR Pro (UNVR Pro)',
+ 'uacc-patch-24': 'UniFi Patch Panel Blank 24 (UACC-Rack-Panel-Patch-Blank-24)',
'sliger-cx4712': 'Sliger CX4712 4U (10-bay)',
'rosewill-rsv-l4500u': 'Rosewill RSV-L4500U 4U (15-bay)',
'server-1u': 'Generic 1U server',
diff --git a/static/rack.js b/static/rack.js
index d9b0130..d42e686 100644
--- a/static/rack.js
+++ b/static/rack.js
@@ -105,6 +105,48 @@ function shelfHtml(item) {
`;
}
+// Ubiquiti UACC-Rack-Panel-Patch-Blank-24, laid out from the CC0 NetBox devicetype-library image
+// (elevation-images/Ubiquiti/ubiquiti-uacc-rack-panel-patch-blank-24.front.png): a grey steel face,
+// darker ears with oval holes, 24 square keystone openings in one row with the cable-support bar
+// behind them, and a narrow cable slot at each end. A port with a documented cable gets a keystone jack.
+function uaccPanelSvg(item) {
+ const W = 1178, H = 108, x0 = 107, pitch = 40.5, ow = 34, oy = 36, oh = 40;
+ const count = Math.min(item.port_count || 24, 24);
+ const ports = [];
+ for (let n = 1; n <= count; n++) {
+ const x = x0 + (n - 1) * pitch;
+ const m = item.ports[String(n)];
+ const label = item.labels[String(n)] || '';
+ const title = [`Port ${n}`, label && `"${label}"`, m ? m.text : 'not on the cutsheet'].filter(Boolean).join(' · ');
+ const attrs = m ? ` data-run="${m.run.join(' ')}" data-cable="${m.cable_id}" tabindex="0" role="link"` : '';
+ const jack = m ? `
+ ` : '';
+ ports.push(`
+ ${esc(title)}
+
+
+ ${jack}
+
+ ${n}
+ ${label ? `${esc(label.length > 7 ? `${label.slice(0, 6)}…` : label)}` : ''}
+
+ `);
+ }
+ const hole = (x, y) => ``;
+ return ``;
+}
+
function plateHtml(item) {
const unlinked = item.kind === 'device' && !item.linked;
const sub = [item.device !== item.name ? item.device : '', KIND_TEXT[item.kind] || item.kind, item.location,
@@ -129,6 +171,7 @@ function itemHtml(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' && item.chassis === 'uacc-patch-24') body = uaccPanelSvg(item);
else if (item.kind === 'patch_panel') body = panelSvg(item);
else if (item.kind === 'shelf') body = shelfHtml(item);
else body = plateHtml(item);
diff --git a/static/style.css b/static/style.css
index 75eca17..d3362af 100644
--- a/static/style.css
+++ b/static/style.css
@@ -1567,3 +1567,20 @@ body.fp-marks-off .fp-svg .fp-mark { display: none; }
.rf-unvr-dot { fill: #ffffff; stroke: #c4c6ca; }
.rf-screen { fill: #0f1726; }
.rf-screen-ring { fill: none; stroke-width: 3; }
+
+/* Ubiquiti UACC-Rack-Panel-Patch-Blank-24 (static/rack.js uaccPanelSvg) */
+.ua-ear { fill: #6f7277; }
+.ua-hole { fill: #15181c; }
+.ua-face { fill: #8e9095; stroke: #6c6f74; }
+.ua-lip { fill: #a6a8ad; }
+.ua-open { fill: #17191d; }
+.ua-bar { fill: #9a9ca1; }
+.ua-keystone { fill: #101113; stroke: #2b2d31; }
+.ua-keystone-hole { fill: #2f3236; }
+.ua-num { fill: #2b2d31; font-size: 9px; font-family: Arial, Helvetica, sans-serif; }
+.ua-label { fill: #1f2124; font-size: 8.5px; font-weight: 700; font-family: Arial, Helvetica, sans-serif; }
+.ua-mark { fill: none; stroke: transparent; }
+.rack-pp-ok .ua-mark { stroke: #00b860; stroke-width: 2.5; }
+.rack-pp-bad .ua-mark { stroke: #e0312b; stroke-width: 3; }
+.rack-pp-doc .ua-mark { stroke: #4c5662; stroke-width: 2; }
+.rack-pp-port.rack-hl .ua-mark { fill: rgba(0,212,255,.25); stroke: #00b8e6; stroke-width: 3.5; }
diff --git a/tests/test_cutsheet.py b/tests/test_cutsheet.py
index ec16f20..0023c38 100644
--- a/tests/test_cutsheet.py
+++ b/tests/test_cutsheet.py
@@ -367,4 +367,5 @@ class TestChassis:
cs.validate_item({'name': 'x', 'chassis': 'tardis'})
def test_every_drawing_has_a_label(self):
- assert all(cs.CHASSIS.values()) and 'sliger-cx4712' in cs.CHASSIS and 'rosewill-rsv-l4500u' in cs.CHASSIS
+ assert all(cs.CHASSIS.values())
+ assert {'sliger-cx4712', 'rosewill-rsv-l4500u', 'unvr-pro', 'uacc-patch-24'} <= set(cs.CHASSIS)