diff --git a/cmd/rollcall/web/room3d.js b/cmd/rollcall/web/room3d.js index ef81844..d6960a9 100644 --- a/cmd/rollcall/web/room3d.js +++ b/cmd/rollcall/web/room3d.js @@ -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 diff --git a/cmd/rollcall/web/sites.js b/cmd/rollcall/web/sites.js index 9d5dad0..e1fecec 100644 --- a/cmd/rollcall/web/sites.js +++ b/cmd/rollcall/web/sites.js @@ -218,7 +218,7 @@ function pageRoom(siteName, roomArg) { ${SITE.edit ? `
` : ''} ${rackElev(selRack)}${cablingTable(site, selRack)}` : `
Click a rack for its front.${autoN ? `

${autoN} rack(s) have no place yet and are laid out automatically (${v3 ? 'see-through' : 'dashed'}): Edit layout to place them.` : ''}
${v3 ? switchTable() : ''}
`; - const legend = v3 ? `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' : ''}` + const legend = v3 ? `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' : ''}` : `emptythick edge: the front · bar: units used${SITE.edit ? ' · drag to move, select and Rotate' : ''}`; return `

Sites / ${esc(site.name)} / ${esc(room || '(no room)')}

${racks.length} racks · floor tiles of 600 mm @@ -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 '

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).

'; const where = s => s.away ? `in ${esc(s.away)}` : s.at ? `${esc(rackLabel(s.at.rack))} U${s.at.unit}${s.at.estimated ? ' estimated' : ` ${esc(s.source)}`}` : ''; return `

Switches

${sws.map(s => `
${esc(s.name)}${s.role === 'oob' ? ' OOB' : ''}${s.model ? ` ${esc(s.model)}` : ''} ${s.source === 'netbox' ? nbLink('/dcim/devices/' + nbQ({ name: s.name }), '↗') : ''}
${where(s)} · ${s.here} cables