Sites: rooms and racks match whatever their case
CI / agent (push) Successful in 3s
CI / site (push) Successful in 3s
CI / go (push) Successful in 44s

A rack typed in by hand (lab / pxe hall / R1) and the same rack from
NetBox (Lab / PXE Hall / R1) showed as two rooms and two racks. Site
views and the rack front match room and rack names ignoring case, as the
NetBox device sync does; one rack is shown, NetBox's spelling and a
placed one first. Found on the lab region.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Ilker ManapandClaude Opus 5.5 committed 2026-11-01 00:25:14 +01:00
1 parent 148c602b82
commit 54bda28b82
2 files changed
+25 -9

No files matched your search

+2 -1
View File
@@ -605,7 +605,8 @@ function wireJobs() { $('#refresh').onclick = () => refresh(true); }
function rackElev(rk) {
const keyOf = (b, rm, n) => `${b}|${rm}|${n}`;
const placed = S.machines.filter(m => m.location && m.location.rack);
const ms = placed.filter(m => (m.location.building || '') === rk.building && (m.location.room || '') === rk.room && m.location.rack === rk.name);
const lo = x => String(x || '').toLowerCase(); // a rack typed in by hand and the same from NetBox may differ in case
const ms = placed.filter(m => lo(m.location.building) === lo(rk.building) && lo(m.location.room) === lo(rk.room) && lo(m.location.rack) === lo(rk.name));
// blocks of units: a server spans its height (size from the firmware or the
// model, ?U when unknown); sleds of one multi-node chassis share one block
const H = rk.height || 42, noU = [], blocks = [];
+23 -8
View File
@@ -23,12 +23,28 @@ function siteList() {
return out;
}
// siteRacks: a site's racks, one per room and name whatever their case (a
// rack typed in by hand and the same rack from NetBox), NetBox's spelling and
// a placed one preferred
function siteRacks(site) {
const by = new Map();
for (const r of S.racks.filter(x => inSite(site, x.building))) {
const k = lc(r.room) + '|' + lc(r.name), cur = by.get(k);
const score = x => (x.x != null ? 2 : 0) + (S.rooms.some(rm => rm.name === x.room && inSite(site, rm.building)) ? 1 : 0);
if (!cur || score(r) > score(cur)) by.set(k, r);
}
return [...by.values()];
}
const sameRoom = (a, b) => lc(a) === lc(b);
function siteStats(site) {
const ms = S.machines.filter(m => m.location && inSite(site, m.location.building));
const racks = S.racks.filter(r => inSite(site, r.building));
const racks = siteRacks(site);
const by = {}; for (const m of ms) by[stateClass(m.state)] = (by[stateClass(m.state)] || 0) + 1;
const worst = ['bad', 'warn', 'ok', 'idle'].find(k => by[k]) || 'idle';
const rooms = [...new Set([...racks.map(r => r.room), ...S.rooms.filter(r => inSite(site, r.building)).map(r => r.name), ...ms.map(m => m.location.room || '')])].sort();
const named = new Map(); // one room per name whatever its case; NetBox's (S.rooms) spelling first
for (const n of [...S.rooms.filter(r => inSite(site, r.building)).map(r => r.name), ...racks.map(r => r.room), ...ms.map(m => m.location.room || '')]) if (!named.has(lc(n))) named.set(lc(n), n);
const rooms = [...named.values()].sort();
return { ms, racks, by, worst, rooms };
}
@@ -118,7 +134,7 @@ function pageSite(name) {
if (!site) return `<div class="empty">No site ${esc(name)}. <a href="#/sites">Sites</a></div>`;
const st = siteStats(site);
const cards = st.rooms.map(rm => {
const racks = st.racks.filter(r => r.room === rm), ms = st.ms.filter(m => (m.location.room || '') === rm);
const racks = st.racks.filter(r => sameRoom(r.room, rm)), ms = st.ms.filter(m => sameRoom(m.location.room || '', rm));
const bad = ms.filter(m => stateClass(m.state) === 'bad').length;
return `<a class="card room-card" href="#/sites/${encodeURIComponent(site.name)}/${encodeURIComponent(rm || '-')}"><h3>${esc(rm || '(no room)')}</h3>
<div class="room-mini">${floorSVG(site, rm, { mini: true })}</div>
@@ -141,7 +157,7 @@ function wireSite(name) {
// automatic places in rows of ten below the placed ones, fronts facing
// alternate aisles (cold aisle between rows 0/1, 2/3, ...).
function racksOnFloor(site, room) {
const racks = S.racks.filter(r => inSite(site, r.building) && r.room === room)
const racks = siteRacks(site).filter(r => sameRoom(r.room, room))
.map(r => ({ ...r, auto: r.x == null, facing: r.facing || 'N' }));
const placed = racks.filter(r => !r.auto);
let y0 = placed.length ? Math.max(...placed.map(r => r.y + footprint(r).h)) + 2 : 1;
@@ -154,7 +170,7 @@ function racksOnFloor(site, room) {
const footprint = r => (r.facing === 'E' || r.facing === 'W') ? { w: 2, h: 1 } : { w: 1, h: 2 };
function rackState(site, r) {
const ms = S.machines.filter(m => m.location && inSite(site, m.location.building) && (m.location.room || '') === r.room && m.location.rack === r.name);
const ms = S.machines.filter(m => m.location && inSite(site, m.location.building) && sameRoom(m.location.room || '', r.room) && lc(m.location.rack) === lc(r.name));
const used = ms.reduce((a, m) => a + (m.size && m.size.node ? 0 : Math.max(1, (m.size && m.size.u) || 1)), 0) +
new Set(ms.filter(m => m.size && m.size.node).map(m => m.location.unit)).size * 2;
const worst = ['bad', 'warn', 'ok', 'idle'].find(k => ms.some(m => stateClass(m.state) === k)) || 'vacant';
@@ -164,7 +180,7 @@ function rackState(site, r) {
// floorSVG draws the room; opt.mini: a thumbnail for the site page
function floorSVG(site, room, opt = {}) {
const racks = SITE.edit && !opt.mini ? SITE.edit.racks : racksOnFloor(site, room);
const rm = S.rooms.find(r => inSite(site, r.building) && r.name === room) || {};
const rm = S.rooms.find(r => inSite(site, r.building) && sameRoom(r.name, room)) || {};
const T = opt.mini ? 9 : 40;
const W = Math.max(rm.width || 0, ...racks.map(r => r.x + footprint(r).w + 1), 8), D = Math.max(rm.depth || 0, ...racks.map(r => r.y + footprint(r).h + 1), 6);
const o = [`<svg class="floor${opt.mini ? ' mini' : ''}" viewBox="0 0 ${W * T} ${D * T}" width="${W * T}" height="${D * T}"${opt.mini ? ' style="max-width:100%;height:auto"' : ''}>`];
@@ -218,8 +234,7 @@ function wireRoom(siteName, roomArg) {
const c = $('#fl-cancel'); if (c) c.onclick = () => { SITE.edit = null; render(); };
const save = async (auto) => {
const body = SITE.edit.racks.map(r => {
const orig = S.racks.find(x => x.name === r.name && x.room === room && inSite(site, x.building));
return { building: orig.building, room, name: r.name, x: auto ? null : r.x, y: auto ? null : r.y, facing: auto ? '' : r.facing };
return { building: r.building, room: r.room, name: r.name, x: auto ? null : r.x, y: auto ? null : r.y, facing: auto ? '' : r.facing };
});
try { await api('POST', '/api/v1/racks/positions', body); SITE.edit = null; toast(auto ? 'racks laid out automatically again' : `layout of ${room || site.name} saved`); refresh(true); } catch (err) { toast(err.message, true); }
};