eindelijk weer eens een push

This commit is contained in:
allard
2026-05-31 16:07:30 +02:00
parent 01cff8e165
commit ff21c258e0
2747 changed files with 302316 additions and 131101 deletions
+97
View File
@@ -0,0 +1,97 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Node Balancer WebUI</title>
<style>
body { font-family: sans-serif; margin: 20px; }
svg { border:1px solid #ccc; width:100%; height:300px; background:#f9f9f9; }
.node { fill:#4CAF50; stroke:#333; stroke-width:2; }
.pod { fill:#2196F3; stroke:#000; stroke-width:1; }
.pod text { font-size:10px; fill:#fff; text-anchor:middle; dominant-baseline:middle; }
.label { font-size:14px; text-anchor:middle; }
</style>
</head>
<body>
<h2>Node Balancer</h2>
<svg id="canvas"></svg>
<script>
// SVG setup
const svg = document.getElementById("canvas");
const nodePositions = {};
const nodeY = 50;
const nodeWidth = 100;
const nodeHeight = 40;
// Fetch node names from the API
fetch("/api/nodes")
.then(res => res.json())
.then(nodeNames => {
// Draw nodes
nodeNames.forEach((name,i)=>{
const x = 50 + i*200;
nodePositions[name] = {x: x + nodeWidth/2, y: nodeY + nodeHeight/2};
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);
const label = document.createElementNS("http://www.w3.org/2000/svg","text");
label.setAttribute("x", x + nodeWidth/2);
label.setAttribute("y", nodeY + nodeHeight/2 + 25);
label.setAttribute("class","label");
label.textContent = name;
svg.appendChild(label);
});
});
// Animate a pod
function flyPod(podName, fromNode, toNode){
const g = document.createElementNS("http://www.w3.org/2000/svg","g");
const circle = document.createElementNS("http://www.w3.org/2000/svg","circle");
circle.setAttribute("r", 15);
circle.setAttribute("class","pod");
g.appendChild(circle);
const text = document.createElementNS("http://www.w3.org/2000/svg","text");
text.textContent = podName;
g.appendChild(text);
svg.appendChild(g);
let start = nodePositions[fromNode];
let end = nodePositions[toNode];
if(!start || !end) return; // safety check
let progress = 0;
const duration = 2000;
const steps = 60;
const dx = (end.x - start.x)/steps;
const dy = (end.y - start.y)/steps;
const anim = setInterval(()=>{
progress++;
const cx = start.x + dx*progress;
const cy = start.y + dy*progress;
circle.setAttribute("cx", cx);
circle.setAttribute("cy", cy);
text.setAttribute("x", cx);
text.setAttribute("y", cy);
if(progress>=steps){
clearInterval(anim);
setTimeout(()=>svg.removeChild(g),1000);
}
}, duration/steps);
}
// SSE stream
const evtSource = new EventSource("/api/stream");
evtSource.onmessage = e => {
const evt = JSON.parse(e.data);
flyPod(evt.pod, evt.fromNode, evt.toNode);
};
</script>
</body>
</html>
+268
View File
@@ -0,0 +1,268 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Node Balancer</title>
<style>
html, body {
margin: 0;
padding: 0;
min-height: 100%;
}
body { font-family: sans-serif; margin: 20px; }
svg { border:1px solid #ccc; width:100%; 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; }
header {
display: flex;
align-items: center;
margin-bottom: 20px;
background-color: #f0f0f0; /* light gray background for the header */
padding: 10px 15px;
border-radius: 8px;
}
header .logo {
font-weight: bold;
font-size: 18px;
color: #2196F3; /* blue logo text */
margin-right: 15px;
padding: 5px 10px;
background-color: #333; /* dark background behind logo text */
color: #fff;
border-radius: 4px;
font-family: monospace;
}
header .title {
font-size: 18px;
}
header .title span {
margin-left: 10px;
color: #555;
font-weight: normal;
}
header .cluster-label {
margin-left: 20px;
color: #333;
font-weight: normal;
}
</style>
</head>
<body>
<header>
<div class="logo">allarddcs</div>
<div class="title">
Node Balancer <span id="version">v?</span>
<span class="cluster-label">Cluster: <span id="cluster-name">?</span></span>
</div>
</header>
<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;
document.getElementById("version").textContent = "v" + cfg.version;
document.getElementById("cluster-name").textContent = cfg.clusterName;
});
// 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;
updateSVGHeight();
}
function updateSVGHeight() {
let maxY = nodeY + nodeHeight; // start with top node
for (const nodeName in podsPerNode) {
const stack = podsPerNode[nodeName];
if (!stack) continue;
if (stack.length === 0) continue;
const stackHeight = stack.length * (20 + 5) +40 ; // pod height + spacing
const bottomY = nodeY + nodeHeight + stackHeight;
if (bottomY > maxY) maxY = bottomY;
}
// optional extra padding at bottom
svg.setAttribute("height", maxY + 20);
}
// Animate pod moving from one node to another
function flyPod(podName, fromNode, toNode) {
if (!fromNode || !toNode) return;
const fromStack = podsPerNode[fromNode];
const idx = fromStack.findIndex(g => g.querySelector("text").textContent === podName);
if (idx === -1) return;
// Grab the actual pod <g> element
const g = fromStack[idx];
// Remove it from leaving stack array
fromStack.splice(idx, 1);
redrawPods(fromNode); // redraw remaining pods to close gap
// Reparent pod to SVG root so it can move freely
svg.appendChild(g);
const rect = g.querySelector("rect");
const text = g.querySelector("text");
// Get current position
const startX = parseFloat(rect.getAttribute("x"));
const startY = parseFloat(rect.getAttribute("y"));
// Calculate target position in destination stack
const endX = nodePositions[toNode].x - nodeWidth / 2;
const endY = nodeY + nodeHeight + podsPerNode[toNode].length * 25;
let progress = 0;
const steps = 60;
const dx = (endX - startX) / steps;
const dy = (endY - startY) / steps;
// Animate pod
const anim = setInterval(() => {
progress++;
rect.setAttribute("x", startX + dx * progress);
rect.setAttribute("y", startY + dy * progress);
text.setAttribute("x", startX + dx * progress + nodeWidth / 2);
text.setAttribute("y", startY + dy * progress + 10);
if (progress >= steps) {
clearInterval(anim);
// Add pod to destination stack permanently
podsPerNode[toNode].push(g);
// Ensure destination stack is redrawn correctly
redrawPods(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);
});
updateSVGHeight();
}
// 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>
+281
View File
@@ -0,0 +1,281 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Node Balancer</title>
<style>
html, body {
margin: 0;
padding: 0;
min-height: 100%;
}
body { font-family: sans-serif; margin: 20px; }
svg { border:1px solid #ccc; width:100%; 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; }
header {
display: flex;
align-items: center;
margin-bottom: 20px;
background-color: #f0f0f0; /* light gray background for the header */
padding: 10px 15px;
border-radius: 8px;
}
header .logo {
font-weight: bold;
font-size: 18px;
color: #2196F3; /* blue logo text */
margin-right: 15px;
padding: 5px 10px;
background-color: #333; /* dark background behind logo text */
color: #fff;
border-radius: 4px;
font-family: monospace;
}
header .title {
font-size: 18px;
}
header .title span {
margin-left: 10px;
color: #555;
font-weight: normal;
}
header .cluster-label {
margin-left: 20px;
color: #333;
font-weight: normal;
}
</style>
</head>
<body>
<header>
<div class="logo">allarddcs</div>
<div class="title">
Node Balancer <span id="version">v?</span>
<span class="cluster-label">Cluster: <span id="cluster-name">?</span></span>
</div>
</header>
<svg id="canvas"></svg>
<script>
const svg = document.getElementById("canvas");
const nodePositions = {};
const nodeY = 50;
const nodeWidth = 120; // slightly wider so text fits better
const nodeHeight = 55; // taller
const podHeight = 36; // taller
const podSpacing = 6; // vertical spacing between pods
const textPaddingY = 6; // vertical padding inside pod
let animationDuration = 2000;
const podsPerNode = {};
// Fetch config
fetch("/api/config")
.then(res => res.json())
.then(cfg => {
animationDuration = cfg.animationDurationMs;
document.getElementById("version").textContent = "v" + cfg.version;
document.getElementById("cluster-name").textContent = cfg.clusterName;
});
// Draw nodes
fetch("/api/nodes")
.then(res => res.json())
.then(nodeNames => {
nodeNames.forEach((name,i)=>{
const x = 50 + i*220;
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.setAttribute("text-anchor","middle");
label.setAttribute("dominant-baseline","middle");
label.textContent = name;
svg.appendChild(label);
podsPerNode[name] = [];
});
fetch("/api/pods")
.then(res => res.json())
.then(podsByNode => {
for(const nodeName in podsByNode){
podsByNode[nodeName].forEach(podName => addPod(podName, nodeName));
}
});
});
// Add pod
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*(podHeight + podSpacing);
const g = document.createElementNS("http://www.w3.org/2000/svg","g");
g.dataset.podName = podName;
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", podHeight);
rect.setAttribute("class","pod");
g.appendChild(rect);
// Pod text (centered, multi-line if needed)
const text = document.createElementNS("http://www.w3.org/2000/svg","text");
text.setAttribute("class","pod-label");
text.setAttribute("text-anchor","middle");
text.setAttribute("x", x + nodeWidth/2);
if(podName.length <= 12){
text.setAttribute("y", y + podHeight/2 + textPaddingY/2);
text.setAttribute("dominant-baseline","middle");
text.textContent = podName;
} else {
const line1 = podName.substring(0,12);
const line2 = podName.substring(12);
text.setAttribute("y", y + textPaddingY + 6);
const t1 = document.createElementNS("http://www.w3.org/2000/svg","tspan");
t1.setAttribute("x", x + nodeWidth/2);
t1.textContent = line1;
const t2 = document.createElementNS("http://www.w3.org/2000/svg","tspan");
t2.setAttribute("x", x + nodeWidth/2);
t2.setAttribute("dy", "12");
t2.textContent = line2;
text.appendChild(t1);
text.appendChild(t2);
}
g.appendChild(text);
svg.appendChild(g);
stack.push(g);
podsPerNode[nodeName] = stack;
updateSVGHeight();
}
// Update SVG height
function updateSVGHeight() {
let maxY = nodeY + nodeHeight;
for(const nodeName in podsPerNode){
const stack = podsPerNode[nodeName];
if(!stack || stack.length===0) continue;
const bottomY = nodeY + nodeHeight + stack.length*(podHeight + podSpacing);
if(bottomY > maxY) maxY = bottomY;
}
svg.setAttribute("height", maxY + 20);
}
// Fly pod animation
function flyPod(podName, fromNode, toNode){
if(!fromNode || !toNode) return;
const fromStack = podsPerNode[fromNode];
const idx = fromStack.findIndex(g => g.dataset.podName===podName);
if(idx===-1) return;
const g = fromStack[idx];
fromStack.splice(idx,1);
redrawPods(fromNode);
svg.appendChild(g);
const rect = g.querySelector("rect");
const text = g.querySelector("text");
const startX = parseFloat(rect.getAttribute("x"));
const startY = parseFloat(rect.getAttribute("y"));
const endX = nodePositions[toNode].x - nodeWidth/2;
const endY = nodeY + nodeHeight + podsPerNode[toNode].length*(podHeight+podSpacing);
let progress = 0;
const steps = 60;
const dx = (endX - startX)/steps;
const dy = (endY - startY)/steps;
const anim = setInterval(()=>{
progress++;
const nx = startX + dx*progress;
const ny = startY + dy*progress;
rect.setAttribute("x", nx);
rect.setAttribute("y", ny);
text.setAttribute("x", nx + nodeWidth/2);
text.setAttribute("y", ny + podHeight/2 + textPaddingY/2);
if(progress>=steps){
clearInterval(anim);
podsPerNode[toNode].push(g);
redrawPods(toNode);
}
}, animationDuration/steps);
}
// Redraw pods on a node
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 + idx*(podHeight+podSpacing);
rect.setAttribute("x", x);
rect.setAttribute("y", y);
text.setAttribute("x", x + nodeWidth/2);
text.setAttribute("y", y + podHeight/2 + textPaddingY/2);
});
updateSVGHeight();
}
// SSE stream
const evtSource = new EventSource("/api/stream");
evtSource.onmessage = e=>{
const evt = JSON.parse(e.data);
if(evt.fromNode===evt.toNode){
redrawPods(evt.toNode);
return;
}
if(!evt.fromNode){
addPod(evt.pod, evt.toNode);
redrawPods(evt.toNode);
} else {
flyPod(evt.pod, evt.fromNode, evt.toNode);
}
};
</script>
</body>
</html>
+262
View File
@@ -0,0 +1,262 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Node Balancer</title>
<style>
html, body {
margin: 0;
padding: 0;
min-height: 100%;
}
body { font-family: sans-serif; margin: 20px; }
svg { border:1px solid #ccc; width:100%; 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; fill:#fff; }
header {
display: flex;
align-items: center;
margin-bottom: 20px;
background-color: #f0f0f0;
padding: 10px 15px;
border-radius: 8px;
}
header .logo {
font-weight: bold;
font-size: 18px;
color: #2196F3;
margin-right: 15px;
padding: 5px 10px;
background-color: #333;
color: #fff;
border-radius: 4px;
font-family: monospace;
}
header .title {
font-size: 18px;
}
header .title span {
margin-left: 10px;
color: #555;
font-weight: normal;
}
header .cluster-label {
margin-left: 20px;
color: #333;
font-weight: normal;
}
</style>
</head>
<body>
<header>
<div class="logo">allarddcs</div>
<div class="title">
Node Balancer <span id="version">v?</span>
<span class="cluster-label">Cluster: <span id="cluster-name">?</span></span>
</div>
</header>
<svg id="canvas"></svg>
<script>
const svg = document.getElementById("canvas");
const nodePositions = {};
const nodeY = 50;
const nodeWidth = 120; // node box width
const nodeHeight = 55; // node box height
const podHeight = 36; // pod box height
const podSpacing = 6; // vertical spacing between pods
let animationDuration = 2000;
const podsPerNode = {};
// Fetch config
fetch("/api/config")
.then(res => res.json())
.then(cfg => {
animationDuration = cfg.animationDurationMs;
document.getElementById("version").textContent = "v" + cfg.version;
document.getElementById("cluster-name").textContent = cfg.clusterName;
});
// Draw nodes
fetch("/api/nodes")
.then(res => res.json())
.then(nodeNames => {
nodeNames.forEach((name,i)=>{
const x = 50 + i*220;
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.setAttribute("text-anchor","middle");
label.setAttribute("dominant-baseline","middle");
label.textContent = name;
svg.appendChild(label);
podsPerNode[name] = [];
});
// Fetch pods after nodes
fetch("/api/pods")
.then(res => res.json())
.then(podsByNode => {
for(const nodeName in podsByNode){
podsByNode[nodeName].forEach(podName => addPod(podName, nodeName));
}
});
});
// Add pod
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*(podHeight + podSpacing);
const g = document.createElementNS("http://www.w3.org/2000/svg","g");
g.dataset.podName = podName;
// Pod rectangle
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", podHeight);
rect.setAttribute("class","pod");
g.appendChild(rect);
const text = document.createElementNS("http://www.w3.org/2000/svg","text");
text.setAttribute("class","pod-label");
text.setAttribute("text-anchor","middle");
text.setAttribute("x", x + nodeWidth/2);
text.setAttribute("y", y); // <-- top of pod rectangle
g.appendChild(text);
// first tspan for top padding
const tspan = document.createElementNS("http://www.w3.org/2000/svg","tspan");
tspan.setAttribute("x", x + nodeWidth/2);
tspan.setAttribute("dy", 6); // top padding inside pod
tspan.textContent = podName;
text.appendChild(tspan);
svg.appendChild(g);
stack.push(g);
podsPerNode[nodeName] = stack;
updateSVGHeight();
}
// Update SVG height
function updateSVGHeight() {
let maxY = nodeY + nodeHeight;
for(const nodeName in podsPerNode){
const stack = podsPerNode[nodeName];
if(!stack || stack.length===0) continue;
const bottomY = nodeY + nodeHeight + stack.length*(podHeight + podSpacing);
if(bottomY > maxY) maxY = bottomY;
}
svg.setAttribute("height", maxY + 20);
}
// Animate pod moving
function flyPod(podName, fromNode, toNode){
if(!fromNode || !toNode) return;
const fromStack = podsPerNode[fromNode];
const idx = fromStack.findIndex(g => g.dataset.podName===podName);
if(idx===-1) return;
const g = fromStack[idx];
fromStack.splice(idx,1);
redrawPods(fromNode);
svg.appendChild(g);
const rect = g.querySelector("rect");
const text = g.querySelector("text");
const startX = parseFloat(rect.getAttribute("x"));
const startY = parseFloat(rect.getAttribute("y"));
const endX = nodePositions[toNode].x - nodeWidth/2;
const endY = nodeY + nodeHeight + podsPerNode[toNode].length*(podHeight+podSpacing);
let progress = 0;
const steps = 60;
const dx = (endX - startX)/steps;
const dy = (endY - startY)/steps;
const anim = setInterval(()=>{
progress++;
const nx = startX + dx*progress;
const ny = startY + dy*progress;
rect.setAttribute("x", nx);
rect.setAttribute("y", ny);
text.setAttribute("x", nx + nodeWidth/2);
// text y remains managed by first tspan dy
if(progress>=steps){
clearInterval(anim);
podsPerNode[toNode].push(g);
redrawPods(toNode);
}
}, animationDuration/steps);
}
// Redraw pods on a node
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 + idx*(podHeight+podSpacing);
rect.setAttribute("x", x);
rect.setAttribute("y", y);
text.setAttribute("x", x + nodeWidth/2);
// leave first tspan dy as 6 for top padding
});
updateSVGHeight();
}
// SSE stream
const evtSource = new EventSource("/api/stream");
evtSource.onmessage = e=>{
const evt = JSON.parse(e.data);
if(evt.fromNode===evt.toNode){
redrawPods(evt.toNode);
return;
}
if(!evt.fromNode){
addPod(evt.pod, evt.toNode);
redrawPods(evt.toNode);
} else {
flyPod(evt.pod, evt.fromNode, evt.toNode);
}
};
</script>
</body>
</html>
+286
View File
@@ -0,0 +1,286 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Node Balancer</title>
<style>
html, body {
margin: 0;
padding: 0;
min-height: 100%;
}
body { font-family: sans-serif; margin: 20px; }
svg { border:1px solid #ccc; width:100%; 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; fill:#fff; }
header {
display: flex;
align-items: center;
margin-bottom: 20px;
background-color: #f0f0f0;
padding: 10px 15px;
border-radius: 8px;
}
header .logo {
font-weight: bold;
font-size: 18px;
color: #2196F3;
margin-right: 15px;
padding: 5px 10px;
background-color: #333;
color: #fff;
border-radius: 4px;
font-family: monospace;
}
header .title {
font-size: 18px;
}
header .title span {
margin-left: 10px;
color: #555;
font-weight: normal;
}
header .cluster-label {
margin-left: 20px;
color: #333;
font-weight: normal;
}
</style>
</head>
<body>
<header>
<div class="logo">allarddcs</div>
<div class="title">
Node Balancer <span id="version">v?</span>
<span class="cluster-label">Cluster: <span id="cluster-name">?</span></span>
</div>
</header>
<svg id="canvas"></svg>
<script>
const svg = document.getElementById("canvas");
const nodePositions = {};
const nodeY = 50;
const nodeWidth = 120;
const nodeHeight = 55;
const podHeight = 36;
const podSpacing = 6;
const textPaddingY = 6;
let animationDuration = 2000;
const podsPerNode = {};
// Fetch config
fetch("/api/config")
.then(res => res.json())
.then(cfg => {
animationDuration = cfg.animationDurationMs;
document.getElementById("version").textContent = "v" + cfg.version;
document.getElementById("cluster-name").textContent = cfg.clusterName;
});
// Draw nodes
fetch("/api/nodes")
.then(res => res.json())
.then(nodeNames => {
nodeNames.forEach((name,i)=>{
const x = 50 + i*220;
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.setAttribute("text-anchor","middle");
label.setAttribute("dominant-baseline","middle");
label.textContent = name;
svg.appendChild(label);
podsPerNode[name] = [];
});
fetch("/api/pods")
.then(res => res.json())
.then(podsByNode => {
for(const nodeName in podsByNode){
podsByNode[nodeName].forEach(podName => addPod(podName, nodeName));
}
});
});
// Truncate podname
function truncatePodName(podName){
const parts = podName.split('-');
const hashRegex = /^[a-z0-9]+$/i;
// Remove last segment if it looks like a hash
if(parts.length > 1 && parts[parts.length - 1].match(hashRegex) && parts[parts.length - 1].length >= 5){
parts.pop();
}
// Check if the new last segment is also a hash (optional second hash)
if(parts.length > 1 && parts[parts.length - 1].match(hashRegex) && parts[parts.length - 1].length >= 5){
parts.pop();
}
return parts.join('-');
}
// Add pod
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*(podHeight + podSpacing);
const g = document.createElementNS("http://www.w3.org/2000/svg","g");
g.dataset.podName = podName;
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", podHeight);
rect.setAttribute("class","pod");
g.appendChild(rect);
// Pod text with padding and multi-line
const text = document.createElementNS("http://www.w3.org/2000/svg","text");
text.setAttribute("class","pod-label");
text.setAttribute("text-anchor","middle");
text.setAttribute("x", x + nodeWidth/2);
text.setAttribute("y", y);
g.appendChild(text);
// Use your original truncated pod name here
const truncatedName = podName;
// Wrap in tspans for multi-line with top padding
const maxCharsPerLine = 16;
for(let i = 0; i < truncatedName.length; i += maxCharsPerLine){
const tspan = document.createElementNS("http://www.w3.org/2000/svg","tspan");
tspan.setAttribute("x", x + nodeWidth/2);
tspan.setAttribute("dy", i === 0 ? textPaddingY : 14);
tspan.textContent = truncatedName.substr(i, maxCharsPerLine);
text.appendChild(tspan);
}
svg.appendChild(g);
stack.push(g);
podsPerNode[nodeName] = stack;
updateSVGHeight();
}
// Update SVG height
function updateSVGHeight() {
let maxY = nodeY + nodeHeight;
for(const nodeName in podsPerNode){
const stack = podsPerNode[nodeName];
if(!stack || stack.length===0) continue;
const bottomY = nodeY + nodeHeight + stack.length*(podHeight + podSpacing);
if(bottomY > maxY) maxY = bottomY;
}
svg.setAttribute("height", maxY + 20);
}
// Animate pod moving
function flyPod(podName, fromNode, toNode){
if(!fromNode || !toNode) return;
const fromStack = podsPerNode[fromNode];
const idx = fromStack.findIndex(g => g.dataset.podName===podName);
if(idx===-1) return;
const g = fromStack[idx];
fromStack.splice(idx,1);
redrawPods(fromNode);
svg.appendChild(g);
const rect = g.querySelector("rect");
const text = g.querySelector("text");
const startX = parseFloat(rect.getAttribute("x"));
const startY = parseFloat(rect.getAttribute("y"));
const endX = nodePositions[toNode].x - nodeWidth/2;
const endY = nodeY + nodeHeight + podsPerNode[toNode].length*(podHeight+podSpacing);
let progress = 0;
const steps = 60;
const dx = (endX - startX)/steps;
const dy = (endY - startY)/steps;
const anim = setInterval(()=>{
progress++;
const nx = startX + dx*progress;
const ny = startY + dy*progress;
rect.setAttribute("x", nx);
rect.setAttribute("y", ny);
text.setAttribute("x", nx + nodeWidth/2);
if(progress>=steps){
clearInterval(anim);
podsPerNode[toNode].push(g);
redrawPods(toNode);
}
}, animationDuration/steps);
}
// Redraw pods on a node
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 + idx*(podHeight+podSpacing);
rect.setAttribute("x", x);
rect.setAttribute("y", y);
text.setAttribute("x", x + nodeWidth/2);
text.setAttribute("y", y);
});
updateSVGHeight();
}
// SSE stream
const evtSource = new EventSource("/api/stream");
evtSource.onmessage = e=>{
const evt = JSON.parse(e.data);
if(evt.fromNode===evt.toNode){
redrawPods(evt.toNode);
return;
}
if(!evt.fromNode){
addPod(evt.pod, evt.toNode);
redrawPods(evt.toNode);
} else {
flyPod(evt.pod, evt.fromNode, evt.toNode);
}
};
</script>
</body>
</html>
+288
View File
@@ -0,0 +1,288 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Node Balancer</title>
<style>
html, body {
margin: 0;
padding: 0;
min-height: 100%;
}
body { font-family: sans-serif; margin: 20px; }
svg { border:1px solid #ccc; width:100%; 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; fill:#fff; }
header {
display: flex;
align-items: center;
margin-bottom: 20px;
background-color: #f0f0f0;
padding: 10px 15px;
border-radius: 8px;
}
header .logo {
font-weight: bold;
font-size: 18px;
color: #2196F3;
margin-right: 15px;
padding: 5px 10px;
background-color: #333;
color: #fff;
border-radius: 4px;
font-family: monospace;
}
header .title {
font-size: 18px;
}
header .title span {
margin-left: 10px;
color: #555;
font-weight: normal;
}
header .cluster-label {
margin-left: 20px;
color: #333;
font-weight: normal;
}
</style>
</head>
<body>
<header>
<div class="logo">allarddcs</div>
<div class="title">
Node Balancer <span id="version">v?</span>
<span class="cluster-label">Cluster: <span id="cluster-name">?</span></span>
</div>
</header>
<svg id="canvas"></svg>
<script>
const svg = document.getElementById("canvas");
const nodePositions = {};
const nodeY = 50;
const nodeWidth = 120;
const nodeHeight = 55;
const podHeight = 36;
const podSpacing = 6;
const textPaddingY = 6;
let animationDuration = 2000;
const podsPerNode = {};
// Truncate pod names
function truncatePodName(podName){
const parts = podName.split('-');
const hashRegex = /^[a-z0-9]+$/i;
// Remove last segment if it looks like a hash
if(parts.length > 1 && parts[parts.length - 1].match(hashRegex) && parts[parts.length - 1].length >= 5){
parts.pop();
}
// Remove second last segment if it also looks like a hash
if(parts.length > 1 && parts[parts.length - 1].match(hashRegex) && parts[parts.length - 1].length >= 5){
parts.pop();
}
return parts.join('-');
}
// Fetch config
fetch("/api/config")
.then(res => res.json())
.then(cfg => {
animationDuration = cfg.animationDurationMs;
document.getElementById("version").textContent = "v" + cfg.version;
document.getElementById("cluster-name").textContent = cfg.clusterName;
});
// Draw nodes
fetch("/api/nodes")
.then(res => res.json())
.then(nodeNames => {
nodeNames.forEach((name,i)=>{
const x = 50 + i*220;
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.setAttribute("text-anchor","middle");
label.setAttribute("dominant-baseline","middle");
label.textContent = name;
svg.appendChild(label);
podsPerNode[name] = [];
});
fetch("/api/pods")
.then(res => res.json())
.then(podsByNode => {
for(const nodeName in podsByNode){
podsByNode[nodeName].forEach(podName => addPod(podName, nodeName));
}
});
});
// Add pod
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*(podHeight + podSpacing);
const g = document.createElementNS("http://www.w3.org/2000/svg","g");
g.dataset.podName = podName;
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", podHeight);
rect.setAttribute("class","pod");
g.appendChild(rect);
// Pod text with padding and multi-line
const text = document.createElementNS("http://www.w3.org/2000/svg","text");
text.setAttribute("class","pod-label");
text.setAttribute("text-anchor","middle");
text.setAttribute("x", x + nodeWidth/2);
text.setAttribute("y", y);
g.appendChild(text);
const truncatedName = truncatePodName(podName);
const maxCharsPerLine = 16;
for(let i = 0; i < truncatedName.length; i += maxCharsPerLine){
const tspan = document.createElementNS("http://www.w3.org/2000/svg","tspan");
tspan.setAttribute("x", x + nodeWidth/2);
// first line gets top padding equal to half the padding + half font height
tspan.setAttribute("dy", i === 0 ? textPaddingY + 10 : 14);
tspan.textContent = truncatedName.substr(i, maxCharsPerLine);
text.appendChild(tspan);
}
svg.appendChild(g);
stack.push(g);
podsPerNode[nodeName] = stack;
updateSVGHeight();
}
// Update SVG height
function updateSVGHeight() {
let maxY = nodeY + nodeHeight;
for(const nodeName in podsPerNode){
const stack = podsPerNode[nodeName];
if(!stack || stack.length===0) continue;
const bottomY = nodeY + nodeHeight + stack.length*(podHeight + podSpacing);
if(bottomY > maxY) maxY = bottomY;
}
svg.setAttribute("height", maxY + 20);
}
// Animate pod moving
function flyPod(podName, fromNode, toNode){
if(!fromNode || !toNode) return;
const fromStack = podsPerNode[fromNode];
const idx = fromStack.findIndex(g => g.dataset.podName===podName);
if(idx===-1) return;
const g = fromStack[idx];
fromStack.splice(idx,1);
redrawPods(fromNode);
svg.appendChild(g);
const rect = g.querySelector("rect");
const text = g.querySelector("text");
const startX = parseFloat(rect.getAttribute("x"));
const startY = parseFloat(rect.getAttribute("y"));
const endX = nodePositions[toNode].x - nodeWidth/2;
const endY = nodeY + nodeHeight + podsPerNode[toNode].length*(podHeight+podSpacing);
let progress = 0;
const steps = 60;
const dx = (endX - startX)/steps;
const dy = (endY - startY)/steps;
const anim = setInterval(()=>{
progress++;
const nx = startX + dx*progress;
const ny = startY + dy*progress;
rect.setAttribute("x", nx);
rect.setAttribute("y", ny);
text.setAttribute("x", nx + nodeWidth/2);
if(progress>=steps){
clearInterval(anim);
podsPerNode[toNode].push(g);
redrawPods(toNode);
}
}, animationDuration/steps);
}
// Redraw pods on a node
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 + idx*(podHeight+podSpacing);
rect.setAttribute("x", x);
rect.setAttribute("y", y);
text.setAttribute("x", x + nodeWidth/2);
text.setAttribute("y", y);
});
updateSVGHeight();
}
// SSE stream
const evtSource = new EventSource("/api/stream");
evtSource.onmessage = e=>{
const evt = JSON.parse(e.data);
if(evt.fromNode===evt.toNode){
redrawPods(evt.toNode);
return;
}
if(!evt.fromNode){
addPod(evt.pod, evt.toNode);
redrawPods(evt.toNode);
} else {
flyPod(evt.pod, evt.fromNode, evt.toNode);
}
};
</script>
</body>
</html>
+328
View File
@@ -0,0 +1,328 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Node Balancer</title>
<style>
html, body {
margin: 0;
padding: 0;
min-height: 100%;
}
body { font-family: sans-serif; margin: 20px; }
svg { border:1px solid #ccc; width:100%; 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; fill:#fff; }
header {
display: flex;
align-items: center;
margin-bottom: 20px;
background-color: #f0f0f0;
padding: 10px 15px;
border-radius: 8px;
}
header .logo {
font-weight: bold;
font-size: 18px;
color: #2196F3;
margin-right: 15px;
padding: 5px 10px;
background-color: #333;
color: #fff;
border-radius: 4px;
font-family: monospace;
}
header .title {
font-size: 18px;
}
header .title span {
margin-left: 10px;
color: #555;
font-weight: normal;
}
header .cluster-label {
margin-left: 20px;
color: #333;
font-weight: normal;
}
</style>
</head>
<body>
<header>
<div class="logo">allarddcs</div>
<div class="title">
Node Balancer <span id="version">v?</span>
<span class="cluster-label">Cluster: <span id="cluster-name">?</span></span>
</div>
</header>
<svg id="canvas"></svg>
<script>
const svg = document.getElementById("canvas");
const nodePositions = {};
const nodeY = 50;
const nodeWidth = 120;
const nodeHeight = 55;
const podHeight = 36;
const podSpacing = 6;
const textPaddingY = 6;
let animationDuration = 2000;
const podsPerNode = {};
// Truncate pod names (remove 1-2 trailing hashes)
function truncatePodName(podName){
const parts = podName.split('-');
const hashRegex = /^[a-z0-9]+$/i;
// Remove last segment if it looks like a hash
if(parts.length > 1 && parts[parts.length - 1].match(hashRegex) && parts[parts.length - 1].length >= 5){
parts.pop();
}
// Remove second last segment if it also looks like a hash
if(parts.length > 1 && parts[parts.length - 1].match(hashRegex) && parts[parts.length - 1].length >= 5){
parts.pop();
}
return parts.join('-');
}
// Fetch config
fetch("/api/config")
.then(res => res.json())
.then(cfg => {
animationDuration = cfg.animationDurationMs;
document.getElementById("version").textContent = "v" + cfg.version;
document.getElementById("cluster-name").textContent = cfg.clusterName;
});
// Add a node to DOM
function addNode(name, i){
const x = 50 + i*220;
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.setAttribute("text-anchor","middle");
label.setAttribute("dominant-baseline","middle");
label.textContent = name;
svg.appendChild(label);
podsPerNode[name] = [];
}
// Remove a node from DOM
function removeNode(name){
delete nodePositions[name];
// Remove pods
if(podsPerNode[name]){
podsPerNode[name].forEach(g => g.remove());
delete podsPerNode[name];
}
// Remove node rect and label
const rects = svg.querySelectorAll("rect.node");
rects.forEach(r => {
if(r.nextSibling && r.nextSibling.textContent === name){
r.remove();
}
});
const labels = svg.querySelectorAll("text.label");
labels.forEach(t => {
if(t.textContent === name) t.remove();
});
}
function refreshNodes(){
fetch("/api/nodes")
.then(res => res.json())
.then(nodeNames => {
// Remove existing node visuals
svg.querySelectorAll("rect.node").forEach(n => n.remove());
svg.querySelectorAll("text.label").forEach(n => n.remove());
// Reset node state
for(const k in nodePositions){
delete nodePositions[k];
}
// Rebuild nodes from cluster state
nodeNames.forEach((name, i) => {
addNode(name, i);
});
});
}
// Initial node fetch
refreshNodes();
// Refresh every 5s
setInterval(refreshNodes, 5000);
// Draw pods
fetch("/api/pods")
.then(res => res.json())
.then(podsByNode => {
for(const nodeName in podsByNode){
podsByNode[nodeName].forEach(podName => addPod(podName, nodeName));
}
});
// Add a pod
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*(podHeight + podSpacing);
const g = document.createElementNS("http://www.w3.org/2000/svg","g");
g.dataset.podName = podName;
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", podHeight);
rect.setAttribute("class","pod");
g.appendChild(rect);
// Pod text
const text = document.createElementNS("http://www.w3.org/2000/svg","text");
text.setAttribute("class","pod-label");
text.setAttribute("text-anchor","middle");
text.setAttribute("x", x + nodeWidth/2);
text.setAttribute("y", y);
g.appendChild(text);
const truncatedName = truncatePodName(podName);
const maxCharsPerLine = 16;
for(let i = 0; i < truncatedName.length; i += maxCharsPerLine){
const tspan = document.createElementNS("http://www.w3.org/2000/svg","tspan");
tspan.setAttribute("x", x + nodeWidth/2);
tspan.setAttribute("dy", i===0 ? textPaddingY + 10 : 14);
tspan.textContent = truncatedName.substr(i, maxCharsPerLine);
text.appendChild(tspan);
}
svg.appendChild(g);
stack.push(g);
podsPerNode[nodeName] = stack;
updateSVGHeight();
}
// Update SVG height
function updateSVGHeight(){
let maxY = nodeY + nodeHeight;
for(const nodeName in podsPerNode){
const stack = podsPerNode[nodeName];
if(!stack || stack.length === 0) continue;
const bottomY = nodeY + nodeHeight + stack.length*(podHeight + podSpacing);
if(bottomY > maxY) maxY = bottomY;
}
svg.setAttribute("height", maxY + 20);
}
// Animate pod move
function flyPod(podName, fromNode, toNode){
if(!fromNode || !toNode) return;
const fromStack = podsPerNode[fromNode];
const idx = fromStack.findIndex(g => g.dataset.podName === podName);
if(idx === -1) return;
const g = fromStack[idx];
fromStack.splice(idx,1);
redrawPods(fromNode);
svg.appendChild(g);
const rect = g.querySelector("rect");
const text = g.querySelector("text");
const startX = parseFloat(rect.getAttribute("x"));
const startY = parseFloat(rect.getAttribute("y"));
const endX = nodePositions[toNode].x - nodeWidth/2;
const endY = nodeY + nodeHeight + podsPerNode[toNode].length*(podHeight+podSpacing);
let progress = 0;
const steps = 60;
const dx = (endX - startX)/steps;
const dy = (endY - startY)/steps;
const anim = setInterval(()=>{
progress++;
const nx = startX + dx*progress;
const ny = startY + dy*progress;
rect.setAttribute("x", nx);
rect.setAttribute("y", ny);
text.setAttribute("x", nx + nodeWidth/2);
if(progress>=steps){
clearInterval(anim);
podsPerNode[toNode].push(g);
redrawPods(toNode);
}
}, animationDuration/steps);
}
// Redraw pods stack on node
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 + idx*(podHeight+podSpacing);
rect.setAttribute("x", x);
rect.setAttribute("y", y);
text.setAttribute("x", x + nodeWidth/2);
text.setAttribute("y", y);
});
updateSVGHeight();
}
// SSE stream
const evtSource = new EventSource("/api/stream");
evtSource.onmessage = e => {
const evt = JSON.parse(e.data);
if(evt.fromNode === evt.toNode){
redrawPods(evt.toNode);
return;
}
if(!evt.fromNode){
addPod(evt.pod, evt.toNode);
redrawPods(evt.toNode);
} else {
flyPod(evt.pod, evt.fromNode, evt.toNode);
}
};
</script>
</body>
</html>
+313
View File
@@ -0,0 +1,313 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Node Balancer</title>
<style>
html, body {
margin: 0;
padding: 0;
min-height: 100%;
}
body { font-family: sans-serif; margin: 20px; }
svg { border:1px solid #ccc; width:100%; 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; fill:#fff; }
header {
display: flex;
align-items: center;
margin-bottom: 20px;
background-color: #f0f0f0;
padding: 10px 15px;
border-radius: 8px;
}
header .logo {
font-weight: bold;
font-size: 18px;
color: #2196F3;
margin-right: 15px;
padding: 5px 10px;
background-color: #333;
color: #fff;
border-radius: 4px;
font-family: monospace;
}
header .title {
font-size: 18px;
}
header .title span {
margin-left: 10px;
color: #555;
font-weight: normal;
}
header .cluster-label {
margin-left: 20px;
color: #333;
font-weight: normal;
}
</style>
</head>
<body>
<header>
<div class="logo">allarddcs</div>
<div class="title">
Node Balancer <span id="version">v?</span>
<span class="cluster-label">Cluster: <span id="cluster-name">?</span></span>
</div>
</header>
<svg id="canvas"></svg>
<script>
const svg = document.getElementById("canvas");
// Node/pod layout constants
const nodePositions = {};
const nodeY = 50;
const nodeWidth = 120;
const nodeHeight = 55;
const podHeight = 36;
const podSpacing = 6;
const textPaddingY = 6;
let animationDuration = 2000;
const podsPerNode = {};
// Fetch config
fetch("/api/config")
.then(res => res.json())
.then(cfg => {
animationDuration = cfg.animationDurationMs;
document.getElementById("version").textContent = "v" + cfg.version;
document.getElementById("cluster-name").textContent = cfg.clusterName;
});
// Utility: truncate pod names
function truncatePodName(podName){
// remove trailing hash if present
const parts = podName.split("-");
if(parts.length > 2){
return parts.slice(0, parts.length-2).join("-");
}
return podName;
}
// Draw a node
function addNode(name, index){
const x = 50 + index*220;
nodePositions[name] = {x: x + nodeWidth/2, y: nodeY + nodeHeight/2};
// Node rectangle
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.setAttribute("text-anchor","middle");
label.setAttribute("dominant-baseline","middle");
label.textContent = name;
svg.appendChild(label);
podsPerNode[name] = [];
}
// Add a pod
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*(podHeight + podSpacing);
const g = document.createElementNS("http://www.w3.org/2000/svg","g");
g.dataset.podName = podName;
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", podHeight);
rect.setAttribute("class","pod");
g.appendChild(rect);
// Pod label
const text = document.createElementNS("http://www.w3.org/2000/svg","text");
text.setAttribute("class","pod-label");
text.setAttribute("text-anchor","middle");
text.setAttribute("x", x + nodeWidth/2);
const truncated = truncatePodName(podName);
if(truncated.length <= 12){
text.setAttribute("y", y + podHeight/2 + textPaddingY/2);
text.setAttribute("dominant-baseline","middle");
text.textContent = truncated;
} else {
// split into two lines
const line1 = truncated.substring(0,12);
const line2 = truncated.substring(12);
text.setAttribute("y", y + textPaddingY + 6);
const t1 = document.createElementNS("http://www.w3.org/2000/svg","tspan");
t1.setAttribute("x", x + nodeWidth/2);
t1.textContent = line1;
const t2 = document.createElementNS("http://www.w3.org/2000/svg","tspan");
t2.setAttribute("x", x + nodeWidth/2);
t2.setAttribute("dy", "12");
t2.textContent = line2;
text.appendChild(t1);
text.appendChild(t2);
}
g.appendChild(text);
svg.appendChild(g);
stack.push(g);
podsPerNode[nodeName] = stack;
updateSVGHeight();
}
// Update SVG height
function updateSVGHeight() {
let maxY = nodeY + nodeHeight;
for(const nodeName in podsPerNode){
const stack = podsPerNode[nodeName];
if(!stack || stack.length===0) continue;
const bottomY = nodeY + nodeHeight + stack.length*(podHeight + podSpacing);
if(bottomY > maxY) maxY = bottomY;
}
svg.setAttribute("height", maxY + 20);
}
// Animate pod move
function flyPod(podName, fromNode, toNode){
if(!fromNode || !toNode) return;
const fromStack = podsPerNode[fromNode];
const idx = fromStack.findIndex(g => g.dataset.podName===podName);
if(idx===-1) return;
const g = fromStack[idx];
fromStack.splice(idx,1);
redrawPods(fromNode);
svg.appendChild(g);
const rect = g.querySelector("rect");
const text = g.querySelector("text");
const startX = parseFloat(rect.getAttribute("x"));
const startY = parseFloat(rect.getAttribute("y"));
const endX = nodePositions[toNode].x - nodeWidth/2;
const endY = nodeY + nodeHeight + podsPerNode[toNode].length*(podHeight+podSpacing);
let progress = 0;
const steps = 60;
const dx = (endX - startX)/steps;
const dy = (endY - startY)/steps;
const anim = setInterval(()=>{
progress++;
const nx = startX + dx*progress;
const ny = startY + dy*progress;
rect.setAttribute("x", nx);
rect.setAttribute("y", ny);
text.setAttribute("x", nx + nodeWidth/2);
text.setAttribute("y", ny + podHeight/2 + textPaddingY/2);
if(progress>=steps){
clearInterval(anim);
podsPerNode[toNode].push(g);
redrawPods(toNode);
}
}, animationDuration/steps);
}
// Redraw pods on a node
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 + idx*(podHeight+podSpacing);
rect.setAttribute("x", x);
rect.setAttribute("y", y);
text.setAttribute("x", x + nodeWidth/2);
text.setAttribute("y", y + podHeight/2 + textPaddingY/2);
});
updateSVGHeight();
}
// Fetch authoritative cluster state and redraw
function refreshCluster(){
Promise.all([
fetch("/api/nodes").then(r=>r.json()),
fetch("/api/pods").then(r=>r.json())
]).then(([nodeNames, podsByNode])=>{
// clear SVG
while(svg.firstChild) svg.removeChild(svg.firstChild);
// reset state
for(const k in nodePositions) delete nodePositions[k];
for(const k in podsPerNode) delete podsPerNode[k];
// draw nodes
nodeNames.forEach((name,i)=>addNode(name,i));
// draw pods
for(const nodeName in podsByNode){
podsByNode[nodeName].forEach(podName=>{
addPod(podName,nodeName);
});
}
updateSVGHeight();
});
}
// SSE events
const evtSource = new EventSource("/api/stream");
evtSource.onmessage = e=>{
const evt = JSON.parse(e.data);
if(evt.fromNode && evt.toNode && evt.fromNode !== evt.toNode){
// animate first
flyPod(evt.pod, evt.fromNode, evt.toNode);
// then authoritative redraw after animation
setTimeout(refreshCluster, animationDuration);
} else {
// no animation needed
refreshCluster();
}
};
// Periodic refresh every 5s
setInterval(refreshCluster, 5000);
// Initial load
refreshCluster();
</script>
</body>
</html>
+342
View File
@@ -0,0 +1,342 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Node Balancer</title>
<style>
html, body {
margin: 0;
padding: 0;
min-height: 100%;
}
body { font-family: sans-serif; margin: 20px; }
svg { border:1px solid #ccc; width:100%; 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; fill:#fff; }
header {
display: flex;
align-items: center;
margin-bottom: 20px;
background-color: #f0f0f0;
padding: 10px 15px;
border-radius: 8px;
}
header .logo {
font-weight: bold;
font-size: 18px;
color: #2196F3;
margin-right: 15px;
padding: 5px 10px;
background-color: #333;
color: #fff;
border-radius: 4px;
font-family: monospace;
}
header .title {
font-size: 18px;
}
header .title span {
margin-left: 10px;
color: #555;
font-weight: normal;
}
header .cluster-label {
margin-left: 20px;
color: #333;
font-weight: normal;
}
</style>
</head>
<body>
<header>
<div class="logo">allarddcs</div>
<div class="title">
Node Balancer <span id="version">v?</span>
<span class="cluster-label">Cluster: <span id="cluster-name">?</span></span>
</div>
</header>
<svg id="canvas"></svg>
<script>
const svg = document.getElementById("canvas");
// Node/pod layout constants
const nodePositions = {};
const nodeY = 50;
const nodeWidth = 120;
const nodeHeight = 55;
const podHeight = 36;
const podSpacing = 6;
const textPaddingY = 6;
let animationDuration = 2000;
const podsPerNode = {};
// Fetch config
fetch("/api/config")
.then(res => res.json())
.then(cfg => {
animationDuration = cfg.animationDurationMs;
document.getElementById("version").textContent = "v" + cfg.version;
document.getElementById("cluster-name").textContent = cfg.clusterName;
});
// Remove trailing k8s-style hash if present
function truncatePodName(podName){
const parts = podName.split("-");
// Helper: is this segment a k8s-style hash?
// Alphanumeric, length >= 5, not a short numeric like "0", "1", "2"…
function isHash(seg){
return /^[a-z0-9]{5,}$/.test(seg);
}
// Remove trailing hash segments until we hit a non-hash or only one segment left
while(parts.length > 1 && isHash(parts[parts.length-1])){
parts.pop();
}
return parts.join("-");
}
// Split pod name over multiple lines without breaking words
function splitPodNameLines(podName, maxCharsPerLine=12){
const words = podName.split("-");
const lines = [];
let currentLine = "";
words.forEach(word => {
if(currentLine.length === 0){
currentLine = word;
} else if((currentLine.length + 1 + word.length) <= maxCharsPerLine){
currentLine += "-" + word;
} else {
lines.push(currentLine);
currentLine = word;
}
});
if(currentLine) lines.push(currentLine);
return lines;
}
// Draw a node
function addNode(name, index){
const x = 50 + index*220;
nodePositions[name] = {x: x + nodeWidth/2, y: nodeY + nodeHeight/2};
// Node rectangle
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.setAttribute("text-anchor","middle");
label.setAttribute("dominant-baseline","middle");
label.textContent = name;
svg.appendChild(label);
podsPerNode[name] = [];
}
// Add a pod
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*(podHeight + podSpacing);
const g = document.createElementNS("http://www.w3.org/2000/svg","g");
g.dataset.podName = podName;
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", podHeight);
rect.setAttribute("class","pod");
g.appendChild(rect);
// Pod label
const text = document.createElementNS("http://www.w3.org/2000/svg","text");
text.setAttribute("class","pod-label");
text.setAttribute("text-anchor","middle");
text.setAttribute("x", x + nodeWidth/2);
const truncated = truncatePodName(podName);
if(truncated.length <= 12){
text.setAttribute("y", y + podHeight/2 + textPaddingY/2);
text.setAttribute("dominant-baseline","middle");
text.textContent = truncated;
} else {
// split into two lines
const line1 = truncated.substring(0,12);
const line2 = truncated.substring(12);
text.setAttribute("y", y + textPaddingY + 6);
const t1 = document.createElementNS("http://www.w3.org/2000/svg","tspan");
t1.setAttribute("x", x + nodeWidth/2);
t1.textContent = line1;
const t2 = document.createElementNS("http://www.w3.org/2000/svg","tspan");
t2.setAttribute("x", x + nodeWidth/2);
t2.setAttribute("dy", "12");
t2.textContent = line2;
text.appendChild(t1);
text.appendChild(t2);
}
g.appendChild(text);
svg.appendChild(g);
stack.push(g);
podsPerNode[nodeName] = stack;
updateSVGHeight();
}
// Update SVG height
function updateSVGHeight() {
let maxY = nodeY + nodeHeight;
for(const nodeName in podsPerNode){
const stack = podsPerNode[nodeName];
if(!stack || stack.length===0) continue;
const bottomY = nodeY + nodeHeight + stack.length*(podHeight + podSpacing);
if(bottomY > maxY) maxY = bottomY;
}
svg.setAttribute("height", maxY + 20);
}
// Animate pod move
function flyPod(podName, fromNode, toNode){
if(!fromNode || !toNode) return;
const fromStack = podsPerNode[fromNode];
const idx = fromStack.findIndex(g => g.dataset.podName===podName);
if(idx===-1) return;
const g = fromStack[idx];
fromStack.splice(idx,1);
redrawPods(fromNode);
svg.appendChild(g);
const rect = g.querySelector("rect");
const text = g.querySelector("text");
const startX = parseFloat(rect.getAttribute("x"));
const startY = parseFloat(rect.getAttribute("y"));
const endX = nodePositions[toNode].x - nodeWidth/2;
const endY = nodeY + nodeHeight + podsPerNode[toNode].length*(podHeight+podSpacing);
let progress = 0;
const steps = 60;
const dx = (endX - startX)/steps;
const dy = (endY - startY)/steps;
const anim = setInterval(()=>{
progress++;
const nx = startX + dx*progress;
const ny = startY + dy*progress;
rect.setAttribute("x", nx);
rect.setAttribute("y", ny);
text.setAttribute("x", nx + nodeWidth/2);
text.setAttribute("y", ny + podHeight/2 + textPaddingY/2);
if(progress>=steps){
clearInterval(anim);
podsPerNode[toNode].push(g);
redrawPods(toNode);
}
}, animationDuration/steps);
}
// Redraw pods on a node
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 + idx*(podHeight+podSpacing);
rect.setAttribute("x", x);
rect.setAttribute("y", y);
text.setAttribute("x", x + nodeWidth/2);
text.setAttribute("y", y + podHeight/2 + textPaddingY/2);
});
updateSVGHeight();
}
// Fetch authoritative cluster state and redraw
function refreshCluster(){
Promise.all([
fetch("/api/nodes").then(r=>r.json()),
fetch("/api/pods").then(r=>r.json())
]).then(([nodeNames, podsByNode])=>{
// clear SVG
while(svg.firstChild) svg.removeChild(svg.firstChild);
// reset state
for(const k in nodePositions) delete nodePositions[k];
for(const k in podsPerNode) delete podsPerNode[k];
// draw nodes
nodeNames.forEach((name,i)=>addNode(name,i));
// draw pods
for(const nodeName in podsByNode){
podsByNode[nodeName].forEach(podName=>{
addPod(podName,nodeName);
});
}
updateSVGHeight();
});
}
// SSE events
const evtSource = new EventSource("/api/stream");
evtSource.onmessage = e=>{
const evt = JSON.parse(e.data);
if(evt.fromNode && evt.toNode && evt.fromNode !== evt.toNode){
// animate first
flyPod(evt.pod, evt.fromNode, evt.toNode);
// then authoritative redraw after animation
setTimeout(refreshCluster, animationDuration);
} else {
// no animation needed
refreshCluster();
}
};
// Periodic refresh every 5s
setInterval(refreshCluster, 5000);
// Initial load
refreshCluster();
</script>
</body>
</html>
+350
View File
@@ -0,0 +1,350 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Node Balancer</title>
<style>
html, body {
margin: 0;
padding: 0;
min-height: 100%;
}
body { font-family: sans-serif; margin: 20px; }
svg { border:1px solid #ccc; width:100%; 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; fill:#fff; }
header {
display: flex;
align-items: center;
margin-bottom: 20px;
background-color: #f0f0f0;
padding: 10px 15px;
border-radius: 8px;
}
header .logo {
font-weight: bold;
font-size: 18px;
color: #2196F3;
margin-right: 15px;
padding: 5px 10px;
background-color: #333;
color: #fff;
border-radius: 4px;
font-family: monospace;
}
header .title {
font-size: 18px;
}
header .title span {
margin-left: 10px;
color: #555;
font-weight: normal;
}
header .cluster-label {
margin-left: 20px;
color: #333;
font-weight: normal;
}
</style>
</head>
<body>
<header>
<div class="logo">allarddcs</div>
<div class="title">
Node Balancer <span id="version">v?</span>
<span class="cluster-label">Cluster: <span id="cluster-name">?</span></span>
</div>
</header>
<svg id="canvas"></svg>
<script>
const svg = document.getElementById("canvas");
// Node/pod layout constants
const nodePositions = {};
const nodeY = 50;
const nodeWidth = 120;
const nodeHeight = 55;
const podHeight = 36;
const podSpacing = 6;
const textPaddingY = 6;
let animationDuration = 2000;
const podsPerNode = {};
// Fetch config
fetch("/api/config")
.then(res => res.json())
.then(cfg => {
animationDuration = cfg.animationDurationMs;
document.getElementById("version").textContent = "v" + cfg.version;
document.getElementById("cluster-name").textContent = cfg.clusterName;
});
// Remove trailing k8s-style hash if present
function truncatePodName(podName) {
const parts = podName.split("-");
function isPodId(seg) {
// Pod suffix like "9tl49"
return /^[a-z0-9]{5}$/.test(seg);
}
function isReplicaSetHash(seg) {
// ReplicaSet hash typically 810 hex chars
return /^[a-f0-9]{8,10}$/.test(seg);
}
if (parts.length > 1 && isPodId(parts[parts.length - 1])) {
parts.pop();
}
if (parts.length > 1 && isReplicaSetHash(parts[parts.length - 1])) {
parts.pop();
}
return parts.join("-");
}
// Split pod name over multiple lines without breaking words
function splitPodNameLines(podName, maxCharsPerLine=12){
const words = podName.split("-");
const lines = [];
let currentLine = "";
words.forEach(word => {
if(currentLine.length === 0){
currentLine = word;
} else if((currentLine.length + 1 + word.length) <= maxCharsPerLine){
currentLine += "-" + word;
} else {
lines.push(currentLine);
currentLine = word;
}
});
if(currentLine) lines.push(currentLine);
return lines;
}
// Draw a node
function addNode(name, index){
const x = 50 + index*220;
nodePositions[name] = {x: x + nodeWidth/2, y: nodeY + nodeHeight/2};
// Node rectangle
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.setAttribute("text-anchor","middle");
label.setAttribute("dominant-baseline","middle");
label.textContent = name;
svg.appendChild(label);
podsPerNode[name] = [];
}
// Add a pod
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*(podHeight + podSpacing);
const g = document.createElementNS("http://www.w3.org/2000/svg","g");
g.dataset.podName = podName;
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", podHeight);
rect.setAttribute("class","pod");
g.appendChild(rect);
// Pod label
const text = document.createElementNS("http://www.w3.org/2000/svg","text");
text.setAttribute("class","pod-label");
text.setAttribute("text-anchor","middle");
text.setAttribute("x", x + nodeWidth/2);
const truncated = truncatePodName(podName);
if(truncated.length <= 12){
text.setAttribute("y", y + podHeight/2 + textPaddingY/2);
text.setAttribute("dominant-baseline","middle");
text.textContent = truncated;
} else {
// split into two lines
const line1 = truncated.substring(0,12);
const line2 = truncated.substring(12);
text.setAttribute("y", y + textPaddingY + 6);
const t1 = document.createElementNS("http://www.w3.org/2000/svg","tspan");
t1.setAttribute("x", x + nodeWidth/2);
t1.textContent = line1;
const t2 = document.createElementNS("http://www.w3.org/2000/svg","tspan");
t2.setAttribute("x", x + nodeWidth/2);
t2.setAttribute("dy", "12");
t2.textContent = line2;
text.appendChild(t1);
text.appendChild(t2);
}
g.appendChild(text);
svg.appendChild(g);
stack.push(g);
podsPerNode[nodeName] = stack;
updateSVGHeight();
}
// Update SVG height
function updateSVGHeight() {
let maxY = nodeY + nodeHeight;
for(const nodeName in podsPerNode){
const stack = podsPerNode[nodeName];
if(!stack || stack.length===0) continue;
const bottomY = nodeY + nodeHeight + stack.length*(podHeight + podSpacing);
if(bottomY > maxY) maxY = bottomY;
}
svg.setAttribute("height", maxY + 20);
}
// Animate pod move
function flyPod(podName, fromNode, toNode){
if(!fromNode || !toNode) return;
const fromStack = podsPerNode[fromNode];
const idx = fromStack.findIndex(g => g.dataset.podName===podName);
if(idx===-1) return;
const g = fromStack[idx];
fromStack.splice(idx,1);
redrawPods(fromNode);
svg.appendChild(g);
const rect = g.querySelector("rect");
const text = g.querySelector("text");
const startX = parseFloat(rect.getAttribute("x"));
const startY = parseFloat(rect.getAttribute("y"));
const endX = nodePositions[toNode].x - nodeWidth/2;
const endY = nodeY + nodeHeight + podsPerNode[toNode].length*(podHeight+podSpacing);
let progress = 0;
const steps = 60;
const dx = (endX - startX)/steps;
const dy = (endY - startY)/steps;
const anim = setInterval(()=>{
progress++;
const nx = startX + dx*progress;
const ny = startY + dy*progress;
rect.setAttribute("x", nx);
rect.setAttribute("y", ny);
text.setAttribute("x", nx + nodeWidth/2);
text.setAttribute("y", ny + podHeight/2 + textPaddingY/2);
if(progress>=steps){
clearInterval(anim);
podsPerNode[toNode].push(g);
redrawPods(toNode);
}
}, animationDuration/steps);
}
// Redraw pods on a node
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 + idx*(podHeight+podSpacing);
rect.setAttribute("x", x);
rect.setAttribute("y", y);
text.setAttribute("x", x + nodeWidth/2);
text.setAttribute("y", y + podHeight/2 + textPaddingY/2);
});
updateSVGHeight();
}
// Fetch authoritative cluster state and redraw
function refreshCluster(){
Promise.all([
fetch("/api/nodes").then(r=>r.json()),
fetch("/api/pods").then(r=>r.json())
]).then(([nodeNames, podsByNode])=>{
// clear SVG
while(svg.firstChild) svg.removeChild(svg.firstChild);
// reset state
for(const k in nodePositions) delete nodePositions[k];
for(const k in podsPerNode) delete podsPerNode[k];
// draw nodes
nodeNames.forEach((name,i)=>addNode(name,i));
// draw pods
for(const nodeName in podsByNode){
podsByNode[nodeName].forEach(podName=>{
addPod(podName,nodeName);
});
}
updateSVGHeight();
});
}
// SSE events
const evtSource = new EventSource("/api/stream");
evtSource.onmessage = e=>{
const evt = JSON.parse(e.data);
if(evt.fromNode && evt.toNode && evt.fromNode !== evt.toNode){
// animate first
flyPod(evt.pod, evt.fromNode, evt.toNode);
// then authoritative redraw after animation
setTimeout(refreshCluster, animationDuration);
} else {
// no animation needed
refreshCluster();
}
};
// Periodic refresh every 5s
setInterval(refreshCluster, 5000);
// Initial load
refreshCluster();
</script>
</body>
</html>
+152
View File
@@ -0,0 +1,152 @@
<!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; }
.pod-label { font-size:10px; 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;
// fetch animation duration
fetch("/api/config")
.then(res => res.json())
.then(cfg => { animationDuration = cfg.animationDurationMs; });
// fetch 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);
});
});
fetch("/api/pods")
.then(res => res.json())
.then(podsByNode => {
for(const nodeName in podsByNode){
podsByNode[nodeName].forEach(podName => addPod(podName, nodeName));
}
});
// pod tracking per node
const podsPerNode = {};
// function to show pod vertically under node
function addPod(podName, nodeName) {
if(!nodePositions[nodeName]) return;
if(!podsPerNode[nodeName]) podsPerNode[nodeName] = [];
const podIndex = podsPerNode[nodeName].length;
const x = nodePositions[nodeName].x;
const y = nodeY + nodeHeight + 20 + podIndex*25;
const g = document.createElementNS("http://www.w3.org/2000/svg","g");
const circle = document.createElementNS("http://www.w3.org/2000/svg","circle");
circle.setAttribute("cx", x);
circle.setAttribute("cy", y);
circle.setAttribute("r", 10);
circle.setAttribute("class","pod");
g.appendChild(circle);
const text = document.createElementNS("http://www.w3.org/2000/svg","text");
text.setAttribute("x", x);
text.setAttribute("y", y);
text.setAttribute("class","pod-label");
text.textContent = podName;
g.appendChild(text);
svg.appendChild(g);
podsPerNode[nodeName].push(g);
}
// animate pod move
function movePod(podName, fromNode, toNode) {
const fromPods = podsPerNode[fromNode];
if(!fromPods) return;
// find pod element
let podG = null;
for(let i=0;i<fromPods.length;i++){
const g = fromPods[i];
const text = g.querySelector("text");
if(text && text.textContent === podName){
podG = g;
fromPods.splice(i,1); // remove from old node
break;
}
}
if(!podG) return;
// add to new node
if(!podsPerNode[toNode]) podsPerNode[toNode]=[];
podsPerNode[toNode].push(podG);
const steps = 60;
let progress=0;
const start = podG.querySelector("circle").getAttribute("cy")*1;
const end = nodeY + nodeHeight + 20 + (podsPerNode[toNode].length-1)*25;
const dx = nodePositions[toNode].x - nodePositions[fromNode].x;
const dy = end - start;
const anim = setInterval(()=>{
progress++;
const cx = nodePositions[fromNode].x + dx*progress/steps;
const cy = start + dy*progress/steps;
podG.querySelector("circle").setAttribute("cx", cx);
podG.querySelector("circle").setAttribute("cy", cy);
podG.querySelector("text").setAttribute("x", cx);
podG.querySelector("text").setAttribute("y", cy);
if(progress>=steps){
clearInterval(anim);
}
}, animationDuration/steps);
}
// SSE stream
const evtSource = new EventSource("/api/stream");
evtSource.onmessage = e => {
const evt = JSON.parse(e.data);
if(!evt.fromNode){
addPod(evt.pod, evt.toNode);
} else {
movePod(evt.pod, evt.fromNode, evt.toNode);
}
};
</script>
</body>
</html>
+329
View File
@@ -0,0 +1,329 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Node Balancer</title>
<style>
html, body {
margin: 0;
padding: 0;
min-height: 100%;
}
body { font-family: sans-serif; margin: 20px; }
svg { border:1px solid #ccc; width:100%; 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; fill:#fff; }
header {
display: flex;
align-items: center;
margin-bottom: 20px;
background-color: #f0f0f0;
padding: 10px 15px;
border-radius: 8px;
}
header .logo {
font-weight: bold;
font-size: 18px;
color: #2196F3;
margin-right: 15px;
padding: 5px 10px;
background-color: #333;
color: #fff;
border-radius: 4px;
font-family: monospace;
}
header .title {
font-size: 18px;
}
header .title span {
margin-left: 10px;
color: #555;
font-weight: normal;
}
header .cluster-label {
margin-left: 20px;
color: #333;
font-weight: normal;
}
</style>
</head>
<body>
<header>
<div class="logo">allarddcs</div>
<div class="title">
Node Balancer <span id="version">v?</span>
<span class="cluster-label">Cluster: <span id="cluster-name">?</span></span>
</div>
</header>
<svg id="canvas"></svg>
<script>
const svg = document.getElementById("canvas");
// Node/pod layout constants
const nodePositions = {};
const nodeY = 50;
let nodeWidth = 120;
const nodeHeight = 55;
const podHeight = 36;
const podSpacing = 6;
const textPaddingY = 6;
let animationDuration = 2000;
const podsPerNode = {};
// Fetch config
fetch("/api/config")
.then(res => res.json())
.then(cfg => {
animationDuration = cfg.animationDurationMs;
document.getElementById("version").textContent = "v" + cfg.version;
document.getElementById("cluster-name").textContent = cfg.clusterName;
});
// Remove trailing k8s-style hash if present
function truncatePodName(podName) {
const parts = podName.split("-");
function isPodId(seg) { return /^[a-z0-9]{5}$/.test(seg); }
function isReplicaSetHash(seg) { return /^[a-f0-9]{8,10}$/.test(seg); }
if (parts.length > 1 && isPodId(parts[parts.length - 1])) parts.pop();
if (parts.length > 1 && isReplicaSetHash(parts[parts.length - 1])) parts.pop();
return parts.join("-");
}
// Split pod name into multiple lines
function splitPodNameLines(podName, maxCharsPerLine=12){
const words = podName.split("-");
const lines = [];
let currentLine = "";
words.forEach(word => {
if(currentLine.length === 0) currentLine = word;
else if((currentLine.length + 1 + word.length) <= maxCharsPerLine) currentLine += "-" + word;
else { lines.push(currentLine); currentLine = word; }
});
if(currentLine) lines.push(currentLine);
return lines;
}
// Draw a node (responsive)
function addNode(name, index, totalNodes){
const svgWidth = svg.clientWidth || window.innerWidth;
const outerPadding = 60; // distance from canvas edge
const innerMargin = 20; // breathing space inside edges
const usableWidth = svgWidth - 2 * outerPadding;
const spacing = totalNodes > 1
? usableWidth / totalNodes
: 0;
// Optional responsive node width scaling
const maxNodeWidth = 120;
const minNodeWidth = 60;
nodeWidth = Math.max(minNodeWidth, Math.min(maxNodeWidth, spacing * 0.8));
// Center nodes within their spacing slot
const centerX = outerPadding + spacing * index + spacing / 2;
const x = centerX - nodeWidth / 2;
nodePositions[name] = {
x: centerX,
y: nodeY + nodeHeight/2
};
// Node rectangle
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", centerX);
label.setAttribute("y", nodeY + nodeHeight/2);
label.setAttribute("class","label");
label.setAttribute("text-anchor","middle");
label.setAttribute("dominant-baseline","middle");
label.textContent = name;
svg.appendChild(label);
podsPerNode[name] = [];
}
// Add a pod
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*(podHeight + podSpacing);
const g = document.createElementNS("http://www.w3.org/2000/svg","g");
g.dataset.podName = podName;
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", podHeight);
rect.setAttribute("class","pod");
g.appendChild(rect);
const text = document.createElementNS("http://www.w3.org/2000/svg","text");
text.setAttribute("class","pod-label");
text.setAttribute("text-anchor","middle");
text.setAttribute("x", x + nodeWidth/2);
const truncated = truncatePodName(podName);
if(truncated.length <= 12){
text.setAttribute("y", y + podHeight/2 + textPaddingY/2);
text.setAttribute("dominant-baseline","middle");
text.textContent = truncated;
} else {
const line1 = truncated.substring(0,12);
const line2 = truncated.substring(12);
text.setAttribute("y", y + textPaddingY + 6);
const t1 = document.createElementNS("http://www.w3.org/2000/svg","tspan");
t1.setAttribute("x", x + nodeWidth/2); t1.textContent = line1;
const t2 = document.createElementNS("http://www.w3.org/2000/svg","tspan");
t2.setAttribute("x", x + nodeWidth/2); t2.setAttribute("dy","12"); t2.textContent = line2;
text.appendChild(t1);
text.appendChild(t2);
}
g.appendChild(text);
svg.appendChild(g);
stack.push(g);
podsPerNode[nodeName] = stack;
updateSVGHeight();
}
// Update SVG height
function updateSVGHeight() {
let maxY = nodeY + nodeHeight;
for(const nodeName in podsPerNode){
const stack = podsPerNode[nodeName];
if(!stack || stack.length===0) continue;
const bottomY = nodeY + nodeHeight + stack.length*(podHeight + podSpacing);
if(bottomY > maxY) maxY = bottomY;
}
svg.setAttribute("height", maxY + 20);
}
// Animate pod move
function flyPod(podName, fromNode, toNode){
if(!fromNode || !toNode) return;
const fromStack = podsPerNode[fromNode];
const idx = fromStack.findIndex(g => g.dataset.podName===podName);
if(idx===-1) return;
const g = fromStack[idx];
fromStack.splice(idx,1);
redrawPods(fromNode);
svg.appendChild(g);
const rect = g.querySelector("rect");
const text = g.querySelector("text");
const startX = parseFloat(rect.getAttribute("x"));
const startY = parseFloat(rect.getAttribute("y"));
const endX = nodePositions[toNode].x - nodeWidth/2;
const endY = nodeY + nodeHeight + podsPerNode[toNode].length*(podHeight+podSpacing);
let progress = 0;
const steps = 60;
const dx = (endX - startX)/steps;
const dy = (endY - startY)/steps;
const anim = setInterval(()=>{
progress++;
const nx = startX + dx*progress;
const ny = startY + dy*progress;
rect.setAttribute("x", nx);
rect.setAttribute("y", ny);
text.setAttribute("x", nx + nodeWidth/2);
text.setAttribute("y", ny + podHeight/2 + textPaddingY/2);
if(progress>=steps){
clearInterval(anim);
podsPerNode[toNode].push(g);
redrawPods(toNode);
}
}, animationDuration/steps);
}
// Redraw pods on a node
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 + idx*(podHeight+podSpacing);
rect.setAttribute("x", x);
rect.setAttribute("y", y);
text.setAttribute("x", x + nodeWidth/2);
text.setAttribute("y", y + podHeight/2 + textPaddingY/2);
});
updateSVGHeight();
}
// Fetch authoritative cluster state and redraw
function refreshCluster(){
Promise.all([
fetch("/api/nodes").then(r=>r.json()),
fetch("/api/pods").then(r=>r.json())
]).then(([nodeNames, podsByNode])=>{
while(svg.firstChild) svg.removeChild(svg.firstChild);
for(const k in nodePositions) delete nodePositions[k];
for(const k in podsPerNode) delete podsPerNode[k];
nodeNames.forEach((name,i)=>addNode(name,i,nodeNames.length));
for(const nodeName in podsByNode){
podsByNode[nodeName].forEach(podName=>addPod(podName,nodeName));
}
updateSVGHeight();
});
}
// SSE events
const evtSource = new EventSource("/api/stream");
evtSource.onmessage = e=>{
const evt = JSON.parse(e.data);
if(evt.fromNode && evt.toNode && evt.fromNode !== evt.toNode){
flyPod(evt.pod, evt.fromNode, evt.toNode);
setTimeout(refreshCluster, animationDuration);
} else {
refreshCluster();
}
};
// Periodic refresh every 5s
setInterval(refreshCluster, 5000);
// Responsive redraw on resize
window.addEventListener("resize", refreshCluster);
// Initial load
refreshCluster();
</script>
</body>
</html>
+196
View File
@@ -0,0 +1,196 @@
<!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;
}
// Redraw the stack of pods for a node
function redrawPods(nodeName){
const stack = podsPerNode[nodeName] || [];
stack.forEach((g,i)=>{
const rect = g.querySelector("rect");
const text = g.querySelector("text");
const x = nodePositions[nodeName].x - nodeWidth/2;
const y = nodeY + nodeHeight + i*25;
rect.setAttribute("x", x);
rect.setAttribute("y", y);
text.setAttribute("x", x + nodeWidth/2);
text.setAttribute("y", y + 10);
});
}
// Animate pod moving from one node to another
function flyPod(podName, fromNode, toNode){
const g = document.createElementNS("http://www.w3.org/2000/svg","g");
// Pod rectangle
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);
// Pod text
const text = document.createElementNS("http://www.w3.org/2000/svg","text");
text.textContent = podName;
text.setAttribute("class","pod-label");
g.appendChild(text);
svg.appendChild(g);
// Remove from fromNode stack
if(fromNode && podsPerNode[fromNode]){
podsPerNode[fromNode].forEach(pg => {
const t = pg.querySelector("text");
if(t.textContent === podName){
svg.removeChild(pg); // remove visually
}
});
podsPerNode[fromNode] = podsPerNode[fromNode].filter(pg=>{
const t = pg.querySelector("text");
return t.textContent !== podName;
});
redrawPods(fromNode); // redraw remaining pods
}
// Animate movement
let start = fromNode ? nodePositions[fromNode] : nodePositions[toNode]; // start at old node or directly at target
let end = nodePositions[toNode];
if(!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);
// remove animated g
svg.removeChild(g);
// Add pod to new node stack
addPod(podName, toNode);
}
}, animationDuration/steps);
}
// SSE stream
const evtSource = new EventSource("/api/stream");
evtSource.onmessage = e => {
const evt = JSON.parse(e.data);
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>
+201
View File
@@ -0,0 +1,201 @@
<!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;
}
// Redraw the stack of pods for a node
function redrawPods(nodeName){
const stack = podsPerNode[nodeName] || [];
stack.forEach((g,i)=>{
const rect = g.querySelector("rect");
const text = g.querySelector("text");
const x = nodePositions[nodeName].x - nodeWidth/2;
const y = nodeY + nodeHeight + i*25;
rect.setAttribute("x", x);
rect.setAttribute("y", y);
text.setAttribute("x", x + nodeWidth/2);
text.setAttribute("y", y + 10);
});
}
// Animate pod moving from one node to another
function flyPod(podName, fromNode, toNode){
const g = document.createElementNS("http://www.w3.org/2000/svg","g");
// Pod rectangle
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);
// Pod text
const text = document.createElementNS("http://www.w3.org/2000/svg","text");
text.textContent = podName;
text.setAttribute("class","pod-label");
g.appendChild(text);
svg.appendChild(g);
// Remove from fromNode stack
if(fromNode && podsPerNode[fromNode]){
podsPerNode[fromNode].forEach(pg => {
const t = pg.querySelector("text");
if(t.textContent === podName){
svg.removeChild(pg); // remove visually
}
});
podsPerNode[fromNode] = podsPerNode[fromNode].filter(pg=>{
const t = pg.querySelector("text");
return t.textContent !== podName;
});
redrawPods(fromNode); // redraw remaining pods
}
// Animate movement
let start = fromNode ? nodePositions[fromNode] : nodePositions[toNode]; // start at old node or directly at target
let end = nodePositions[toNode];
if(!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);
// remove animated g
svg.removeChild(g);
// Add pod to new node stack
addPod(podName, toNode);
}
}, animationDuration/steps);
}
// 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>
+193
View File
@@ -0,0 +1,193 @@
<!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>
+269
View File
@@ -0,0 +1,269 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Node Balancer</title>
<style>
html, body {
margin: 0;
padding: 0;
min-height: 100%;
}
body { font-family: sans-serif; margin: 20px; }
svg { border:1px solid #ccc; width:100%; 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; }
header {
display: flex;
align-items: center;
margin-bottom: 20px;
background-color: #f0f0f0; /* light gray background for the header */
padding: 10px 15px;
border-radius: 8px;
}
header .logo {
font-weight: bold;
font-size: 18px;
color: #2196F3; /* blue logo text */
margin-right: 15px;
padding: 5px 10px;
background-color: #333; /* dark background behind logo text */
color: #fff;
border-radius: 4px;
font-family: monospace;
}
header .title {
font-size: 18px;
}
header .title span {
margin-left: 10px;
color: #555;
font-weight: normal;
}
header .cluster-label {
margin-left: 20px;
color: #333;
font-weight: normal;
}
</style>
</head>
<body>
<header>
<div class="logo">allarddcs</div>
<div class="title">
Node Balancer <span id="version">v?</span>
<span class="cluster-label">Cluster: <span id="cluster-name">?</span></span>
</div>
</header>
<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;
document.getElementById("version").textContent = "v" + cfg.version;
document.getElementById("cluster-name").textContent = cfg.clusterName;
});
// 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;
updateSVGHeight();
}
function updateSVGHeight() {
let maxY = nodeY + nodeHeight; // start with top node
for (const nodeName in podsPerNode) {
const stack = podsPerNode[nodeName];
if (!stack) continue;
if (stack.length === 0) continue;
const stackHeight = stack.length * (20 + 5) +40 ; // pod height + spacing
const bottomY = nodeY + nodeHeight + stackHeight;
if (bottomY > maxY) maxY = bottomY;
}
// optional extra padding at bottom
svg.setAttribute("height", maxY + 20);
}
// 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);
});
updateSVGHeight();
}
// 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>
+268
View File
@@ -0,0 +1,268 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Node Balancer</title>
<style>
html, body {
margin: 0;
padding: 0;
min-height: 100%;
}
body { font-family: sans-serif; margin: 20px; }
svg { border:1px solid #ccc; width:100%; 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; }
header {
display: flex;
align-items: center;
margin-bottom: 20px;
background-color: #f0f0f0; /* light gray background for the header */
padding: 10px 15px;
border-radius: 8px;
}
header .logo {
font-weight: bold;
font-size: 18px;
color: #2196F3; /* blue logo text */
margin-right: 15px;
padding: 5px 10px;
background-color: #333; /* dark background behind logo text */
color: #fff;
border-radius: 4px;
font-family: monospace;
}
header .title {
font-size: 18px;
}
header .title span {
margin-left: 10px;
color: #555;
font-weight: normal;
}
header .cluster-label {
margin-left: 20px;
color: #333;
font-weight: normal;
}
</style>
</head>
<body>
<header>
<div class="logo">allarddcs</div>
<div class="title">
Node Balancer <span id="version">v?</span>
<span class="cluster-label">Cluster: <span id="cluster-name">?</span></span>
</div>
</header>
<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;
document.getElementById("version").textContent = "v" + cfg.version;
document.getElementById("cluster-name").textContent = cfg.clusterName;
});
// 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;
updateSVGHeight();
}
function updateSVGHeight() {
let maxY = nodeY + nodeHeight; // start with top node
for (const nodeName in podsPerNode) {
const stack = podsPerNode[nodeName];
if (!stack) continue;
if (stack.length === 0) continue;
const stackHeight = stack.length * (20 + 5) +40 ; // pod height + spacing
const bottomY = nodeY + nodeHeight + stackHeight;
if (bottomY > maxY) maxY = bottomY;
}
// optional extra padding at bottom
svg.setAttribute("height", maxY + 20);
}
// Animate pod moving from one node to another
function flyPod(podName, fromNode, toNode) {
if (!fromNode || !toNode) return;
const fromStack = podsPerNode[fromNode];
const idx = fromStack.findIndex(g => g.querySelector("text").textContent === podName);
if (idx === -1) return;
// Grab the actual pod <g> element
const g = fromStack[idx];
// Remove it from leaving stack array
fromStack.splice(idx, 1);
redrawPods(fromNode); // redraw remaining pods to close gap
// Reparent pod to SVG root so it can move freely
svg.appendChild(g);
const rect = g.querySelector("rect");
const text = g.querySelector("text");
// Get current position
const startX = parseFloat(rect.getAttribute("x"));
const startY = parseFloat(rect.getAttribute("y"));
// Calculate target position in destination stack
const endX = nodePositions[toNode].x - nodeWidth / 2;
const endY = nodeY + nodeHeight + podsPerNode[toNode].length * 25;
let progress = 0;
const steps = 60;
const dx = (endX - startX) / steps;
const dy = (endY - startY) / steps;
// Animate pod
const anim = setInterval(() => {
progress++;
rect.setAttribute("x", startX + dx * progress);
rect.setAttribute("y", startY + dy * progress);
text.setAttribute("x", startX + dx * progress + nodeWidth / 2);
text.setAttribute("y", startY + dy * progress + 10);
if (progress >= steps) {
clearInterval(anim);
// Add pod to destination stack permanently
podsPerNode[toNode].push(g);
// Ensure destination stack is redrawn correctly
redrawPods(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);
});
updateSVGHeight();
}
// 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>
+201
View File
@@ -0,0 +1,201 @@
<!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;
}
// Redraw the stack of pods for a node
function redrawPods(nodeName){
const stack = podsPerNode[nodeName] || [];
stack.forEach((g,i)=>{
const rect = g.querySelector("rect");
const text = g.querySelector("text");
const x = nodePositions[nodeName].x - nodeWidth/2;
const y = nodeY + nodeHeight + i*25;
rect.setAttribute("x", x);
rect.setAttribute("y", y);
text.setAttribute("x", x + nodeWidth/2);
text.setAttribute("y", y + 10);
});
}
// Animate pod moving from one node to another
function flyPod(podName, fromNode, toNode){
const g = document.createElementNS("http://www.w3.org/2000/svg","g");
// Pod rectangle
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);
// Pod text
const text = document.createElementNS("http://www.w3.org/2000/svg","text");
text.textContent = podName;
text.setAttribute("class","pod-label");
g.appendChild(text);
svg.appendChild(g);
// Remove from fromNode stack
if(fromNode && podsPerNode[fromNode]){
podsPerNode[fromNode].forEach(pg => {
const t = pg.querySelector("text");
if(t.textContent === podName){
svg.removeChild(pg); // remove visually
}
});
podsPerNode[fromNode] = podsPerNode[fromNode].filter(pg=>{
const t = pg.querySelector("text");
return t.textContent !== podName;
});
redrawPods(fromNode); // redraw remaining pods
}
// Animate movement
let start = fromNode ? nodePositions[fromNode] : nodePositions[toNode]; // start at old node or directly at target
let end = nodePositions[toNode];
if(!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);
// remove animated g
svg.removeChild(g);
// Add pod to new node stack
addPod(podName, toNode);
}
}, animationDuration/steps);
}
// 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>
+260
View File
@@ -0,0 +1,260 @@
<!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; }
header {
display: flex;
align-items: center;
margin-bottom: 20px;
background-color: #f0f0f0; /* light gray background for the header */
padding: 10px 15px;
border-radius: 8px;
}
header .logo {
font-weight: bold;
font-size: 18px;
color: #2196F3; /* blue logo text */
margin-right: 15px;
padding: 5px 10px;
background-color: #333; /* dark background behind logo text */
color: #fff;
border-radius: 4px;
font-family: monospace;
}
header .title {
font-size: 18px;
}
header .title span {
margin-left: 10px;
color: #555;
font-weight: normal;
}
header .cluster-label {
margin-left: 20px;
color: #333;
font-weight: normal;
}
</style>
</head>
<body>
<header>
<div class="logo">allarddcs</div>
<div class="title">
Node Balancer <span id="version">v?</span>
<span class="cluster-label">Cluster: <span id="cluster-name">?</span></span>
</div>
</header>
<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;
document.getElementById("version").textContent = "v" + cfg.version;
document.getElementById("cluster-name").textContent = cfg.clusterName;
});
// Draw nodes
fetch("/api/nodes")
.then(res => res.json())
.then(nodeNames => {
nodeNames.forEach((name, i) => {
const x = 50 + i*200;
// 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);
// store position and rect reference
nodePositions[name] = {
x: x + nodeWidth/2,
y: nodeY + nodeHeight/2,
rect: rect
};
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 nodeRect = nodePositions[nodeName].rect;
// Update node box height first
const newHeight = nodeHeight + (index + 1)*(20 + 5); // 20 = pod height, 5 = spacing
nodeRect.setAttribute("height", newHeight);
// Pod Y now starts right below node rect
const x = nodePositions[nodeName].x - nodeWidth/2;
const y = nodeY + newHeight - (index + 1)*(20 + 5) + 5; // stack pods inside the box
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);
text.setAttribute("class","pod-label");
const maxChars = Math.floor(nodeWidth / 8);
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);
addPod(podName, toNode); // add permanently
}
}, animationDuration/steps);
}
// Redraw all pods on a node and adjust node box height
function redrawPods(nodeName){
if(!podsPerNode[nodeName]) return;
const stack = podsPerNode[nodeName];
const nodeRect = nodePositions[nodeName].rect;
const newHeight = nodeHeight + stack.length*(20 + 5);
nodeRect.setAttribute("height", newHeight);
stack.forEach((g, idx)=>{
const rect = g.querySelector("rect");
const text = g.querySelector("text");
const x = nodePositions[nodeName].x - nodeWidth/2;
const y = nodeY + newHeight - (stack.length - idx)*(20 + 5) + 5; // align pods inside node box
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){
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>
+362
View File
@@ -0,0 +1,362 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Node Balancer</title>
<style>
html, body {
margin: 0;
padding: 0;
min-height: 100%;
}
body { font-family: sans-serif; margin: 20px; }
svg { border:1px solid #ccc; width:100%; 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; fill:#fff; }
header {
display: flex;
align-items: center;
margin-bottom: 20px;
background-color: #f0f0f0;
padding: 10px 15px;
border-radius: 8px;
}
header .logo {
font-weight: bold;
font-size: 18px;
color: #2196F3;
margin-right: 15px;
padding: 5px 10px;
background-color: #333;
color: #fff;
border-radius: 4px;
font-family: monospace;
}
header .title {
font-size: 18px;
}
header .title span {
margin-left: 10px;
color: #555;
font-weight: normal;
}
header .cluster-label {
margin-left: 20px;
color: #333;
font-weight: normal;
}
</style>
</head>
<body>
<header>
<div class="logo">allarddcs</div>
<div class="title">
Node Balancer <span id="version">v?</span>
<span class="cluster-label">Cluster: <span id="cluster-name">?</span></span>
</div>
</header>
<svg id="canvas"></svg>
<script>
const svg = document.getElementById("canvas");
// Node/pod layout constants
const nodePositions = {};
const nodeY = 50;
let nodeWidth = 120;
const nodeHeight = 55;
const podHeight = 48;
const podSpacing = 6;
const textPaddingY = 6;
let animationDuration = 2000;
const podsPerNode = {};
// Fetch config
fetch("/api/config")
.then(res => res.json())
.then(cfg => {
animationDuration = cfg.animationDurationMs;
document.getElementById("version").textContent = "v" + cfg.version;
document.getElementById("cluster-name").textContent = cfg.clusterName;
});
// Remove trailing k8s-style hash if present
function truncatePodName(podName) {
const parts = podName.split("-");
function isPodId(seg) { return /^[a-z0-9]{5}$/.test(seg); }
function isReplicaSetHash(seg) { return /^[a-f0-9]{8,10}$/.test(seg); }
if (parts.length > 1 && isPodId(parts[parts.length - 1])) parts.pop();
if (parts.length > 1 && isReplicaSetHash(parts[parts.length - 1])) parts.pop();
return parts.join("-");
}
// Split pod name into multiple lines
function splitPodNameLines(podName, maxCharsPerLine = 16) {
const parts = podName.split("-");
const lines = [];
let currentLine = "";
for (let i = 0; i < parts.length; i++) {
const part = parts[i];
const isLastPart = i === parts.length - 1;
if (currentLine.length === 0) {
currentLine = part;
continue;
}
const candidate = currentLine + "-" + part;
if (candidate.length <= maxCharsPerLine) {
currentLine = candidate;
} else {
// finalize current line with dash (since more parts follow)
lines.push(currentLine + "-");
currentLine = part;
}
}
// last line never gets trailing dash
if (currentLine.length > 0) {
lines.push(currentLine);
}
return lines;
}
// Draw a node (responsive)
function addNode(name, index, totalNodes){
const svgWidth = svg.clientWidth || window.innerWidth;
const outerPadding = 60; // distance from canvas edge
const innerMargin = 20; // breathing space inside edges
const usableWidth = svgWidth - 2 * outerPadding;
const spacing = totalNodes > 1
? usableWidth / totalNodes
: 0;
// Optional responsive node width scaling
const maxNodeWidth = 120;
const minNodeWidth = 60;
nodeWidth = Math.max(minNodeWidth, Math.min(maxNodeWidth, spacing * 0.8));
// Center nodes within their spacing slot
const centerX = outerPadding + spacing * index + spacing / 2;
const x = centerX - nodeWidth / 2;
nodePositions[name] = {
x: centerX,
y: nodeY + nodeHeight/2
};
// Node rectangle
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", centerX);
label.setAttribute("y", nodeY + nodeHeight/2);
label.setAttribute("class","label");
label.setAttribute("text-anchor","middle");
label.setAttribute("dominant-baseline","middle");
label.textContent = name;
svg.appendChild(label);
podsPerNode[name] = [];
}
// Add a pod
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*(podHeight + podSpacing);
const y = nodeY + nodeHeight + podSpacing + index * (podHeight + podSpacing);
const g = document.createElementNS("http://www.w3.org/2000/svg","g");
g.dataset.podName = podName;
// Pod rectangle
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", podHeight);
rect.setAttribute("class","pod");
g.appendChild(rect);
// Pod label
const text = document.createElementNS("http://www.w3.org/2000/svg","text");
text.setAttribute("class","pod-label");
text.setAttribute("text-anchor","middle");
text.setAttribute("x", x + nodeWidth/2);
const truncated = truncatePodName(podName);
const lines = splitPodNameLines(truncated, 14);
if(lines.length === 1){
// Single line: vertically centered
text.setAttribute("y", y + podHeight/2 + textPaddingY/2);
text.setAttribute("dominant-baseline","middle");
text.textContent = lines[0];
} else {
// Multi-line
const lineHeight = 12;
const totalTextHeight = lines.length * lineHeight;
// Center block vertically inside pod
const startY = y + (podHeight - totalTextHeight)/2 + lineHeight - 2;
text.setAttribute("y", startY);
lines.forEach((line, i) => {
const tspan = document.createElementNS("http://www.w3.org/2000/svg","tspan");
tspan.setAttribute("x", x + nodeWidth/2);
tspan.setAttribute("dy", i === 0 ? "0" : lineHeight);
tspan.textContent = line;
text.appendChild(tspan);
});
}
g.appendChild(text);
svg.appendChild(g);
stack.push(g);
podsPerNode[nodeName] = stack;
updateSVGHeight();
}
// Update SVG height
function updateSVGHeight() {
let maxY = nodeY + nodeHeight;
for(const nodeName in podsPerNode){
const stack = podsPerNode[nodeName];
if(!stack || stack.length===0) continue;
const bottomY = nodeY + nodeHeight + stack.length*(podHeight + podSpacing);
if(bottomY > maxY) maxY = bottomY;
}
svg.setAttribute("height", maxY + 20);
}
// Animate pod move
function flyPod(podName, fromNode, toNode){
if(!fromNode || !toNode) return;
const fromStack = podsPerNode[fromNode];
const idx = fromStack.findIndex(g => g.dataset.podName===podName);
if(idx===-1) return;
const g = fromStack[idx];
fromStack.splice(idx,1);
redrawPods(fromNode);
svg.appendChild(g);
const rect = g.querySelector("rect");
const text = g.querySelector("text");
const startX = parseFloat(rect.getAttribute("x"));
const startY = parseFloat(rect.getAttribute("y"));
const endX = nodePositions[toNode].x - nodeWidth/2;
const endY = nodeY + nodeHeight + podsPerNode[toNode].length*(podHeight+podSpacing);
let progress = 0;
const steps = 60;
const dx = (endX - startX)/steps;
const dy = (endY - startY)/steps;
const anim = setInterval(()=>{
progress++;
const nx = startX + dx*progress;
const ny = startY + dy*progress;
rect.setAttribute("x", nx);
rect.setAttribute("y", ny);
text.setAttribute("x", nx + nodeWidth/2);
text.setAttribute("y", ny + podHeight/2 + textPaddingY/2);
if(progress>=steps){
clearInterval(anim);
podsPerNode[toNode].push(g);
redrawPods(toNode);
}
}, animationDuration/steps);
}
// Redraw pods on a node
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 + idx*(podHeight+podSpacing);
rect.setAttribute("x", x);
rect.setAttribute("y", y);
text.setAttribute("x", x + nodeWidth/2);
text.setAttribute("y", y + podHeight/2 + textPaddingY/2);
});
updateSVGHeight();
}
// Fetch authoritative cluster state and redraw
function refreshCluster(){
Promise.all([
fetch("/api/nodes").then(r=>r.json()),
fetch("/api/pods").then(r=>r.json())
]).then(([nodeNames, podsByNode])=>{
while(svg.firstChild) svg.removeChild(svg.firstChild);
for(const k in nodePositions) delete nodePositions[k];
for(const k in podsPerNode) delete podsPerNode[k];
nodeNames.forEach((name,i)=>addNode(name,i,nodeNames.length));
for(const nodeName in podsByNode){
podsByNode[nodeName].forEach(podName=>addPod(podName,nodeName));
}
updateSVGHeight();
});
}
// SSE events
const evtSource = new EventSource("/api/stream");
evtSource.onmessage = e=>{
const evt = JSON.parse(e.data);
if(evt.fromNode && evt.toNode && evt.fromNode !== evt.toNode){
flyPod(evt.pod, evt.fromNode, evt.toNode);
setTimeout(refreshCluster, animationDuration);
} else {
refreshCluster();
}
};
// Periodic refresh every 5s
setInterval(refreshCluster, 5000);
// Responsive redraw on resize
window.addEventListener("resize", refreshCluster);
// Initial load
refreshCluster();
</script>
</body>
</html>
+117
View File
@@ -0,0 +1,117 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Node Balancer WebUI</title>
<style>
body {
font-family: sans-serif;
margin: 20px;
}
svg {
border: 1px solid #ccc;
width: 100%;
height: 300px;
background: #f9f9f9;
}
.node {
fill: #4CAF50;
stroke: #333;
stroke-width: 2;
}
.pod {
fill: #2196F3;
stroke: #000;
stroke-width: 1;
}
.pod text {
font-size: 10px;
fill: #fff;
text-anchor: middle;
dominant-baseline: middle;
}
.label {
font-size: 14px;
text-anchor: middle;
}
</style>
</head>
<body>
<h2>Node Balancer</h2>
<svg id="canvas"></svg>
<script>
const svg = document.getElementById("canvas");
const nodeNames = ["worker-1","worker-2","worker-3"];
const nodePositions = {};
const nodeY = 50;
const nodeWidth = 100;
const nodeHeight = 40;
// draw nodes
nodeNames.forEach((name,i)=>{
const x = 50 + i*200;
nodePositions[name] = {x: x + nodeWidth/2, y: nodeY + nodeHeight/2};
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);
const label = document.createElementNS("http://www.w3.org/2000/svg","text");
label.setAttribute("x", x + nodeWidth/2);
label.setAttribute("y", nodeY + nodeHeight/2 + 25);
label.setAttribute("class","label");
label.textContent = name;
svg.appendChild(label);
});
// animate a pod
function flyPod(podName, fromNode, toNode){
const g = document.createElementNS("http://www.w3.org/2000/svg","g");
const circle = document.createElementNS("http://www.w3.org/2000/svg","circle");
circle.setAttribute("r", 15);
circle.setAttribute("class","pod");
g.appendChild(circle);
const text = document.createElementNS("http://www.w3.org/2000/svg","text");
text.textContent = podName;
g.appendChild(text);
svg.appendChild(g);
let start = nodePositions[fromNode];
let end = nodePositions[toNode];
let progress = 0;
const duration = 2000; // ms
const steps = 60;
const dx = (end.x - start.x)/steps;
const dy = (end.y - start.y)/steps;
const anim = setInterval(()=>{
progress++;
const cx = start.x + dx*progress;
const cy = start.y + dy*progress;
circle.setAttribute("cx", cx);
circle.setAttribute("cy", cy);
text.setAttribute("x", cx);
text.setAttribute("y", cy);
if(progress >= steps){
clearInterval(anim);
// leave pod at destination for a short moment then remove
setTimeout(()=>svg.removeChild(g),1000);
}
}, duration/steps);
}
// SSE stream
const evtSource = new EventSource("/api/stream");
evtSource.onmessage = e => {
const evt = JSON.parse(e.data);
flyPod(evt.pod, evt.fromNode, evt.toNode);
};
</script>
</body>
</html>