This commit is contained in:
2025-01-26 16:58:18 -05:00
commit 788526b029
9 changed files with 1282 additions and 0 deletions

5
static/fan.svg Normal file
View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg fill="#000000" width="800px" height="800px" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M12 3.48154C7.29535 3.48154 3.48148 7.29541 3.48148 12.0001C3.48148 16.7047 7.29535 20.5186 12 20.5186C16.7046 20.5186 20.5185 16.7047 20.5185 12.0001C20.5185 7.29541 16.7046 3.48154 12 3.48154ZM2 12.0001C2 6.47721 6.47715 2.00006 12 2.00006C17.5228 2.00006 22 6.47721 22 12.0001C22 17.5229 17.5228 22.0001 12 22.0001C6.47715 22.0001 2 17.5229 2 12.0001Z"/>
<path d="M12 11.3C11.8616 11.3 11.7262 11.3411 11.6111 11.418C11.496 11.4949 11.4063 11.6042 11.3533 11.7321C11.3003 11.86 11.2864 12.0008 11.3134 12.1366C11.3405 12.2724 11.4071 12.3971 11.505 12.495C11.6029 12.5929 11.7277 12.6596 11.8634 12.6866C11.9992 12.7136 12.14 12.6997 12.2679 12.6467C12.3958 12.5937 12.5051 12.504 12.582 12.3889C12.6589 12.2738 12.7 12.1385 12.7 12C12.7 11.8144 12.6262 11.6363 12.495 11.505C12.3637 11.3738 12.1857 11.3 12 11.3ZM12.35 5.00002C15.5 5.00002 15.57 7.49902 13.911 8.32502C13.6028 8.50778 13.3403 8.75856 13.1438 9.05822C12.9473 9.35787 12.8218 9.69847 12.777 10.054C13.1117 10.1929 13.4073 10.4116 13.638 10.691C16.2 9.29102 19 9.84401 19 12.35C19 15.5 16.494 15.57 15.675 13.911C15.4869 13.6029 15.232 13.341 14.9291 13.1448C14.6262 12.9485 14.283 12.8228 13.925 12.777C13.7844 13.1108 13.566 13.406 13.288 13.638C14.688 16.221 14.128 19 11.622 19C8.5 19 8.423 16.494 10.082 15.668C10.3852 15.4828 10.644 15.2332 10.84 14.9368C11.036 14.6404 11.1644 14.3046 11.216 13.953C10.8729 13.8188 10.5711 13.5967 10.341 13.309C7.758 14.695 5 14.149 5 11.65C5 8.50002 7.478 8.42302 8.304 10.082C8.48945 10.3888 8.74199 10.6496 9.04265 10.8448C9.34332 11.0399 9.68431 11.1645 10.04 11.209C10.1748 10.8721 10.3971 10.5772 10.684 10.355C9.291 7.80001 9.844 5.00002 12.336 5.00002H12.35Z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

23
static/fan2.svg Normal file
View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg version="1.1" id="Layer_1" xmlns:sketch="http://www.bohemiancoding.com/sketch/ns"
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="800px" height="800px"
viewBox="0 0 64 64" enable-background="new 0 0 64 64" xml:space="preserve">
<title>Fan</title>
<desc>Created with Sketch.</desc>
<g id="Page-1" sketch:type="MSPage">
<g id="Fan" transform="translate(1.000000, 1.000000)" sketch:type="MSLayerGroup">
<circle id="Oval_1_" sketch:type="MSShapeGroup" fill="none" stroke="#6B6C6E" stroke-width="2" cx="31" cy="31" r="31">
</circle>
<path id="Shape" sketch:type="MSShapeGroup" fill="none" stroke="#6B6C6E" stroke-width="2" d="M51,22c-6.2,0-9.8,4.5-14.7,7.1
c-0.6-1.6-1.9-2.8-3.5-3.3c2.6-5,7-8.7,7-14.9C39.8,6,35.9,4,31,4s-8.8,2.1-8.8,6.9c0,6.2,4.4,9.9,7,14.9
c-1.6,0.5-2.8,1.8-3.4,3.3c-4.9-2.6-8.5-7.1-14.7-7.1c-4.9,0-7,4.1-7,8.9s2.1,9,7,9c6.2,0,9.8-4.5,14.7-7.1
c0.6,1.6,1.8,2.9,3.4,3.4c-2.6,4.9-6.9,8.6-6.9,14.8c0,4.9,3.9,6.9,8.8,6.9s8.8-2.1,8.8-6.9c0-6.2-4.3-9.9-6.9-14.8
c1.6-0.6,2.9-1.8,3.4-3.4c4.9,2.6,8.5,7.1,14.7,7.1c4.9,0,7-4.1,7-9S55.9,22,51,22L51,22z"/>
<ellipse id="Oval" sketch:type="MSShapeGroup" fill="none" stroke="#6B6C6E" stroke-width="2" cx="30.9" cy="31" rx="1.9" ry="2">
</ellipse>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

361
static/script.js Normal file
View File

@ -0,0 +1,361 @@
const container = document.getElementById('patch-container');
const svgOverlay = document.getElementById('cable-overlay');
const portsState = {};
const LED_COLORS = [
// Gruvbox dark bright colors
'#fb4934', // red
'#b8bb26', // green
'#fabd2f', // yellow
'#83a598', // blue
'#d3869b', // purple
'#8ec07c', // aqua
'#fe8019', // orange
];
const CABLE_COLORS = [
// Gruvbox light dark colors
'#9d0006', // red
'#79740e', // green
'#b57614', // yellow
'#076678', // blue
'#8f3f71', // purple
'#427b58', // aqua
'#af3a03', // orange
];
let cables = [];
let dragSource = null;
let tempCablePath = null;
const NUM_SEGMENTS = 64;
const GRAVITY = 0.6;
const DAMPING = 0.4;
const CONSTRAINT_ITERATIONS = 15;
const SWAY_AMPLITUDE = 0.05;
const SWAY_FREQUENCY = 0.002;
function getPortCenter(portEl) {
const rect = portEl.getBoundingClientRect();
return {
x: rect.left + rect.width / 2,
y: rect.top + rect.height / 2
};
}
/**********************************************
* LED logic
**********************************************/
function initPortState(portId) {
if (!portsState[portId]) {
portsState[portId] = {
connectionCount: 0,
ledColor: null,
ledIsLit: false,
nextBlinkTime: 0
};
}
}
function addConnection(portId) {
initPortState(portId);
const st = portsState[portId];
st.connectionCount++;
if (st.connectionCount === 1) {
// choose a color if not set
if (!st.ledColor) {
st.ledColor = LED_COLORS[Math.floor(Math.random() * LED_COLORS.length)];
}
// schedule next blink
st.nextBlinkTime = performance.now();
}
}
function removeConnection(portId) {
initPortState(portId);
const st = portsState[portId];
if (st.connectionCount > 0) {
st.connectionCount--;
}
if (st.connectionCount <= 0) {
st.connectionCount = 0;
st.ledIsLit = false;
updatePortLED(portId);
}
}
/** Check if we should toggle LED, then update DOM. */
function updateLEDs(now) {
for (const portId in portsState) {
const st = portsState[portId];
if (st.connectionCount > 0) {
if (now >= st.nextBlinkTime) {
st.ledIsLit = !st.ledIsLit;
const interval = 50 + Math.random() * 150; // 200..800 ms
st.nextBlinkTime = now + interval;
}
}
updatePortLED(portId);
}
}
function updatePortLED(portId) {
const st = portsState[portId];
const portEl = document.getElementById(portId);
if (!portEl) return;
const ledEl = portEl.querySelector('.led');
if (!ledEl) return;
if (st.connectionCount > 0 && st.ledColor) {
ledEl.style.background = st.ledIsLit ? st.ledColor : '#222';
} else {
ledEl.style.background = '#222';
}
}
/**********************************************
* Cable creation & removal
**********************************************/
function randomColor() {
return CABLE_COLORS[Math.floor(Math.random() * CABLE_COLORS.length)];
}
function createCablePath(color) {
const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
path.setAttribute('stroke', color);
path.setAttribute('fill', 'none');
path.setAttribute('stroke-width', '3');
path.classList.add('cable-path');
// We want to click cables to remove them
path.addEventListener('click', onCableClick);
svgOverlay.appendChild(path);
return path;
}
function createRope(sourceEl, targetEl, numSegments) {
const s = getPortCenter(sourceEl);
const t = getPortCenter(targetEl);
const rope = [];
for (let i = 0; i <= numSegments; i++) {
const alpha = i / numSegments;
rope.push({
x: s.x + (t.x - s.x) * alpha,
y: s.y + (t.y - s.y) * alpha,
vx: 0, vy: 0
});
}
return rope;
}
function connect(sourceId, targetId, color) {
if (!sourceId || !targetId || sourceId === targetId) return;
const sourceEl = document.getElementById(sourceId);
const targetEl = document.getElementById(targetId);
if (!sourceEl || !targetEl) return;
color = color || randomColor();
const rope = createRope(sourceEl, targetEl, NUM_SEGMENTS);
const dx = rope[rope.length - 1].x - rope[0].x;
const dy = rope[rope.length - 1].y - rope[0].y;
const totalDist = Math.sqrt(dx * dx + dy * dy);
const restLength = totalDist / NUM_SEGMENTS;
const pathEl = createCablePath(color);
const swayOffset = Math.random() * 1000;
cables.push({
sourceId, targetId,
pathEl, ropeSegments: rope,
restLength, color, swayOffset
});
addConnection(sourceId);
addConnection(targetId);
}
function removeCable(cable) {
svgOverlay.removeChild(cable.pathEl);
cables = cables.filter(c => c !== cable);
removeConnection(cable.sourceId);
removeConnection(cable.targetId);
}
function onCableClick(e) {
const pathEl = e.currentTarget;
const cable = cables.find(c => c.pathEl === pathEl);
if (cable) removeCable(cable);
}
/**********************************************
* Dragging logic (user can draw cables)
**********************************************/
function handlePortMouseDown(e) {
e.preventDefault();
dragSource = e.currentTarget;
const color = randomColor();
tempCablePath = createCablePath(color);
tempCablePath.style.pointerEvents = 'none';
}
function handleMouseMove(e) {
if (!dragSource || !tempCablePath) return;
const s = getPortCenter(dragSource);
const mx = e.clientX, my = e.clientY;
const midX = (s.x + mx) / 2, midY = (s.y + my) / 2;
const d = `
M ${s.x},${s.y}
C ${midX},${s.y}
${midX},${my}
${mx},${my}
`;
tempCablePath.setAttribute('d', d);
}
function handleMouseUp(e) {
const dropTargetEl = document.elementFromPoint(e.clientX, e.clientY);
const portEl = dropTargetEl && dropTargetEl.closest('.port');
if (portEl) {
const sourceId = dragSource.id;
const targetId = portEl.id;
if (sourceId !== targetId) {
const color = tempCablePath.getAttribute('stroke');
svgOverlay.removeChild(tempCablePath);
tempCablePath = null;
connect(sourceId, targetId, color);
} else {
svgOverlay.removeChild(tempCablePath);
tempCablePath = null;
}
} else {
svgOverlay.removeChild(tempCablePath);
tempCablePath = null;
}
dragSource = null;
}
/***********************************************
* NAS drive-lights random blinking
***********************************************/
let driveLights = [];
function initDriveLights() {
driveLights = document.querySelectorAll('.drive-light');
driveLights.forEach(light => {
light.dataset.nextBlinkTime = '0';
});
}
function updateDriveLights(timestamp) {
driveLights.forEach(light => {
let nextBlink = parseFloat(light.dataset.nextBlinkTime) || 0;
if (timestamp >= nextBlink) {
light.classList.toggle('lit');
const interval = 300 + Math.random() * 500; // 300..800 ms
light.dataset.nextBlinkTime = (timestamp + interval).toString();
}
});
}
/**********************************************
* Main animation loop
**********************************************/
function animate(timestamp) {
updateLEDs(timestamp);
updateDriveLights(timestamp);
const dt = 1.0;
cables.forEach(cable => {
const rope = cable.ropeSegments;
const n = rope.length;
if (n < 2) return;
const sourceEl = document.getElementById(cable.sourceId);
const targetEl = document.getElementById(cable.targetId);
if (!sourceEl || !targetEl) return;
const s = getPortCenter(sourceEl);
const t = getPortCenter(targetEl);
// anchors
rope[0].x = s.x; rope[0].y = s.y;
rope[0].vx = 0; rope[0].vy = 0;
rope[n - 1].x = t.x; rope[n - 1].y = t.y;
rope[n - 1].vx = 0; rope[n - 1].vy = 0;
// 1) gravity + wind for interior segments
for (let i = 1; i < n - 1; i++) {
rope[i].vy += GRAVITY;
const wave = Math.sin((timestamp + cable.swayOffset + i * 50) * SWAY_FREQUENCY);
rope[i].vx += wave * SWAY_AMPLITUDE;
rope[i].vx *= DAMPING;
rope[i].vy *= DAMPING;
rope[i].x += rope[i].vx * dt;
rope[i].y += rope[i].vy * dt;
}
// 2) constraints
for (let iter = 0; iter < CONSTRAINT_ITERATIONS; iter++) {
for (let i = 0; i < n - 1; i++) {
let p1 = rope[i], p2 = rope[i + 1];
let dx = p2.x - p1.x, dy = p2.y - p1.y;
let dist = Math.sqrt(dx * dx + dy * dy);
if (dist < 0.001) dist = 0.001;
let diff = (dist - cable.restLength) / dist;
if (i !== 0) {
p1.x += 0.5 * dx * diff;
p1.y += 0.5 * dy * diff;
}
if (i + 1 !== n - 1) {
p2.x -= 0.5 * dx * diff;
p2.y -= 0.5 * dy * diff;
}
}
}
// 3) recalc velocities
for (let i = 1; i < n - 1; i++) {
rope[i].vx = rope[i].x - (rope[i].x - rope[i].vx * dt);
rope[i].vy = rope[i].y - (rope[i].y - rope[i].vy * dt);
}
// 4) update path
let pathD = `M ${rope[0].x},${rope[0].y}`;
for (let i = 1; i < n; i++) {
pathD += ` L ${rope[i].x},${rope[i].y}`;
}
cable.pathEl.setAttribute('d', pathD);
});
requestAnimationFrame(animate);
}
/**********************************************
* Startup
**********************************************/
initDriveLights();
document.querySelectorAll('.port').forEach(portEl => {
portEl.addEventListener('mousedown', handlePortMouseDown);
});
container.addEventListener('mousemove', handleMouseMove);
container.addEventListener('mouseup', handleMouseUp);
requestAnimationFrame(animate);
connect('r1s0_portA', 'r1s2_portA',randomColor());
connect('r1s2_portB', 'r2s1_portA',randomColor());
connect('r1s2_portC', 'r1s3_portA',randomColor());
connect('r2s1_portB', 'r2s2_portS',randomColor());
connect('r2s2_portA', 'r2s3_portA',randomColor());
connect('r2s2_portB', 'r3s1_portA',randomColor());
connect('r2s2_portC', 'r3s2_portA',randomColor());
connect('r2s2_portD', 'r3s3_portA',randomColor());
connect('r2s1_portD', 'r2s1_portG',randomColor());
connect('r2s1_portK', 'r3s2_portB',randomColor());

323
static/style.css Normal file
View File

@ -0,0 +1,323 @@
* {
margin: 0;
padding: 0
}
/* Basic reset */
html,
body {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
background: #1d2021;
overflow: hidden;
/* No default scrollbars */
font-family: "JetBrains Mono", serif;
font-optical-sizing: auto;
font-weight: 400;
font-style: normal;
font-size: 20px;
}
h1 {
font-size: 48px;
color: #ebdbb2;
}
h2 {
font-size: 36px;
color: #ebdbb2;
}
h3 {
font-size: 28px;
color: #ebdbb2;
}
/*******************************************
* Main Container (holds racks + cables)
*******************************************/
#patch-container {
position: relative;
width: 100%;
height: 100%;
overflow: hidden;
}
/* SVG overlay for cables, on top of racks */
#cable-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 9999;
/* put cables on top */
pointer-events: none;
/* let clicks pass through by default */
}
/* Each cable path can be clicked on => remove */
.cable-path {
pointer-events: auto;
/* re-enable clicks for cable paths specifically */
cursor: pointer;
transition: stroke 0.2s;
}
.cable-path:hover {
stroke: #cc241d !important;
}
/*******************************************
* Data center (the racks)
*******************************************/
#datacenter {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
/* If you want horizontal scrolling if there are many racks, use:
overflow-x: auto;
*/
overflow-x: auto;
overflow-y: hidden;
z-index: 1;
/* behind cable overlay */
}
.rack {
width: 16em;
/* each racks width */
/* fill all available vertical space */
background: #282828;
border: 2px solid #3c3836;
margin: 10px;
box-sizing: border-box;
display: flex;
flex-direction: column;
/* stack servers top to bottom */
}
.rack-title {
text-align: center;
text-transform: uppercase;
font-size: 14px;
color: #ebdbb2;
margin: 8px 0;
letter-spacing: 1px;
}
.server {
background: #32302f;
border: 1px solid #3c3836;
margin: 6px;
/* spacing within the rack */
}
.server-under-construction {
/* Diagonal stripes:
repeating-linear-gradient(angle, color1 start, color1 end, color2 start, color2 end) */
background: repeating-linear-gradient(
45deg,
#282828 0,
#282828 12px,
#32302f 12px,
#32302f 24px
);
color:#000;
border: 2px dashed #282828; /* Give it a dashed border for effect */
}
.server-row {
padding: 5px 8px;
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: space-between;
}
.text-row {
font-size: 14px;
color: #ebdbb2;
}
.port-row {
gap: 8px;
display: flex;
justify-content: right;
}
/* If you want a spacer between servers, you can use a simple div with .spacer. */
.spacer {
height: 20px;
}
/*******************************************
* Port + LED styling
*******************************************/
.port {
position: relative;
width: 1rem;
height: 1rem;
background: #32302f;
border: 2px solid #504945;
border-radius: 50%;
cursor: pointer;
}
.port:hover {
background: #7c6f64;
border-color: #ebdbb2;
}
a:link {
color: #b16286;
}
/* visited link */
a:visited {
color: #8f3f71;
}
/* mouse over link */
a:hover {
color: #d3869b;
}
/* selected link */
a:active {
color: #b16286;
}
.led {
position: absolute;
top: -5px;
right: -5px;
width: .4rem;
height: .4rem;
background: #1d2021;
/* off state */
border: 2px solid #504945;
border-radius: 50%;
box-shadow: 0 0 2px #000;
}
.temp-row {
position: relative;
flex: 1;
}
.temp-label {
color: #eee;
font-size: 11px;
margin-right: 8px;
}
.temp-bar {
flex: 1;
height: 10px;
background: linear-gradient(to right, #f80, #f00);
box-shadow: 0 0 6px rgba(255,80,0,0.5);
animation: flickerHeat 1.5s infinite ease-in-out alternate;
}
@keyframes flickerHeat {
0% { opacity: 1; transform: scaleX(1); }
100% { opacity: 0.7; transform: scaleX(1.05); }
}
/***************************************************
* POWER USAGE LIGHTS (Random Blink)
***************************************************/
.power-lights-row {
gap: 4px;
}
.power-light {
width: 10px; height: 10px;
background: #222;
border: 2px solid #555;
border-radius: 2px;
box-shadow: 0 0 2px #000;
}
.power-light.lit {
background: #0f0; /* or something bright to indicate usage */
}
/************************************************
* FAN SPACER: Animated fans
************************************************/
.fans {
background: #7c6f64;
margin: 6px;
/* spacing within the rack */
}
.fan-row {
display: flex;
gap: 10px;
justify-content: center;
padding: 5px 0;
}
.fan {
width: 5rem;
height: 5rem;
border-radius: 50%;
background-image: url('fan.svg');
filter: invert(29%) sepia(3%) saturate(1342%) hue-rotate(338deg) brightness(93%) contrast(90%);
animation: spin 1s linear infinite;
transform-origin: center;
background-size: contain;
background-repeat: no-repeat;
background-position: center;
transition: all 1s linear;
}
.fan:hover {
animation: 5s;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
/************************************************
* NAS SPACER: Random blinking drive lights
************************************************/
.nas {
background: #504945;
border: 1px solid #7c6f64;
margin: 6px;
/* spacing within the rack */
}
.drive-lights-row {
display: flex;
gap: 4px;
justify-content: center;
padding: 5px 0;
flex-wrap: wrap;
/* if many lights, wrap to next line */
}
.drive-light {
width: 12px;
height: 12px;
background: #1d2021;
/* off by default */
border: 2px solid #504945;
border-radius: 2px;
box-shadow: 0 0 2px #000;
}
.drive-light.lit {
background: #b8bb26;
/* or any color you like */
}