3D rooms: rail posts, solid servers of unknown height, a word when no cabling is known
CI / agent (push) Successful in 3s
CI / site (push) Successful in 3s
CI / go (push) Successful in 44s

Through the see-through cabinet both rail faces showed as two grey
boards, and a server of unknown height (drawn 1U, see-through) read as
an empty shelf. Racks now have their four rail posts instead of panels;
every server is solid, one of unknown height carries an amber outline
(the elevation's ?U). A room whose machines' LLDP saw no switch and
that NetBox has no cables for says so in the side panel, and how to
enter cables by hand.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Ilker ManapandClaude Opus 5.5 committed 2026-11-01 14:56:16 +01:00
1 parent 1d41a83f27
commit 6a7b158644
2 files changed
+15 -11

No files matched your search

+12 -9
View File
@@ -90,7 +90,7 @@ function build(site, room, racks, blocks) {
const mat = (k, make) => mats[k] || (mats[k] = keep(make()));
const stateMat = (cls, known) => mat(cls + known, () => {
const c = cv({ ok: '--ok', warn: '--warn', bad: '--danger' }[cls] || '--muted');
return new T.MeshStandardMaterial({ color: c, emissive: c, emissiveIntensity: 0.25, roughness: 0.55, transparent: !known, opacity: known ? 1 : 0.55 });
return new T.MeshStandardMaterial({ color: c, emissive: c, emissiveIntensity: 0.25, roughness: 0.55 });
});
const lineMat = name => mat('l' + name, () => new T.LineBasicMaterial({ color: cv(name) }));
const box = (w, h, d) => keep(new T.BoxGeometry(w, h, d));
@@ -127,11 +127,10 @@ function build(site, room, racks, blocks) {
body.userData = { rack: r.name, tip: `${r.name} · ${st.ms.length} machines · ${st.used}/${H}U used${r.auto ? ' · not placed yet' : ''}` };
const edges = new T.LineSegments(keep(new T.EdgesGeometry(body.geometry)), lineMat(sel ? '--accent' : '--muted'));
edges.position.copy(body.position);
// the rails' face, lighter than the cabinet: where the front is and what is empty
const face = new T.Mesh(box(RAIL, H * U_M, 0.004), mat('face', () => new T.MeshStandardMaterial({ color: 0x59626d, roughness: 0.9, transparent: true, opacity: 0.55, depthWrite: false })));
face.position.set(0, PLINTH + H * U_M / 2, -TILE + 0.008);
const back = face.clone(); back.position.z = TILE - 0.008;
g.add(body, edges, face, back); R3.pick.push(body);
// the four rail posts, front and back: where a unit is, without a panel to look through
const post = mat('post', () => new T.MeshStandardMaterial({ color: 0x4b535e, roughness: 0.6, metalness: 0.5 }));
const posts = [-1, 1].flatMap(sx => [-1, 1].map(sz => { const p = new T.Mesh(box(0.016, H * U_M, 0.016), post); p.position.set(sx * (RAIL / 2 + 0.01), PLINTH + H * U_M / 2, sz * (TILE - 0.014)); return p; }));
g.add(body, edges, ...posts); R3.pick.push(body);
g.updateMatrixWorld(true);
rackAt[lc(r.name)] = { g, H, h, r, used: blocks[i].covered };
for (const b of Object.values(blocks[i].top)) {
@@ -154,17 +153,21 @@ function build(site, room, racks, blocks) {
if (front) { // the drawn front on the face towards the aisle (-z), the cabinet colour elsewhere
let tex = front.tex;
if (n > 1) { tex = keep(front.tex.clone()); tex.repeat.set(sw / RAIL, 1); tex.offset.set((cuts[k] + RAIL / 2) / RAIL, 0); tex.needsUpdate = true; }
const face = keep(new T.MeshStandardMaterial({ map: tex, emissiveMap: tex, emissive: 0xffffff, emissiveIntensity: 0.3, roughness: 0.7, transparent: !b.known, opacity: b.known ? 1 : 0.55 }));
const side = mat('side' + b.known, () => new T.MeshStandardMaterial({ color: 0x1c1e22, roughness: 0.8, transparent: !b.known, opacity: b.known ? 1 : 0.55 }));
const face = keep(new T.MeshStandardMaterial({ map: tex, emissiveMap: tex, emissive: 0xffffff, emissiveIntensity: 0.3, roughness: 0.7 }));
const side = mat('side', () => new T.MeshStandardMaterial({ color: 0x1c1e22, roughness: 0.8 }));
look = [side, side, side, side, side, face];
}
const s = new T.Mesh(box(sw - (front ? 0.001 : 0.006), hh, 0.03), look);
s.position.set(-(cuts[k] + cuts[k + 1]) / 2, y, -TILE - 0.006); // seen from the aisle, local +x is on the left
s.userData = { rack: r.name, id: m.id, tip: `${nameOf(m)} · ${m.state} · U${b.unit}${span > 1 ? '–' + (b.unit + span - 1) : ''}${b.known ? '' : ' (height unknown)'} · ${[m.manufacturer, m.product].filter(Boolean).join(' ')}` };
g.add(s); R3.pick.push(s);
if (!b.known) { // height unknown (drawn as 1U): an amber outline, as "?U" in the elevation
const o = new T.LineSegments(keep(new T.EdgesGeometry(s.geometry)), mat('unknownU', () => new T.LineBasicMaterial({ color: new T.Color(colorOf.warn) })));
o.position.copy(s.position); o.scale.set(1.01, 1.08, 1.2); g.add(o);
}
});
// the chassis between front and back: solid and dark, apart from the cabinet
const chassis = new T.Mesh(box(RAIL - 0.004, hh - 0.002, 2 * TILE - 0.07), mat('chassis' + b.known, () => new T.MeshStandardMaterial({ color: 0x202328, roughness: 0.7, metalness: 0.4, transparent: !b.known, opacity: b.known ? 1 : 0.5 })));
const chassis = new T.Mesh(box(RAIL - 0.004, hh - 0.002, 2 * TILE - 0.07), mat('chassis', () => new T.MeshStandardMaterial({ color: 0x202328, roughness: 0.7, metalness: 0.4 })));
chassis.position.set(0, y, -0.006);
g.add(chassis);
// the back: PSUs, the BMC port and the network cards with their ports
+3 -2
View File
@@ -218,7 +218,7 @@ function pageRoom(siteName, roomArg) {
${SITE.edit ? `<div class="actions" style="margin-bottom:8px"><button class="btn sm" id="rk-rotate">Rotate</button></div>` : ''}
${rackElev(selRack)}${cablingTable(site, selRack)}</div>`
: `<div class="card floor-side"><span class="muted">Click a rack for its front.${autoN ? `<br><br>${autoN} rack(s) have no place yet and are laid out automatically (${v3 ? 'see-through' : 'dashed'}): Edit layout to place them.` : ''}</span>${v3 ? switchTable() : ''}</div>`;
const legend = v3 ? `<span>drag: turn · right or shift drag: move · wheel: zoom · double click: go to a rack · a click on a selected rack\'s server opens it · see-through: height unknown${frontView.get() === 'real' ? ' · fronts: the drive bays of the model, the strip on the left ear is the state' : ''}${cablesView.get() ? ' · the cables run at the back: Turn around' : ''}</span>`
const legend = v3 ? `<span>drag: turn · right or shift drag: move · wheel: zoom · double click: go to a rack · a click on a selected rack\'s server opens it · amber outline: height unknown (rack_units)${frontView.get() === 'real' ? ' · fronts: the drive bays of the model, the strip on the left ear is the state' : ''}${cablesView.get() ? ' · the cables run at the back: Turn around' : ''}</span>`
: `<span><b class="lg vacant"></b>empty</span><span>thick edge: the front · bar: units used${SITE.edit ? ' · drag to move, select and Rotate' : ''}</span>`;
return `<div class="page-head"><h1><a href="#/sites" class="muted" style="font-weight:400">Sites</a> / <a href="#/sites/${encodeURIComponent(site.name)}" class="muted" style="font-weight:400">${esc(site.name)}</a> / ${esc(room || '(no room)')}</h1>
<span class="sub">${racks.length} racks · floor tiles of 600 mm</span>
@@ -333,7 +333,8 @@ async function editCable(id, port) {
// switchTable: the switches this room's cables lead to, where they stand
function switchTable() {
const sws = (typeof R3 !== 'undefined' && R3.switches) || []; if (!sws.length) return '';
const sws = (typeof R3 !== 'undefined' && R3.switches) || [];
if (!sws.length) return '<p class="muted" style="margin-top:12px">No cabling known in this room: the machines\' LLDP saw no switch, and NetBox records no cables for them. Cables can be entered per port (select a rack, ✎ in its cabling list).</p>';
const where = s => s.away ? `in ${esc(s.away)}` : s.at ? `${esc(rackLabel(s.at.rack))} U${s.at.unit}${s.at.estimated ? ' <span class="warn">estimated</span>' : ` <span class="muted">${esc(s.source)}</span>`}` : '';
return `<h3 style="margin-top:12px">Switches</h3><div class="sw-list">${sws.map(s => `<div class="sw-row"><b class="sw-dot" style="background:${esc(s.color)}"></b>
<div><div><b>${esc(s.name)}</b>${s.role === 'oob' ? ' <span class="pill tag" title="out-of-band: BMC ports">OOB</span>' : ''}${s.model ? ` <span class="muted">${esc(s.model)}</span>` : ''} ${s.source === 'netbox' ? nbLink('/dcim/devices/' + nbQ({ name: s.name }), '↗') : ''}</div><div class="muted">${where(s)} · ${s.here} cables</div></div>