feat(rack): inline interactive elevation with links, tooltips, status borders

This commit is contained in:
sjat 2026-06-24 18:34:06 +02:00
parent d5cfe9665c
commit 8d39fbcdf5
4 changed files with 362 additions and 18 deletions

View file

@ -1,4 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="616" height="1012" viewBox="0 0 616 1012" font-family="sans-serif" font-size="11"> <svg xmlns="http://www.w3.org/2000/svg" width="616" height="1012" viewBox="0 0 616 1012" style="max-width:100%;height:auto" font-family="sans-serif" font-size="11">
<rect width="616" height="1012" fill="#ffffff"/> <rect width="616" height="1012" fill="#ffffff"/>
<text x="12" y="28" font-size="16" font-weight="bold">Rack rack01</text> <text x="12" y="28" font-size="16" font-weight="bold">Rack rack01</text>
<text x="178" y="34" text-anchor="middle" font-weight="bold">front</text> <text x="178" y="34" text-anchor="middle" font-weight="bold">front</text>
@ -147,25 +147,55 @@
<text x="54" y="954" text-anchor="end" fill="#999">46</text> <text x="54" y="954" text-anchor="end" fill="#999">46</text>
<text x="54" y="974" text-anchor="end" fill="#999">47</text> <text x="54" y="974" text-anchor="end" fill="#999">47</text>
<text x="54" y="994" text-anchor="end" fill="#999">48</text> <text x="54" y="994" text-anchor="end" fill="#999">48</text>
<rect x="59" y="121" width="238" height="38" rx="3" fill="#4c78a8" stroke="#333"/> <a href="/hardware/srv04/">
<title>srv04 · server · staging · cluster: — · U5U6</title>
<rect x="59" y="121" width="238" height="38" rx="3" fill="#4c78a8" stroke="#333333" stroke-width="1.5" stroke-dasharray="4 2"/>
<text x="178" y="144" text-anchor="middle" fill="#ffffff">srv04 (U5U6)</text> <text x="178" y="144" text-anchor="middle" fill="#ffffff">srv04 (U5U6)</text>
<rect x="349" y="121" width="238" height="38" rx="3" fill="#4c78a8" stroke="#333"/> </a>
<a href="/hardware/srv05/">
<title>srv05 · server · staging · cluster: — · U5U6</title>
<rect x="349" y="121" width="238" height="38" rx="3" fill="#4c78a8" stroke="#333333" stroke-width="1.5" stroke-dasharray="4 2"/>
<text x="468" y="144" text-anchor="middle" fill="#ffffff">srv05 (U5U6)</text> <text x="468" y="144" text-anchor="middle" fill="#ffffff">srv05 (U5U6)</text>
<rect x="59" y="221" width="238" height="18" rx="3" fill="#59a14f" stroke="#333"/> </a>
<a href="/hardware/sw01/">
<title>sw01 · switch · in-use · cluster: — · U10</title>
<rect x="59" y="221" width="238" height="18" rx="3" fill="#59a14f" stroke="#333333" stroke-width="1.5"/>
<text x="178" y="234" text-anchor="middle" fill="#ffffff">sw01 (U10)</text> <text x="178" y="234" text-anchor="middle" fill="#ffffff">sw01 (U10)</text>
<rect x="59" y="501" width="238" height="18" rx="3" fill="#9c755f" stroke="#333"/> </a>
<a href="/hardware/pp01/">
<title>pp01 · patch-panel · in-use · cluster: — · U24</title>
<rect x="59" y="501" width="238" height="18" rx="3" fill="#9c755f" stroke="#333333" stroke-width="1.5"/>
<text x="178" y="514" text-anchor="middle" fill="#ffffff">pp01 (U24)</text> <text x="178" y="514" text-anchor="middle" fill="#ffffff">pp01 (U24)</text>
<rect x="12" y="40" width="16" height="960" fill="#e15759" stroke="#333"/> </a>
<a href="/hardware/pdu01/">
<title>pdu01 · pdu · in-use · cluster: — · 0U left</title>
<rect x="12" y="40" width="16" height="960" fill="#e15759" stroke="#333333" stroke-width="1.5"/>
<text x="20" y="520" text-anchor="middle" fill="#ffffff" transform="rotate(-90 20 520)">pdu01</text> <text x="20" y="520" text-anchor="middle" fill="#ffffff" transform="rotate(-90 20 520)">pdu01</text>
<rect x="588" y="40" width="16" height="960" fill="#e15759" stroke="#333"/> </a>
<a href="/hardware/pdu02/">
<title>pdu02 · pdu · in-use · cluster: — · 0U right</title>
<rect x="588" y="40" width="16" height="960" fill="#e15759" stroke="#333333" stroke-width="1.5"/>
<text x="596" y="520" text-anchor="middle" fill="#ffffff" transform="rotate(-90 596 520)">pdu02</text> <text x="596" y="520" text-anchor="middle" fill="#ffffff" transform="rotate(-90 596 520)">pdu02</text>
<rect x="59" y="761" width="118" height="192" rx="3" fill="#4c78a8" stroke="#333"/> </a>
<a href="/hardware/srv01/">
<title>srv01 · server · staging · cluster: tappaas · shf01/front/slot 1</title>
<rect x="59" y="761" width="118" height="192" rx="3" fill="#4c78a8" stroke="#333333" stroke-width="1.5" stroke-dasharray="4 2"/>
<text x="118" y="861" text-anchor="middle" fill="#ffffff">srv01</text> <text x="118" y="861" text-anchor="middle" fill="#ffffff">srv01</text>
<rect x="179" y="761" width="118" height="192" rx="3" fill="#4c78a8" stroke="#333"/> </a>
<a href="/hardware/srv02/">
<title>srv02 · server · staging · cluster: tappaas · shf01/front/slot 2</title>
<rect x="179" y="761" width="118" height="192" rx="3" fill="#4c78a8" stroke="#333333" stroke-width="1.5" stroke-dasharray="4 2"/>
<text x="238" y="861" text-anchor="middle" fill="#ffffff">srv02</text> <text x="238" y="861" text-anchor="middle" fill="#ffffff">srv02</text>
<rect x="58" y="954" width="240" height="6" fill="#bab0ac" stroke="#333"/> </a>
<rect x="349" y="761" width="238" height="192" rx="3" fill="#4c78a8" stroke="#333"/> <a href="/hardware/srv03/">
<title>srv03 · server · staging · cluster: tappaas · shf01/rear/slot 1</title>
<rect x="349" y="761" width="238" height="192" rx="3" fill="#4c78a8" stroke="#333333" stroke-width="1.5" stroke-dasharray="4 2"/>
<text x="468" y="861" text-anchor="middle" fill="#ffffff">srv03</text> <text x="468" y="861" text-anchor="middle" fill="#ffffff">srv03</text>
</a>
<a href="/hardware/shf01/">
<title>shf01 · shelf · in-use · cluster: tappaas · U37U46</title>
<rect x="58" y="954" width="240" height="6" fill="#bab0ac" stroke="#333"/>
<rect x="348" y="954" width="240" height="6" fill="#bab0ac" stroke="#333"/> <rect x="348" y="954" width="240" height="6" fill="#bab0ac" stroke="#333"/>
<text x="178" y="959" text-anchor="middle" fill="#333" font-size="9">shf01</text> <text x="178" y="959" text-anchor="middle" fill="#333" font-size="9">shf01</text>
</a>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View file

@ -4,7 +4,211 @@ _Auto-generated from `docs/hardware/*.md` (items with `rack: rack01`) — do not
## Elevation ## Elevation
![Rack rack01 elevation](rack01-elevation.svg) <div class="rack-elevation">
<svg xmlns="http://www.w3.org/2000/svg" width="616" height="1012" viewBox="0 0 616 1012" style="max-width:100%;height:auto" font-family="sans-serif" font-size="11">
<rect width="616" height="1012" fill="#ffffff"/>
<text x="12" y="28" font-size="16" font-weight="bold">Rack rack01</text>
<text x="178" y="34" text-anchor="middle" font-weight="bold">front</text>
<rect x="58" y="40" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
<rect x="58" y="60" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
<rect x="58" y="80" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
<rect x="58" y="100" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
<rect x="58" y="120" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
<rect x="58" y="140" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
<rect x="58" y="160" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
<rect x="58" y="180" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
<rect x="58" y="200" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
<rect x="58" y="220" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
<rect x="58" y="240" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
<rect x="58" y="260" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
<rect x="58" y="280" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
<rect x="58" y="300" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
<rect x="58" y="320" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
<rect x="58" y="340" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
<rect x="58" y="360" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
<rect x="58" y="380" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
<rect x="58" y="400" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
<rect x="58" y="420" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
<rect x="58" y="440" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
<rect x="58" y="460" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
<rect x="58" y="480" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
<rect x="58" y="500" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
<rect x="58" y="520" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
<rect x="58" y="540" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
<rect x="58" y="560" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
<rect x="58" y="580" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
<rect x="58" y="600" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
<rect x="58" y="620" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
<rect x="58" y="640" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
<rect x="58" y="660" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
<rect x="58" y="680" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
<rect x="58" y="700" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
<rect x="58" y="720" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
<rect x="58" y="740" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
<rect x="58" y="760" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
<rect x="58" y="780" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
<rect x="58" y="800" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
<rect x="58" y="820" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
<rect x="58" y="840" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
<rect x="58" y="860" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
<rect x="58" y="880" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
<rect x="58" y="900" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
<rect x="58" y="920" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
<rect x="58" y="940" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
<rect x="58" y="960" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
<rect x="58" y="980" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
<text x="468" y="34" text-anchor="middle" font-weight="bold">rear</text>
<rect x="348" y="40" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
<rect x="348" y="60" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
<rect x="348" y="80" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
<rect x="348" y="100" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
<rect x="348" y="120" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
<rect x="348" y="140" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
<rect x="348" y="160" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
<rect x="348" y="180" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
<rect x="348" y="200" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
<rect x="348" y="220" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
<rect x="348" y="240" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
<rect x="348" y="260" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
<rect x="348" y="280" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
<rect x="348" y="300" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
<rect x="348" y="320" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
<rect x="348" y="340" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
<rect x="348" y="360" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
<rect x="348" y="380" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
<rect x="348" y="400" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
<rect x="348" y="420" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
<rect x="348" y="440" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
<rect x="348" y="460" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
<rect x="348" y="480" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
<rect x="348" y="500" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
<rect x="348" y="520" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
<rect x="348" y="540" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
<rect x="348" y="560" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
<rect x="348" y="580" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
<rect x="348" y="600" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
<rect x="348" y="620" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
<rect x="348" y="640" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
<rect x="348" y="660" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
<rect x="348" y="680" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
<rect x="348" y="700" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
<rect x="348" y="720" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
<rect x="348" y="740" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
<rect x="348" y="760" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
<rect x="348" y="780" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
<rect x="348" y="800" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
<rect x="348" y="820" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
<rect x="348" y="840" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
<rect x="348" y="860" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
<rect x="348" y="880" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
<rect x="348" y="900" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
<rect x="348" y="920" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
<rect x="348" y="940" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
<rect x="348" y="960" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
<rect x="348" y="980" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
<text x="54" y="54" text-anchor="end" fill="#999">1</text>
<text x="54" y="74" text-anchor="end" fill="#999">2</text>
<text x="54" y="94" text-anchor="end" fill="#999">3</text>
<text x="54" y="114" text-anchor="end" fill="#999">4</text>
<text x="54" y="134" text-anchor="end" fill="#999">5</text>
<text x="54" y="154" text-anchor="end" fill="#999">6</text>
<text x="54" y="174" text-anchor="end" fill="#999">7</text>
<text x="54" y="194" text-anchor="end" fill="#999">8</text>
<text x="54" y="214" text-anchor="end" fill="#999">9</text>
<text x="54" y="234" text-anchor="end" fill="#999">10</text>
<text x="54" y="254" text-anchor="end" fill="#999">11</text>
<text x="54" y="274" text-anchor="end" fill="#999">12</text>
<text x="54" y="294" text-anchor="end" fill="#999">13</text>
<text x="54" y="314" text-anchor="end" fill="#999">14</text>
<text x="54" y="334" text-anchor="end" fill="#999">15</text>
<text x="54" y="354" text-anchor="end" fill="#999">16</text>
<text x="54" y="374" text-anchor="end" fill="#999">17</text>
<text x="54" y="394" text-anchor="end" fill="#999">18</text>
<text x="54" y="414" text-anchor="end" fill="#999">19</text>
<text x="54" y="434" text-anchor="end" fill="#999">20</text>
<text x="54" y="454" text-anchor="end" fill="#999">21</text>
<text x="54" y="474" text-anchor="end" fill="#999">22</text>
<text x="54" y="494" text-anchor="end" fill="#999">23</text>
<text x="54" y="514" text-anchor="end" fill="#999">24</text>
<text x="54" y="534" text-anchor="end" fill="#999">25</text>
<text x="54" y="554" text-anchor="end" fill="#999">26</text>
<text x="54" y="574" text-anchor="end" fill="#999">27</text>
<text x="54" y="594" text-anchor="end" fill="#999">28</text>
<text x="54" y="614" text-anchor="end" fill="#999">29</text>
<text x="54" y="634" text-anchor="end" fill="#999">30</text>
<text x="54" y="654" text-anchor="end" fill="#999">31</text>
<text x="54" y="674" text-anchor="end" fill="#999">32</text>
<text x="54" y="694" text-anchor="end" fill="#999">33</text>
<text x="54" y="714" text-anchor="end" fill="#999">34</text>
<text x="54" y="734" text-anchor="end" fill="#999">35</text>
<text x="54" y="754" text-anchor="end" fill="#999">36</text>
<text x="54" y="774" text-anchor="end" fill="#999">37</text>
<text x="54" y="794" text-anchor="end" fill="#999">38</text>
<text x="54" y="814" text-anchor="end" fill="#999">39</text>
<text x="54" y="834" text-anchor="end" fill="#999">40</text>
<text x="54" y="854" text-anchor="end" fill="#999">41</text>
<text x="54" y="874" text-anchor="end" fill="#999">42</text>
<text x="54" y="894" text-anchor="end" fill="#999">43</text>
<text x="54" y="914" text-anchor="end" fill="#999">44</text>
<text x="54" y="934" text-anchor="end" fill="#999">45</text>
<text x="54" y="954" text-anchor="end" fill="#999">46</text>
<text x="54" y="974" text-anchor="end" fill="#999">47</text>
<text x="54" y="994" text-anchor="end" fill="#999">48</text>
<a href="/hardware/srv04/">
<title>srv04 · server · staging · cluster: — · U5U6</title>
<rect x="59" y="121" width="238" height="38" rx="3" fill="#4c78a8" stroke="#333333" stroke-width="1.5" stroke-dasharray="4 2"/>
<text x="178" y="144" text-anchor="middle" fill="#ffffff">srv04 (U5U6)</text>
</a>
<a href="/hardware/srv05/">
<title>srv05 · server · staging · cluster: — · U5U6</title>
<rect x="349" y="121" width="238" height="38" rx="3" fill="#4c78a8" stroke="#333333" stroke-width="1.5" stroke-dasharray="4 2"/>
<text x="468" y="144" text-anchor="middle" fill="#ffffff">srv05 (U5U6)</text>
</a>
<a href="/hardware/sw01/">
<title>sw01 · switch · in-use · cluster: — · U10</title>
<rect x="59" y="221" width="238" height="18" rx="3" fill="#59a14f" stroke="#333333" stroke-width="1.5"/>
<text x="178" y="234" text-anchor="middle" fill="#ffffff">sw01 (U10)</text>
</a>
<a href="/hardware/pp01/">
<title>pp01 · patch-panel · in-use · cluster: — · U24</title>
<rect x="59" y="501" width="238" height="18" rx="3" fill="#9c755f" stroke="#333333" stroke-width="1.5"/>
<text x="178" y="514" text-anchor="middle" fill="#ffffff">pp01 (U24)</text>
</a>
<a href="/hardware/pdu01/">
<title>pdu01 · pdu · in-use · cluster: — · 0U left</title>
<rect x="12" y="40" width="16" height="960" fill="#e15759" stroke="#333333" stroke-width="1.5"/>
<text x="20" y="520" text-anchor="middle" fill="#ffffff" transform="rotate(-90 20 520)">pdu01</text>
</a>
<a href="/hardware/pdu02/">
<title>pdu02 · pdu · in-use · cluster: — · 0U right</title>
<rect x="588" y="40" width="16" height="960" fill="#e15759" stroke="#333333" stroke-width="1.5"/>
<text x="596" y="520" text-anchor="middle" fill="#ffffff" transform="rotate(-90 596 520)">pdu02</text>
</a>
<a href="/hardware/srv01/">
<title>srv01 · server · staging · cluster: tappaas · shf01/front/slot 1</title>
<rect x="59" y="761" width="118" height="192" rx="3" fill="#4c78a8" stroke="#333333" stroke-width="1.5" stroke-dasharray="4 2"/>
<text x="118" 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="179" y="761" width="118" height="192" rx="3" fill="#4c78a8" stroke="#333333" stroke-width="1.5" stroke-dasharray="4 2"/>
<text x="238" y="861" 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="349" y="761" width="238" height="192" rx="3" fill="#4c78a8" stroke="#333333" stroke-width="1.5" stroke-dasharray="4 2"/>
<text x="468" y="861" text-anchor="middle" fill="#ffffff">srv03</text>
</a>
<a href="/hardware/shf01/">
<title>shf01 · shelf · in-use · cluster: tappaas · U37U46</title>
<rect x="58" y="954" width="240" height="6" fill="#bab0ac" stroke="#333"/>
<rect x="348" y="954" width="240" height="6" fill="#bab0ac" stroke="#333"/>
<text x="178" y="959" text-anchor="middle" fill="#333" font-size="9">shf01</text>
</a>
</svg>
</div>
[Download SVG](rack01-elevation.svg)
## Power ## Power

View file

@ -289,6 +289,54 @@ def _esc(s: object) -> str:
return str(s).replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;") return str(s).replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
STATUS_STROKE: dict[str, tuple[str, float, str]] = {
"in-use": ("#333333", 1.5, ""),
"staging": ("#333333", 1.5, "4 2"),
"broken": ("#e15759", 3, ""),
"spare": ("#bbbbbb", 1.5, ""),
"donated": ("#bbbbbb", 1.5, ""),
}
DEFAULT_STATUS_STROKE: tuple[str, float, str] = ("#333333", 1.5, "")
def _status_stroke(status: object) -> tuple[str, float, str]:
return STATUS_STROKE.get(status, DEFAULT_STATUS_STROKE)
def _stroke_attrs(status: object) -> str:
stroke, sw, dash = _status_stroke(status)
dash_attr = f' stroke-dasharray="{dash}"' if dash else ""
return f'stroke="{stroke}" stroke-width="{sw}"{dash_attr}'
def _host_url(host: object) -> str:
return f"/hardware/{host}/"
def _placement(fm: dict) -> str:
if "mounted_on" in fm:
return (
f"{fm.get('mounted_on', '?')}/{fm.get('shelf_face', '')}/"
f"slot {fm.get('shelf_slot', '')}"
)
face = fm.get("rack_face")
if face in ZERO_U_FACES:
return f"0U {face}"
u = fm.get("rack_u")
h = fm.get("u_height")
if isinstance(u, int) and isinstance(h, int):
return f"U{u}" if h == 1 else f"U{u}U{u + h - 1}"
return "?"
def _tooltip(fm: dict) -> str:
host = fm.get("hostname", "?")
return _esc(
f"{host} · {fm.get('kind', '')} · {fm.get('status', '')} · "
f"cluster: {fm.get('cluster', '')} · {_placement(fm)}"
)
def _sorted_items(items: list[dict]) -> list[dict]: def _sorted_items(items: list[dict]) -> list[dict]:
"""Deterministic order: faced items by U then hostname, 0U items last.""" """Deterministic order: faced items by U then hostname, 0U items last."""
return sorted( return sorted(
@ -329,6 +377,7 @@ def render_svg(rack: str, items: list[dict]) -> str:
p.append( p.append(
f'<svg xmlns="http://www.w3.org/2000/svg" width="{width}" ' f'<svg xmlns="http://www.w3.org/2000/svg" width="{width}" '
f'height="{height}" viewBox="0 0 {width} {height}" ' f'height="{height}" viewBox="0 0 {width} {height}" '
f'style="max-width:100%;height:auto" '
f'font-family="sans-serif" font-size="11">' f'font-family="sans-serif" font-size="11">'
) )
p.append(f'<rect width="{width}" height="{height}" fill="#ffffff"/>') p.append(f'<rect width="{width}" height="{height}" fill="#ffffff"/>')
@ -365,15 +414,19 @@ def render_svg(rack: str, items: list[dict]) -> str:
color = KIND_COLORS.get(fm.get("kind", ""), DEFAULT_COLOR) color = KIND_COLORS.get(fm.get("kind", ""), DEFAULT_COLOR)
name = fm.get("hostname", "?") name = fm.get("hostname", "?")
urange = f"U{u}" if h == 1 else f"U{u}U{u + h - 1}" urange = f"U{u}" if h == 1 else f"U{u}U{u + h - 1}"
p.append(f'<a href="{_host_url(name)}">')
p.append(f"<title>{_tooltip(fm)}</title>")
p.append( p.append(
f'<rect x="{col_x + 1}" y="{y + 1}" width="{COL_W - 2}" ' f'<rect x="{col_x + 1}" y="{y + 1}" width="{COL_W - 2}" '
f'height="{box_h - 2}" rx="3" fill="{color}" stroke="#333"/>' f'height="{box_h - 2}" rx="3" fill="{color}" '
f"{_stroke_attrs(fm.get('status'))}/>"
) )
p.append( p.append(
f'<text x="{col_x + COL_W // 2}" y="{y + box_h // 2 + 4}" ' f'<text x="{col_x + COL_W // 2}" y="{y + box_h // 2 + 4}" '
f'text-anchor="middle" fill="#ffffff">' f'text-anchor="middle" fill="#ffffff">'
f'{_esc(name)} ({urange})</text>' f"{_esc(name)} ({urange})</text>"
) )
p.append("</a>")
for fm in items: for fm in items:
if fm.get("kind") == "shelf" or "mounted_on" in fm: if fm.get("kind") == "shelf" or "mounted_on" in fm:
@ -389,14 +442,17 @@ def render_svg(rack: str, items: list[dict]) -> str:
name = fm.get("hostname", "?") name = fm.get("hostname", "?")
cx = x + RAIL_W // 2 cx = x + RAIL_W // 2
cy = top + body_h // 2 cy = top + body_h // 2
p.append(f'<a href="{_host_url(name)}">')
p.append(f"<title>{_tooltip(fm)}</title>")
p.append( p.append(
f'<rect x="{x}" y="{top}" width="{RAIL_W}" height="{body_h}" ' f'<rect x="{x}" y="{top}" width="{RAIL_W}" height="{body_h}" '
f'fill="{color}" stroke="#333"/>' f"fill=\"{color}\" {_stroke_attrs(fm.get('status'))}/>"
) )
p.append( p.append(
f'<text x="{cx}" y="{cy}" text-anchor="middle" fill="#ffffff" ' f'<text x="{cx}" y="{cy}" text-anchor="middle" fill="#ffffff" '
f'transform="rotate(-90 {cx} {cy})">{_esc(name)}</text>' f'transform="rotate(-90 {cx} {cy})">{_esc(name)}</text>'
) )
p.append("</a>")
for idx, fm in enumerate(left_items): for idx, fm in enumerate(left_items):
draw_rail(fm, PAD + idx * RAIL_W) draw_rail(fm, PAD + idx * RAIL_W)
@ -430,14 +486,21 @@ def render_svg(rack: str, items: list[dict]) -> str:
bw = (COL_W - idx * sub_w) if idx == n - 1 else sub_w bw = (COL_W - idx * sub_w) if idx == n - 1 else sub_w
mcolor = KIND_COLORS.get(m.get("kind", ""), DEFAULT_COLOR) mcolor = KIND_COLORS.get(m.get("kind", ""), DEFAULT_COLOR)
mname = m.get("hostname", "?") mname = m.get("hostname", "?")
p.append(f'<a href="{_host_url(mname)}">')
p.append(f"<title>{_tooltip(m)}</title>")
p.append( p.append(
f'<rect x="{bx + 1}" y="{y + 1}" width="{bw - 2}" ' f'<rect x="{bx + 1}" y="{y + 1}" width="{bw - 2}" '
f'height="{avail_h - 2}" rx="3" fill="{mcolor}" stroke="#333"/>' f'height="{avail_h - 2}" rx="3" fill="{mcolor}" '
f"{_stroke_attrs(m.get('status'))}/>"
) )
p.append( p.append(
f'<text x="{bx + bw // 2}" y="{y + avail_h // 2 + 4}" ' f'<text x="{bx + bw // 2}" y="{y + avail_h // 2 + 4}" '
f'text-anchor="middle" fill="#ffffff">{_esc(mname)}</text>' f'text-anchor="middle" fill="#ffffff">{_esc(mname)}</text>'
) )
p.append("</a>")
p.append(f'<a href="{_host_url(sname)}">')
p.append(f"<title>{_tooltip(fm)}</title>")
for col_x in (front_x, rear_x):
p.append( p.append(
f'<rect x="{col_x}" y="{strip_y}" width="{COL_W}" ' f'<rect x="{col_x}" y="{strip_y}" width="{COL_W}" '
f'height="{SHELF_STRIP_H}" fill="{shelf_color}" stroke="#333"/>' f'height="{SHELF_STRIP_H}" fill="{shelf_color}" stroke="#333"/>'
@ -446,6 +509,7 @@ def render_svg(rack: str, items: list[dict]) -> str:
f'<text x="{front_x + COL_W // 2}" y="{strip_y + SHELF_STRIP_H - 1}" ' f'<text x="{front_x + COL_W // 2}" y="{strip_y + SHELF_STRIP_H - 1}" '
f'text-anchor="middle" fill="#333" font-size="9">{_esc(sname)}</text>' f'text-anchor="middle" fill="#333" font-size="9">{_esc(sname)}</text>'
) )
p.append("</a>")
for fm in sorted(shelves, key=lambda s: s.get("hostname", "")): for fm in sorted(shelves, key=lambda s: s.get("hostname", "")):
draw_shelf(fm) draw_shelf(fm)
@ -559,7 +623,11 @@ def render_page(rack: str, items: list[dict]) -> str:
lines.append("") lines.append("")
lines.append("## Elevation") lines.append("## Elevation")
lines.append("") lines.append("")
lines.append(f"![Rack {rack} elevation]({rack}-elevation.svg)") lines.append('<div class="rack-elevation">')
lines.append(render_svg(rack, items).rstrip())
lines.append("</div>")
lines.append("")
lines.append(f"[Download SVG]({rack}-elevation.svg)")
lines.append("") lines.append("")
power = render_power(rack, items) power = render_power(rack, items)
if power: if power:

View file

@ -117,7 +117,9 @@ def test_render_page_has_banner_image_and_table():
items = [item(hostname="mf00", rack_u=1, u_height=2, rack_face="front")] items = [item(hostname="mf00", rack_u=1, u_height=2, rack_face="front")]
page = gen_rack.render_page("rack01", items) page = gen_rack.render_page("rack01", items)
assert "do not edit by hand" in page assert "do not edit by hand" in page
assert "![Rack rack01 elevation](rack01-elevation.svg)" in page assert '<div class="rack-elevation">' in page
assert "<svg" in page
assert "[Download SVG](rack01-elevation.svg)" in page
assert "../../hardware/mf00.md" in page assert "../../hardware/mf00.md" in page
assert "U1U2" in page assert "U1U2" in page
@ -632,3 +634,43 @@ def test_render_page_lists_mounted_devices():
assert "../../hardware/srv01.md" in page assert "../../hardware/srv01.md" in page
assert "front · shf01/1" in page assert "front · shf01/1" in page
assert "U37U46" in page # mounted device shows its shelf's U-range assert "U37U46" in page # mounted device shows its shelf's U-range
def test_svg_boxes_link_to_host_pages():
items = [item(hostname="srv04", rack_u=5, u_height=1, rack_face="front")]
svg = gen_rack.render_svg("rack01", items)
assert '<a href="/hardware/srv04/">' in svg
assert "<title>" in svg
def test_svg_status_border_styles():
staging = gen_rack.render_svg("rack01", [
item(hostname="a", rack_u=1, u_height=1, rack_face="front",
status="staging")])
broken = gen_rack.render_svg("rack01", [
item(hostname="b", rack_u=1, u_height=1, rack_face="front",
status="broken")])
assert 'stroke-dasharray="4 2"' in staging
assert 'stroke="#e15759"' in broken and 'stroke-width="3"' in broken
def test_svg_tooltip_has_cluster_and_placement():
items = [item(hostname="srv01", rack_u=1, u_height=1, rack_face="front",
status="staging", cluster="tappaas")]
svg = gen_rack.render_svg("rack01", items)
assert "cluster: tappaas" in svg
assert "U1" in svg
def test_svg_has_responsive_style():
svg = gen_rack.render_svg("rack01", [])
assert "max-width:100%" in svg
def test_render_page_inlines_svg_with_download_link():
items = [item(hostname="srv04", rack_u=5, u_height=1, rack_face="front")]
page = gen_rack.render_page("rack01", items)
assert '<div class="rack-elevation">' in page
assert "<svg" in page
assert "[Download SVG](rack01-elevation.svg)" in page
assert "![Rack rack01 elevation]" not in page