Files
2026-05-31 16:07:30 +02:00

194 lines
5.9 KiB
Plaintext

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Node Balancer</title>
<style>
body { font-family: sans-serif; margin: 20px; }
svg { border:1px solid #ccc; width:100%; height:500px; background:#f9f9f9; }
.node { fill:#4CAF50; stroke:#333; stroke-width:2; }
.pod { fill:#2196F3; stroke:#000; stroke-width:1; }
.label { font-size:14px; text-anchor:middle; dominant-baseline:middle; }
.pod-label { font-size:14px; text-anchor:middle; dominant-baseline:middle; fill:#fff; }
</style>
</head>
<body>
<h2>Node Balancer</h2>
<svg id="canvas"></svg>
<script>
const svg = document.getElementById("canvas");
const nodePositions = {};
const nodeY = 50;
const nodeWidth = 100;
const nodeHeight = 40;
let animationDuration = 2000;
// Store pod stacks per node
const podsPerNode = {};
// Fetch animation duration from server
fetch("/api/config")
.then(res => res.json())
.then(cfg => { animationDuration = cfg.animationDurationMs; });
// Draw nodes
fetch("/api/nodes")
.then(res => res.json())
.then(nodeNames => {
nodeNames.forEach((name,i)=>{
const x = 50 + i*200;
nodePositions[name] = {x: x + nodeWidth/2, y: nodeY + nodeHeight/2};
// Node box
const rect = document.createElementNS("http://www.w3.org/2000/svg","rect");
rect.setAttribute("x", x);
rect.setAttribute("y", nodeY);
rect.setAttribute("width", nodeWidth);
rect.setAttribute("height", nodeHeight);
rect.setAttribute("class","node");
svg.appendChild(rect);
// Node label
const label = document.createElementNS("http://www.w3.org/2000/svg","text");
label.setAttribute("x", x + nodeWidth/2);
label.setAttribute("y", nodeY + nodeHeight/2);
label.setAttribute("class","label");
label.textContent = name;
svg.appendChild(label);
podsPerNode[name] = [];
});
// Fetch initial pods after nodes are drawn
fetch("/api/pods")
.then(res => res.json())
.then(podsByNode => {
for(const nodeName in podsByNode){
podsByNode[nodeName].forEach(podName => addPod(podName, nodeName));
}
});
});
// Add a pod to the stack under a node
function addPod(podName, nodeName){
if(!nodePositions[nodeName]) return;
const stack = podsPerNode[nodeName] || [];
const index = stack.length;
const x = nodePositions[nodeName].x - nodeWidth/2;
const y = nodeY + nodeHeight + index*25;
const g = document.createElementNS("http://www.w3.org/2000/svg","g");
const rect = document.createElementNS("http://www.w3.org/2000/svg","rect");
rect.setAttribute("x", x);
rect.setAttribute("y", y);
rect.setAttribute("width", nodeWidth);
rect.setAttribute("height", 20);
rect.setAttribute("class","pod");
g.appendChild(rect);
const text = document.createElementNS("http://www.w3.org/2000/svg","text");
text.setAttribute("x", x + nodeWidth/2);
text.setAttribute("y", y + 10); // vertical center
text.setAttribute("class","pod-label");
// truncate text to fit box
const maxChars = Math.floor(nodeWidth / 8); // rough estimate
text.textContent = podName.length > maxChars ? podName.substr(0, maxChars) : podName;
g.appendChild(text);
svg.appendChild(g);
stack.push(g);
podsPerNode[nodeName] = stack;
}
// Animate pod moving from one node to another
function flyPod(podName, fromNode, toNode){
// Remove pod from fromNode stack
if (fromNode && podsPerNode[fromNode]) {
podsPerNode[fromNode] = podsPerNode[fromNode].filter(g => {
const text = g.querySelector("text");
return text && text.textContent !== podName;
});
redrawPods(fromNode);
}
// Add pod to toNode stack (temporarily for animation)
const g = document.createElementNS("http://www.w3.org/2000/svg","g");
const rect = document.createElementNS("http://www.w3.org/2000/svg","rect");
rect.setAttribute("width", nodeWidth);
rect.setAttribute("height", 20);
rect.setAttribute("class","pod");
g.appendChild(rect);
const text = document.createElementNS("http://www.w3.org/2000/svg","text");
text.textContent = podName;
g.appendChild(text);
svg.appendChild(g);
let start = nodePositions[fromNode] || nodePositions[toNode];
let end = nodePositions[toNode];
if(!start || !end) return;
let progress = 0;
const steps = 60;
const dx = (end.x - start.x)/steps;
const dy = (end.y - start.y)/steps;
const anim = setInterval(()=>{
progress++;
const x = start.x - nodeWidth/2 + dx*progress;
const y = start.y + dy*progress;
rect.setAttribute("x", x);
rect.setAttribute("y", y);
text.setAttribute("x", x + nodeWidth/2);
text.setAttribute("y", y + 10);
if(progress>=steps){
clearInterval(anim);
svg.removeChild(g);
// Add the pod to the toNode stack permanently
addPod(podName, toNode);
}
}, animationDuration/steps);
}
// redraw all pods on a node (new)
function redrawPods(nodeName){
if(!podsPerNode[nodeName]) return;
podsPerNode[nodeName].forEach((g, idx)=>{
const rect = g.querySelector("rect");
const text = g.querySelector("text");
const x = nodePositions[nodeName].x - nodeWidth/2;
const y = nodeY + nodeHeight + 20 + idx*25;
rect.setAttribute("x", x);
rect.setAttribute("y", y);
text.setAttribute("x", x + nodeWidth/2);
text.setAttribute("y", y + 10);
});
}
// SSE stream
const evtSource = new EventSource("/api/stream");
evtSource.onmessage = e => {
const evt = JSON.parse(e.data);
if(evt.fromNode === evt.toNode){
// same node, just redraw stack
redrawPods(evt.toNode);
return;
}
if(!evt.fromNode){ // new pod appears
addPod(evt.pod, evt.toNode);
redrawPods(evt.toNode);
} else { // pod moves
flyPod(evt.pod, evt.fromNode, evt.toNode);
}
};
</script>
</body>
</html>