feat(rack): draw shelf-mounted tower heights in the elevation
All checks were successful
Build docs site / build (push) Successful in 50s
Build slides / build (push) Successful in 1m11s

Add an optional `chassis_u` field for shelf-mounted devices (their height
in U where they stand on the shelf) and render it:
- gen_rack draws each tower chassis_u U's tall, rising above the 1U shelf
  line; rail-mounted devices now paint on top so a PDU within a tower's
  span (e.g. pdu03 over srv05/06) stays visible
- occupancy table shows each tower's real U-span (e.g. srv01 U37-U46)
- validate_item checks chassis_u is a positive integer; absent chassis_u
  renders byte-identically to before
- set chassis_u for srv01-07 (10/8/6/6/7/7/6U); document the field in the
  editing guide; regenerate rack01 artifacts

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
sjat 2026-06-30 22:11:19 +02:00
parent 4dc975062a
commit 4153c8d1d9
14 changed files with 228 additions and 148 deletions

View file

@ -118,12 +118,18 @@ Only files that declare a `rack:` appear in a rack elevation. The rack is 48U.
mounted_on: shf01 # an existing kind:shelf in the same rack
shelf_face: front # front | rear
shelf_slot: 2 # integer ≥1
chassis_u: 6 # optional: device height in U (how tall it stands)
# no rack_u / u_height / rack_face
```
The shelf itself must be placed (have `rack_u` + `u_height`). Two devices
can't share the same `(shelf, face, slot)`.
`chassis_u` is optional. Shelves are typically 1U trays; a device sitting on
one (e.g. a tower PC) stands `chassis_u` U's tall, rising above the shelf
line in the elevation without consuming those rack U's (so rail-mounted gear
may still occupy them). Omit it and the device just fills the shelf block.
### Power feeds
A device draws power by listing feeds. Each feed must point at a real `kind:pdu`

View file

@ -13,5 +13,5 @@ cluster: tappaas
- 1U full-depth tray at U46. Tower PCs stand on it and rise above U46; they are
not rail-mounted, so the U's above are not consumed in the rack model.
- Front: srv01 (stands ~U37U46), srv02 (~U39U46).
- Rear: srv03 (~U40U46).
- Front: srv01 (10U, U37U46), srv02 (8U, U39U46).
- Rear: srv03 (6U, U41U46).

View file

@ -13,5 +13,5 @@ rack_face: both
- 1U full-depth tray at U35. Tower PCs stand on it and rise above U35; they are
not rail-mounted, so the U's above are not consumed in the rack model
(e.g. pdu03 sits at U34, just above this shelf).
- Front: srv07 (stands ~U29U35), srv04 (~U27U35).
- Rear: srv05 (~U27U35), srv06 (~U27U35).
- Front: srv07 (6U, U30U35), srv04 (6U, U30U35).
- Rear: srv05 (7U, U29U35), srv06 (7U, U29U35).

View file

@ -14,6 +14,7 @@ rack: rack01
mounted_on: shf01
shelf_face: front
shelf_slot: 1
chassis_u: 10
power:
- { pdu: pdu01, outlet: 1 }
links:

View file

@ -16,6 +16,7 @@ rack: rack01
mounted_on: shf01
shelf_face: front
shelf_slot: 2
chassis_u: 8
power:
- { pdu: pdu01, outlet: 2 }
links:

View file

@ -16,6 +16,7 @@ rack: rack01
mounted_on: shf01
shelf_face: rear
shelf_slot: 1
chassis_u: 6
power:
- { pdu: pdu01, outlet: 3 }
links:

View file

@ -14,6 +14,7 @@ rack: rack01
mounted_on: shf02
shelf_face: front
shelf_slot: 2
chassis_u: 6
power:
- { pdu: pdu01, outlet: 4 }
links:

View file

@ -14,6 +14,7 @@ rack: rack01
mounted_on: shf02
shelf_face: rear
shelf_slot: 1
chassis_u: 7
power:
- { pdu: pdu01, outlet: 5 }
links:

View file

@ -14,6 +14,7 @@ rack: rack01
mounted_on: shf02
shelf_face: rear
shelf_slot: 2
chassis_u: 7
power:
- { pdu: pdu01, outlet: 1 }
- { pdu: pdu02, outlet: 1 }

View file

@ -14,6 +14,7 @@ rack: rack01
mounted_on: shf02
shelf_face: front
shelf_slot: 1
chassis_u: 6
power:
- { pdu: pdu01, outlet: 1 }
- { pdu: pdu02, outlet: 1 }

View file

@ -197,6 +197,65 @@
<text x="576" y="994" text-anchor="start" fill="#999">48</text>
<rect x="42" y="40" width="240" height="960" fill="none" stroke="#999" stroke-width="1.5"/>
<rect x="332" y="40" width="240" height="960" fill="none" stroke="#999" stroke-width="1.5"/>
<a href="/hardware/srv01/">
<title>srv01 · server · in-use · cluster: tappaas · shf01/front/slot 1</title>
<rect x="43" y="761" width="118" height="192" rx="3" fill="#4c78a8" stroke="#333333" stroke-width="1.5"/>
<text x="102" y="861" text-anchor="middle" fill="#ffffff">srv01</text>
</a>
<a href="/hardware/srv02/">
<title>srv02 · server · staging · cluster: tappaas · shf01/front/slot 2</title>
<rect x="163" y="801" width="118" height="152" rx="3" fill="#4c78a8" stroke="#333333" stroke-width="1.5" stroke-dasharray="4 2"/>
<text x="222" y="881" text-anchor="middle" fill="#ffffff">srv02</text>
</a>
<a href="/hardware/srv03/">
<title>srv03 · server · staging · cluster: tappaas · shf01/rear/slot 1</title>
<rect x="333" y="841" width="238" height="112" rx="3" fill="#4c78a8" stroke="#333333" stroke-width="1.5" stroke-dasharray="4 2"/>
<text x="452" y="901" text-anchor="middle" fill="#ffffff">srv03</text>
</a>
<a href="/hardware/shf01/">
<title>shf01 · shelf · in-use · cluster: tappaas · U46</title>
<rect x="42" y="954" width="240" height="6" fill="#bab0ac" stroke="#333"/>
<rect x="332" y="954" width="240" height="6" fill="#bab0ac" stroke="#333"/>
<text x="162" y="959" text-anchor="middle" fill="#333" font-size="9">shf01</text>
</a>
<a href="/hardware/srv07/">
<title>srv07 · server · staging · cluster: tappaas · shf02/front/slot 1</title>
<rect x="43" y="621" width="118" height="112" rx="3" fill="#4c78a8" stroke="#333333" stroke-width="1.5" stroke-dasharray="4 2"/>
<text x="102" y="681" text-anchor="middle" fill="#ffffff">srv07</text>
</a>
<a href="/hardware/srv04/">
<title>srv04 · server · staging · cluster: — · shf02/front/slot 2</title>
<rect x="163" y="621" width="118" height="112" rx="3" fill="#4c78a8" stroke="#333333" stroke-width="1.5" stroke-dasharray="4 2"/>
<text x="222" y="681" text-anchor="middle" fill="#ffffff">srv04</text>
</a>
<a href="/hardware/srv05/">
<title>srv05 · server · staging · cluster: — · shf02/rear/slot 1</title>
<rect x="333" y="601" width="118" height="132" rx="3" fill="#4c78a8" stroke="#333333" stroke-width="1.5" stroke-dasharray="4 2"/>
<text x="392" y="671" text-anchor="middle" fill="#ffffff">srv05</text>
</a>
<a href="/hardware/srv06/">
<title>srv06 · server · staging · cluster: tappaas · shf02/rear/slot 2</title>
<rect x="453" y="601" width="118" height="132" rx="3" fill="#4c78a8" stroke="#333333" stroke-width="1.5" stroke-dasharray="4 2"/>
<text x="512" y="671" text-anchor="middle" fill="#ffffff">srv06</text>
</a>
<a href="/hardware/shf02/">
<title>shf02 · shelf · in-use · cluster: — · U35</title>
<rect x="42" y="734" width="240" height="6" fill="#bab0ac" stroke="#333"/>
<rect x="332" y="734" width="240" height="6" fill="#bab0ac" stroke="#333"/>
<text x="162" y="739" text-anchor="middle" fill="#333" font-size="9">shf02</text>
</a>
<a href="/hardware/shf03/">
<title>shf03 · shelf · in-use · cluster: — · U21</title>
<rect x="42" y="454" width="240" height="6" fill="#bab0ac" stroke="#333"/>
<rect x="332" y="454" width="240" height="6" fill="#bab0ac" stroke="#333"/>
<text x="162" y="459" text-anchor="middle" fill="#333" font-size="9">shf03</text>
</a>
<a href="/hardware/shf04/">
<title>shf04 · shelf · in-use · cluster: — · U21</title>
<rect x="42" y="454" width="240" height="6" fill="#bab0ac" stroke="#333"/>
<rect x="332" y="454" width="240" height="6" fill="#bab0ac" stroke="#333"/>
<text x="162" y="459" text-anchor="middle" fill="#333" font-size="9">shf04</text>
</a>
<a href="/hardware/pdu01/">
<title>pdu01 · pdu · in-use · cluster: — · U1</title>
<rect x="333" y="41" width="238" height="18" rx="3" fill="#e15759" stroke="#333333" stroke-width="1.5"/>
@ -272,65 +331,6 @@
<rect x="333" y="701" width="238" height="18" rx="3" fill="#e15759" stroke="#333333" stroke-width="1.5"/>
<text x="452" y="714" text-anchor="middle" fill="#ffffff">pdu03 (U34)</text>
</a>
<a href="/hardware/srv01/">
<title>srv01 · server · in-use · cluster: tappaas · shf01/front/slot 1</title>
<rect x="43" y="941" width="118" height="12" rx="3" fill="#4c78a8" stroke="#333333" stroke-width="1.5"/>
<text x="102" y="951" text-anchor="middle" fill="#ffffff">srv01</text>
</a>
<a href="/hardware/srv02/">
<title>srv02 · server · staging · cluster: tappaas · shf01/front/slot 2</title>
<rect x="163" y="941" width="118" height="12" rx="3" fill="#4c78a8" stroke="#333333" stroke-width="1.5" stroke-dasharray="4 2"/>
<text x="222" y="951" text-anchor="middle" fill="#ffffff">srv02</text>
</a>
<a href="/hardware/srv03/">
<title>srv03 · server · staging · cluster: tappaas · shf01/rear/slot 1</title>
<rect x="333" y="941" width="238" height="12" rx="3" fill="#4c78a8" stroke="#333333" stroke-width="1.5" stroke-dasharray="4 2"/>
<text x="452" y="951" text-anchor="middle" fill="#ffffff">srv03</text>
</a>
<a href="/hardware/shf01/">
<title>shf01 · shelf · in-use · cluster: tappaas · U46</title>
<rect x="42" y="954" width="240" height="6" fill="#bab0ac" stroke="#333"/>
<rect x="332" y="954" width="240" height="6" fill="#bab0ac" stroke="#333"/>
<text x="162" y="959" text-anchor="middle" fill="#333" font-size="9">shf01</text>
</a>
<a href="/hardware/srv07/">
<title>srv07 · server · staging · cluster: tappaas · shf02/front/slot 1</title>
<rect x="43" y="721" width="118" height="12" rx="3" fill="#4c78a8" stroke="#333333" stroke-width="1.5" stroke-dasharray="4 2"/>
<text x="102" y="731" text-anchor="middle" fill="#ffffff">srv07</text>
</a>
<a href="/hardware/srv04/">
<title>srv04 · server · staging · cluster: — · shf02/front/slot 2</title>
<rect x="163" y="721" width="118" height="12" rx="3" fill="#4c78a8" stroke="#333333" stroke-width="1.5" stroke-dasharray="4 2"/>
<text x="222" y="731" text-anchor="middle" fill="#ffffff">srv04</text>
</a>
<a href="/hardware/srv05/">
<title>srv05 · server · staging · cluster: — · shf02/rear/slot 1</title>
<rect x="333" y="721" width="118" height="12" rx="3" fill="#4c78a8" stroke="#333333" stroke-width="1.5" stroke-dasharray="4 2"/>
<text x="392" y="731" text-anchor="middle" fill="#ffffff">srv05</text>
</a>
<a href="/hardware/srv06/">
<title>srv06 · server · staging · cluster: tappaas · shf02/rear/slot 2</title>
<rect x="453" y="721" width="118" height="12" rx="3" fill="#4c78a8" stroke="#333333" stroke-width="1.5" stroke-dasharray="4 2"/>
<text x="512" y="731" text-anchor="middle" fill="#ffffff">srv06</text>
</a>
<a href="/hardware/shf02/">
<title>shf02 · shelf · in-use · cluster: — · U35</title>
<rect x="42" y="734" width="240" height="6" fill="#bab0ac" stroke="#333"/>
<rect x="332" y="734" width="240" height="6" fill="#bab0ac" stroke="#333"/>
<text x="162" y="739" text-anchor="middle" fill="#333" font-size="9">shf02</text>
</a>
<a href="/hardware/shf03/">
<title>shf03 · shelf · in-use · cluster: — · U21</title>
<rect x="42" y="454" width="240" height="6" fill="#bab0ac" stroke="#333"/>
<rect x="332" y="454" width="240" height="6" fill="#bab0ac" stroke="#333"/>
<text x="162" y="459" text-anchor="middle" fill="#333" font-size="9">shf03</text>
</a>
<a href="/hardware/shf04/">
<title>shf04 · shelf · in-use · cluster: — · U21</title>
<rect x="42" y="454" width="240" height="6" fill="#bab0ac" stroke="#333"/>
<rect x="332" y="454" width="240" height="6" fill="#bab0ac" stroke="#333"/>
<text x="162" y="459" text-anchor="middle" fill="#333" font-size="9">shf04</text>
</a>
<text x="42" y="1020" font-weight="bold">Legend</text>
<rect x="42" y="1028" width="12" height="12" fill="#9c755f" stroke="#333"/>
<text x="58" y="1038">patch-panel</text>

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 23 KiB

View file

@ -204,6 +204,65 @@ _Auto-generated from `docs/hardware/*.md` (items with `rack: rack01`) — do not
<text x="576" y="994" text-anchor="start" fill="#999">48</text>
<rect x="42" y="40" width="240" height="960" fill="none" stroke="#999" stroke-width="1.5"/>
<rect x="332" y="40" width="240" height="960" fill="none" stroke="#999" stroke-width="1.5"/>
<a href="/hardware/srv01/">
<title>srv01 · server · in-use · cluster: tappaas · shf01/front/slot 1</title>
<rect x="43" y="761" width="118" height="192" rx="3" fill="#4c78a8" stroke="#333333" stroke-width="1.5"/>
<text x="102" y="861" text-anchor="middle" fill="#ffffff">srv01</text>
</a>
<a href="/hardware/srv02/">
<title>srv02 · server · staging · cluster: tappaas · shf01/front/slot 2</title>
<rect x="163" y="801" width="118" height="152" rx="3" fill="#4c78a8" stroke="#333333" stroke-width="1.5" stroke-dasharray="4 2"/>
<text x="222" y="881" text-anchor="middle" fill="#ffffff">srv02</text>
</a>
<a href="/hardware/srv03/">
<title>srv03 · server · staging · cluster: tappaas · shf01/rear/slot 1</title>
<rect x="333" y="841" width="238" height="112" rx="3" fill="#4c78a8" stroke="#333333" stroke-width="1.5" stroke-dasharray="4 2"/>
<text x="452" y="901" text-anchor="middle" fill="#ffffff">srv03</text>
</a>
<a href="/hardware/shf01/">
<title>shf01 · shelf · in-use · cluster: tappaas · U46</title>
<rect x="42" y="954" width="240" height="6" fill="#bab0ac" stroke="#333"/>
<rect x="332" y="954" width="240" height="6" fill="#bab0ac" stroke="#333"/>
<text x="162" y="959" text-anchor="middle" fill="#333" font-size="9">shf01</text>
</a>
<a href="/hardware/srv07/">
<title>srv07 · server · staging · cluster: tappaas · shf02/front/slot 1</title>
<rect x="43" y="621" width="118" height="112" rx="3" fill="#4c78a8" stroke="#333333" stroke-width="1.5" stroke-dasharray="4 2"/>
<text x="102" y="681" text-anchor="middle" fill="#ffffff">srv07</text>
</a>
<a href="/hardware/srv04/">
<title>srv04 · server · staging · cluster: — · shf02/front/slot 2</title>
<rect x="163" y="621" width="118" height="112" rx="3" fill="#4c78a8" stroke="#333333" stroke-width="1.5" stroke-dasharray="4 2"/>
<text x="222" y="681" text-anchor="middle" fill="#ffffff">srv04</text>
</a>
<a href="/hardware/srv05/">
<title>srv05 · server · staging · cluster: — · shf02/rear/slot 1</title>
<rect x="333" y="601" width="118" height="132" rx="3" fill="#4c78a8" stroke="#333333" stroke-width="1.5" stroke-dasharray="4 2"/>
<text x="392" y="671" text-anchor="middle" fill="#ffffff">srv05</text>
</a>
<a href="/hardware/srv06/">
<title>srv06 · server · staging · cluster: tappaas · shf02/rear/slot 2</title>
<rect x="453" y="601" width="118" height="132" rx="3" fill="#4c78a8" stroke="#333333" stroke-width="1.5" stroke-dasharray="4 2"/>
<text x="512" y="671" text-anchor="middle" fill="#ffffff">srv06</text>
</a>
<a href="/hardware/shf02/">
<title>shf02 · shelf · in-use · cluster: — · U35</title>
<rect x="42" y="734" width="240" height="6" fill="#bab0ac" stroke="#333"/>
<rect x="332" y="734" width="240" height="6" fill="#bab0ac" stroke="#333"/>
<text x="162" y="739" text-anchor="middle" fill="#333" font-size="9">shf02</text>
</a>
<a href="/hardware/shf03/">
<title>shf03 · shelf · in-use · cluster: — · U21</title>
<rect x="42" y="454" width="240" height="6" fill="#bab0ac" stroke="#333"/>
<rect x="332" y="454" width="240" height="6" fill="#bab0ac" stroke="#333"/>
<text x="162" y="459" text-anchor="middle" fill="#333" font-size="9">shf03</text>
</a>
<a href="/hardware/shf04/">
<title>shf04 · shelf · in-use · cluster: — · U21</title>
<rect x="42" y="454" width="240" height="6" fill="#bab0ac" stroke="#333"/>
<rect x="332" y="454" width="240" height="6" fill="#bab0ac" stroke="#333"/>
<text x="162" y="459" text-anchor="middle" fill="#333" font-size="9">shf04</text>
</a>
<a href="/hardware/pdu01/">
<title>pdu01 · pdu · in-use · cluster: — · U1</title>
<rect x="333" y="41" width="238" height="18" rx="3" fill="#e15759" stroke="#333333" stroke-width="1.5"/>
@ -279,65 +338,6 @@ _Auto-generated from `docs/hardware/*.md` (items with `rack: rack01`) — do not
<rect x="333" y="701" width="238" height="18" rx="3" fill="#e15759" stroke="#333333" stroke-width="1.5"/>
<text x="452" y="714" text-anchor="middle" fill="#ffffff">pdu03 (U34)</text>
</a>
<a href="/hardware/srv01/">
<title>srv01 · server · in-use · cluster: tappaas · shf01/front/slot 1</title>
<rect x="43" y="941" width="118" height="12" rx="3" fill="#4c78a8" stroke="#333333" stroke-width="1.5"/>
<text x="102" y="951" text-anchor="middle" fill="#ffffff">srv01</text>
</a>
<a href="/hardware/srv02/">
<title>srv02 · server · staging · cluster: tappaas · shf01/front/slot 2</title>
<rect x="163" y="941" width="118" height="12" rx="3" fill="#4c78a8" stroke="#333333" stroke-width="1.5" stroke-dasharray="4 2"/>
<text x="222" y="951" text-anchor="middle" fill="#ffffff">srv02</text>
</a>
<a href="/hardware/srv03/">
<title>srv03 · server · staging · cluster: tappaas · shf01/rear/slot 1</title>
<rect x="333" y="941" width="238" height="12" rx="3" fill="#4c78a8" stroke="#333333" stroke-width="1.5" stroke-dasharray="4 2"/>
<text x="452" y="951" text-anchor="middle" fill="#ffffff">srv03</text>
</a>
<a href="/hardware/shf01/">
<title>shf01 · shelf · in-use · cluster: tappaas · U46</title>
<rect x="42" y="954" width="240" height="6" fill="#bab0ac" stroke="#333"/>
<rect x="332" y="954" width="240" height="6" fill="#bab0ac" stroke="#333"/>
<text x="162" y="959" text-anchor="middle" fill="#333" font-size="9">shf01</text>
</a>
<a href="/hardware/srv07/">
<title>srv07 · server · staging · cluster: tappaas · shf02/front/slot 1</title>
<rect x="43" y="721" width="118" height="12" rx="3" fill="#4c78a8" stroke="#333333" stroke-width="1.5" stroke-dasharray="4 2"/>
<text x="102" y="731" text-anchor="middle" fill="#ffffff">srv07</text>
</a>
<a href="/hardware/srv04/">
<title>srv04 · server · staging · cluster: — · shf02/front/slot 2</title>
<rect x="163" y="721" width="118" height="12" rx="3" fill="#4c78a8" stroke="#333333" stroke-width="1.5" stroke-dasharray="4 2"/>
<text x="222" y="731" text-anchor="middle" fill="#ffffff">srv04</text>
</a>
<a href="/hardware/srv05/">
<title>srv05 · server · staging · cluster: — · shf02/rear/slot 1</title>
<rect x="333" y="721" width="118" height="12" rx="3" fill="#4c78a8" stroke="#333333" stroke-width="1.5" stroke-dasharray="4 2"/>
<text x="392" y="731" text-anchor="middle" fill="#ffffff">srv05</text>
</a>
<a href="/hardware/srv06/">
<title>srv06 · server · staging · cluster: tappaas · shf02/rear/slot 2</title>
<rect x="453" y="721" width="118" height="12" rx="3" fill="#4c78a8" stroke="#333333" stroke-width="1.5" stroke-dasharray="4 2"/>
<text x="512" y="731" text-anchor="middle" fill="#ffffff">srv06</text>
</a>
<a href="/hardware/shf02/">
<title>shf02 · shelf · in-use · cluster: — · U35</title>
<rect x="42" y="734" width="240" height="6" fill="#bab0ac" stroke="#333"/>
<rect x="332" y="734" width="240" height="6" fill="#bab0ac" stroke="#333"/>
<text x="162" y="739" text-anchor="middle" fill="#333" font-size="9">shf02</text>
</a>
<a href="/hardware/shf03/">
<title>shf03 · shelf · in-use · cluster: — · U21</title>
<rect x="42" y="454" width="240" height="6" fill="#bab0ac" stroke="#333"/>
<rect x="332" y="454" width="240" height="6" fill="#bab0ac" stroke="#333"/>
<text x="162" y="459" text-anchor="middle" fill="#333" font-size="9">shf03</text>
</a>
<a href="/hardware/shf04/">
<title>shf04 · shelf · in-use · cluster: — · U21</title>
<rect x="42" y="454" width="240" height="6" fill="#bab0ac" stroke="#333"/>
<rect x="332" y="454" width="240" height="6" fill="#bab0ac" stroke="#333"/>
<text x="162" y="459" text-anchor="middle" fill="#333" font-size="9">shf04</text>
</a>
<text x="42" y="1020" font-weight="bold">Legend</text>
<rect x="42" y="1028" width="12" height="12" fill="#9c755f" stroke="#333"/>
<text x="58" y="1038">patch-panel</text>
@ -499,11 +499,11 @@ flowchart LR
| U25 | [pp02](../../hardware/pp02.md) | patch-panel | front | in-use |
| U34 | [pdu03](../../hardware/pdu03.md) | pdu | rear | in-use |
| U35 | [shf02](../../hardware/shf02.md) | shelf | both | in-use |
| U35 | [srv07](../../hardware/srv07.md) | server | front · shf02/1 | staging |
| U35 | [srv04](../../hardware/srv04.md) | server | front · shf02/2 | staging |
| U35 | [srv05](../../hardware/srv05.md) | server | rear · shf02/1 | staging |
| U35 | [srv06](../../hardware/srv06.md) | server | rear · shf02/2 | staging |
| U30U35 | [srv07](../../hardware/srv07.md) | server | front · shf02/1 | staging |
| U30U35 | [srv04](../../hardware/srv04.md) | server | front · shf02/2 | staging |
| U29U35 | [srv05](../../hardware/srv05.md) | server | rear · shf02/1 | staging |
| U29U35 | [srv06](../../hardware/srv06.md) | server | rear · shf02/2 | staging |
| U46 | [shf01](../../hardware/shf01.md) | shelf | both | in-use |
| U46 | [srv01](../../hardware/srv01.md) | server | front · shf01/1 | in-use |
| U46 | [srv02](../../hardware/srv02.md) | server | front · shf01/2 | staging |
| U46 | [srv03](../../hardware/srv03.md) | server | rear · shf01/1 | staging |
| U37U46 | [srv01](../../hardware/srv01.md) | server | front · shf01/1 | in-use |
| U39U46 | [srv02](../../hardware/srv02.md) | server | front · shf01/2 | staging |
| U41U46 | [srv03](../../hardware/srv03.md) | server | rear · shf01/1 | staging |

View file

@ -105,6 +105,14 @@ def validate_item(fm: dict) -> None:
f"{name}: 'shelf_slot' must be a whole number 1 or higher "
f"(got {slot!r})."
)
if "chassis_u" in fm:
cu = fm.get("chassis_u")
if isinstance(cu, bool) or not isinstance(cu, int) or cu < 1:
raise SchemaError(
f"{name}: 'chassis_u' is the device's height in U where it "
f"stands on the shelf — it must be a whole number 1 or "
f"higher (got {cu!r})."
)
return
face = fm.get("rack_face")
if face not in FACES:
@ -495,15 +503,6 @@ def render_svg(rack: str, items: list[dict]) -> str:
)
p.append("</a>")
for fm in items:
if fm.get("kind") == "shelf" or "mounted_on" in fm:
continue
face = fm.get("rack_face")
if face in ("front", "both"):
draw_device(fm, front_x)
if face in ("rear", "both"):
draw_device(fm, rear_x)
def draw_rail(fm: dict, x: int) -> None:
color = KIND_COLORS.get(fm.get("kind", ""), DEFAULT_COLOR)
name = fm.get("hostname", "?")
@ -522,11 +521,6 @@ def render_svg(rack: str, items: list[dict]) -> str:
)
p.append("</a>")
for idx, fm in enumerate(left_items):
draw_rail(fm, PAD + idx * RAIL_W)
for idx, fm in enumerate(right_items):
draw_rail(fm, right_gutter_x + LABEL_W + idx * RAIL_W)
SHELF_STRIP_H = 6
shelves = [i for i in items if i.get("kind") == "shelf"]
mounted = [i for i in items if "mounted_on" in i]
@ -537,7 +531,6 @@ def render_svg(rack: str, items: list[dict]) -> str:
y = u_y(u)
block_h = h * U_H
strip_y = y + block_h - SHELF_STRIP_H
avail_h = block_h - SHELF_STRIP_H
shelf_color = KIND_COLORS.get("shelf", DEFAULT_COLOR)
sname = fm.get("hostname", "?")
for col_x, sface in ((front_x, "front"), (rear_x, "rear")):
@ -554,15 +547,22 @@ def render_svg(rack: str, items: list[dict]) -> str:
bw = (COL_W - idx * sub_w) if idx == n - 1 else sub_w
mcolor = KIND_COLORS.get(m.get("kind", ""), DEFAULT_COLOR)
mname = m.get("hostname", "?")
# The device stands on the shelf strip and rises chassis_u U's
# upward; without chassis_u it fills the shelf block (legacy).
dev_u = m.get("chassis_u")
if not isinstance(dev_u, int) or isinstance(dev_u, bool) or dev_u < 1:
dev_u = h
dev_h = dev_u * U_H - SHELF_STRIP_H
by = strip_y - dev_h
p.append(f'<a href="{_host_url(mname)}">')
p.append(f"<title>{_tooltip(m)}</title>")
p.append(
f'<rect x="{bx + 1}" y="{y + 1}" width="{bw - 2}" '
f'height="{avail_h - 2}" rx="3" fill="{mcolor}" '
f'<rect x="{bx + 1}" y="{by + 1}" width="{bw - 2}" '
f'height="{dev_h - 2}" rx="3" fill="{mcolor}" '
f"{_stroke_attrs(m.get('status'))}/>"
)
p.append(
f'<text x="{bx + bw // 2}" y="{y + avail_h // 2 + 4}" '
f'<text x="{bx + bw // 2}" y="{by + dev_h // 2 + 4}" '
f'text-anchor="middle" fill="#ffffff">{_esc(mname)}</text>'
)
p.append("</a>")
@ -579,9 +579,26 @@ def render_svg(rack: str, items: list[dict]) -> str:
)
p.append("</a>")
# Paint order (bottom → top): shelves and their towers first, then
# U-mounted devices (so a rail-mounted PDU stays visible over a tower),
# then 0U side rails.
for fm in sorted(shelves, key=lambda s: s.get("hostname", "")):
draw_shelf(fm)
for fm in items:
if fm.get("kind") == "shelf" or "mounted_on" in fm:
continue
face = fm.get("rack_face")
if face in ("front", "both"):
draw_device(fm, front_x)
if face in ("rear", "both"):
draw_device(fm, rear_x)
for idx, fm in enumerate(left_items):
draw_rail(fm, PAD + idx * RAIL_W)
for idx, fm in enumerate(right_items):
draw_rail(fm, right_gutter_x + LABEL_W + idx * RAIL_W)
legend_y = top + body_h + PAD + 8
p.append(
f'<text x="{front_x}" y="{legend_y}" font-weight="bold">Legend</text>'
@ -773,6 +790,12 @@ def render_page(rack: str, items: list[dict]) -> str:
if target and isinstance(target.get("rack_u"), int):
su = target["rack_u"]
sh = target["u_height"]
cu = fm.get("chassis_u")
if isinstance(cu, int) and not isinstance(cu, bool) and cu >= 1:
base = su + sh - 1 # the shelf's bottom U; towers rise from it
top = base - cu + 1
urange = f"U{base}" if cu == 1 else f"U{top}U{base}"
else:
urange = f"U{su}" if sh == 1 else f"U{su}U{su + sh - 1}"
else:
urange = ""

View file

@ -623,6 +623,50 @@ def test_render_svg_draws_shelf_and_occupants():
assert "shf01 (U37" not in svg
def test_validate_accepts_chassis_u():
gen_rack.validate_item(item(hostname="srv01", mounted_on="shf01",
shelf_face="front", shelf_slot=1, chassis_u=10))
def test_validate_rejects_bad_chassis_u():
with pytest.raises(gen_rack.SchemaError):
gen_rack.validate_item(item(hostname="srv01", mounted_on="shf01",
shelf_face="front", shelf_slot=1, chassis_u=0))
def test_render_svg_tower_height_reflects_chassis_u():
items = [
shelf(hostname="shf01", rack_u=46, u_height=1),
item(hostname="srv01", mounted_on="shf01", shelf_face="front",
shelf_slot=1, chassis_u=10),
]
svg = gen_rack.render_svg("rack01", items)
# 10U tower drawn as 10*U_H - SHELF_STRIP_H - 2 px tall
assert 'height="192"' in svg
def test_render_svg_paints_rail_device_over_shelf_tower():
# A PDU rail-mounted within a tower's span must be painted after the tower
# (later in the SVG = on top) so it stays visible.
items = [
shelf(hostname="shf02", rack_u=35, u_height=1),
item(hostname="srv05", mounted_on="shf02", shelf_face="rear",
shelf_slot=1, chassis_u=7),
item(hostname="pdu03", kind="pdu", rack_u=34, u_height=1,
rack_face="rear"),
]
svg = gen_rack.render_svg("rack01", items)
assert svg.index("srv05") < svg.index("pdu03")
def test_render_page_mounted_shows_chassis_span():
items = [shelf(hostname="shf01", rack_u=46, u_height=1),
item(hostname="srv01", mounted_on="shf01", shelf_face="front",
shelf_slot=1, chassis_u=10)]
page = gen_rack.render_page("rack01", items)
assert "U37U46" in page
def test_render_svg_shelf_is_deterministic():
base = [
shelf(),