version 0.7 - node labels, os details, master is purple
Build and Publish Docker Image / Build and Push Docker Image (push) Successful in 2m5s Details

This commit is contained in:
Isaac Johnson 2026-07-28 07:05:24 -05:00
parent 339d79d9f0
commit e73e5257c9
3 changed files with 290 additions and 100 deletions

38
app.py
View File

@ -94,6 +94,37 @@ def cluster_data():
for node in nodes: for node in nodes:
node_name = node.metadata.name node_name = node.metadata.name
internal_ip = "N/A"
if node.status and getattr(node.status, 'addresses', None):
for addr in node.status.addresses:
addr_type = getattr(addr, 'type', None) or (addr.get('type') if isinstance(addr, dict) else None)
addr_val = getattr(addr, 'address', None) or (addr.get('address') if isinstance(addr, dict) else None)
if addr_type == 'InternalIP':
internal_ip = addr_val
break
os_image = "N/A"
if node.status and getattr(node.status, 'node_info', None):
node_info = node.status.node_info
if isinstance(node_info, dict):
os_image = node_info.get('osImage') or node_info.get('os_image') or "N/A"
else:
os_image = getattr(node_info, 'os_image', None) or getattr(node_info, 'os_image', "N/A")
labels = node.metadata.labels or {}
roles = []
for k in labels:
if k.startswith("node-role.kubernetes.io/"):
role_name = k.split("/", 1)[1]
if role_name:
roles.append(role_name)
is_control_plane = any(r in ["control-plane", "master"] for r in roles) or ("control-plane" in node_name) or ("master" in node_name)
if is_control_plane and not roles:
roles = ["control-plane" if "control-plane" in node_name else "master"]
elif not roles:
roles = ["worker"]
cpu_allocatable = parse_cpu(node.status.allocatable.get('cpu', '0')) cpu_allocatable = parse_cpu(node.status.allocatable.get('cpu', '0'))
mem_allocatable = parse_memory(node.status.allocatable.get('memory', '0')) mem_allocatable = parse_memory(node.status.allocatable.get('memory', '0'))
@ -103,6 +134,13 @@ def cluster_data():
node_data = { node_data = {
"name": node_name, "name": node_name,
"type": "node", "type": "node",
"roles": roles,
"is_control_plane": is_control_plane,
"isControlPlane": is_control_plane,
"internal_ip": internal_ip,
"internalIp": internal_ip,
"os_image": os_image,
"osImage": os_image,
"cpu_allocatable": cpu_allocatable, "cpu_allocatable": cpu_allocatable,
"mem_allocatable": mem_allocatable, "mem_allocatable": mem_allocatable,
"cpu_usage": node_usage_cpu, "cpu_usage": node_usage_cpu,

View File

@ -1,3 +1,83 @@
// Patch Three.js r128 Sprite raycast camera space vs world space bug
(function() {
const _worldScale = new THREE.Vector3();
const _mvPosition = new THREE.Vector3();
const _vA = new THREE.Vector3();
const _vB = new THREE.Vector3();
const _vC = new THREE.Vector3();
const _uvC = new THREE.Vector2();
const _intersectPoint = new THREE.Vector3();
const _modelViewMatrix = new THREE.Matrix4();
THREE.Sprite.prototype.raycast = function (raycaster, intersects) {
if (!raycaster.camera) return;
_worldScale.setFromMatrixScale(this.matrixWorld);
_modelViewMatrix.multiplyMatrices(raycaster.camera.matrixWorldInverse, this.matrixWorld);
_mvPosition.setFromMatrixPosition(_modelViewMatrix);
if (raycaster.camera.isPerspectiveCamera && this.material.sizeAttenuation === false) {
_worldScale.multiplyScalar(-_mvPosition.z);
}
const rotation = this.material.rotation;
let sin = 0, cos = 1;
if (rotation !== 0) {
cos = Math.cos(rotation);
sin = Math.sin(rotation);
}
const center = this.center;
const transformVertex = (v, mvPosition, center, scale, sin, cos) => {
v.x -= center.x;
v.y -= center.y;
let rx = v.x * scale.x;
let ry = v.y * scale.y;
if (sin !== 0) {
const x = rx;
const y = ry;
rx = cos * x - sin * y;
ry = sin * x + cos * y;
}
v.x = rx + mvPosition.x;
v.y = ry + mvPosition.y;
v.z = mvPosition.z;
// Transform from Camera space to World space
v.applyMatrix4(raycaster.camera.matrixWorld);
};
transformVertex(_vA.set(-0.5, -0.5, 0), _mvPosition, center, _worldScale, sin, cos);
transformVertex(_vB.set(0.5, -0.5, 0), _mvPosition, center, _worldScale, sin, cos);
transformVertex(_vC.set(0.5, 0.5, 0), _mvPosition, center, _worldScale, sin, cos);
let intersect = raycaster.ray.intersectTriangle(_vA, _vB, _vC, false, _intersectPoint);
if (intersect === null) {
transformVertex(_vB.set(-0.5, 0.5, 0), _mvPosition, center, _worldScale, sin, cos);
intersect = raycaster.ray.intersectTriangle(_vA, _vC, _vB, false, _intersectPoint);
if (intersect === null) {
return;
}
}
const distance = raycaster.ray.origin.distanceTo(_intersectPoint);
if (distance < raycaster.near || distance > raycaster.far) return;
intersects.push({
distance: distance,
point: _intersectPoint.clone(),
uv: _uvC.clone(),
face: null,
object: this
});
};
})();
let currentMetric = 'cpu'; let currentMetric = 'cpu';
let clusterData = null; let clusterData = null;
@ -188,24 +268,31 @@ async function fetchData() {
} }
// Helper to create 3D Canvas Text Sprite for Node Labels // Helper to create 3D Canvas Text Sprite for Node Labels
function createTextSprite(text, fontSize = 28, textColor = '#f8fafc', bgColor = 'rgba(15, 23, 42, 0.85)') { function createTextSprite(text, fontSize = 28, textColor = '#f8fafc', bgColor = 'rgba(15, 23, 42, 0.85)', isBold = false, isControlPlane = false) {
const canvas = document.createElement('canvas'); const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d'); const ctx = canvas.getContext('2d');
canvas.width = 512;
canvas.height = 128; // Allocate higher resolution canvas for crisp text
canvas.width = isControlPlane ? 640 : 512;
canvas.height = isControlPlane ? 160 : 128;
const pad = 16;
const w = canvas.width - pad * 2;
const h = canvas.height - pad * 2;
// Pill background // Pill background
ctx.fillStyle = bgColor; ctx.fillStyle = bgColor;
ctx.roundRect(16, 16, canvas.width - 32, canvas.height - 32, 24); ctx.roundRect(pad, pad, w, h, isControlPlane ? 32 : 24);
ctx.fill(); ctx.fill();
// Border // Border
ctx.strokeStyle = 'rgba(59, 130, 246, 0.6)'; ctx.strokeStyle = isControlPlane ? 'rgba(168, 85, 247, 0.95)' : 'rgba(59, 130, 246, 0.6)';
ctx.lineWidth = 6; ctx.lineWidth = isControlPlane ? 8 : 6;
ctx.stroke(); ctx.stroke();
// Text // Text - use 900 weight for control plane / master, 700 for regular
ctx.font = `700 ${fontSize}px Inter, sans-serif`; const fontWeight = isControlPlane ? '900' : (isBold ? '800' : '700');
ctx.font = `${fontWeight} ${fontSize}px Inter, sans-serif`;
ctx.fillStyle = textColor; ctx.fillStyle = textColor;
ctx.textAlign = 'center'; ctx.textAlign = 'center';
ctx.textBaseline = 'middle'; ctx.textBaseline = 'middle';
@ -214,7 +301,21 @@ function createTextSprite(text, fontSize = 28, textColor = '#f8fafc', bgColor =
const texture = new THREE.CanvasTexture(canvas); const texture = new THREE.CanvasTexture(canvas);
const spriteMaterial = new THREE.SpriteMaterial({ map: texture, transparent: true }); const spriteMaterial = new THREE.SpriteMaterial({ map: texture, transparent: true });
const sprite = new THREE.Sprite(spriteMaterial); const sprite = new THREE.Sprite(spriteMaterial);
sprite.scale.set(45, 11.25, 1);
// Differentiate scale: control plane sprite is larger
if (isControlPlane) {
sprite.scale.set(58, 14.5, 1);
} else {
sprite.scale.set(45, 11.25, 1);
}
// Add invisible Mesh plane for 100% reliable raycasting
const hitGeo = new THREE.PlaneGeometry(1, 1);
const hitMat = new THREE.MeshBasicMaterial({ visible: false, depthWrite: false });
const hitMesh = new THREE.Mesh(hitGeo, hitMat);
hitMesh.name = 'labelHitMesh';
sprite.add(hitMesh);
return sprite; return sprite;
} }
@ -282,9 +383,13 @@ function build3DCluster(data) {
const centerX = (n.x0 + n.x1) / 2 - BOUNDS_SIZE / 2; const centerX = (n.x0 + n.x1) / 2 - BOUNDS_SIZE / 2;
const centerZ = (n.y0 + n.y1) / 2 - BOUNDS_SIZE / 2; const centerZ = (n.y0 + n.y1) / 2 - BOUNDS_SIZE / 2;
const isControlPlane = n.data.isControlPlane || n.data.is_control_plane ||
(n.data.roles && (n.data.roles.includes('control-plane') || n.data.roles.includes('master'))) ||
n.data.name.includes('control-plane') || n.data.name.includes('master');
const platformGeo = new THREE.BoxGeometry(nw, 1.5, nh); const platformGeo = new THREE.BoxGeometry(nw, 1.5, nh);
const platformMat = new THREE.MeshStandardMaterial({ const platformMat = new THREE.MeshStandardMaterial({
color: 0x1e293b, color: isControlPlane ? 0x1e1b4b : 0x1e293b,
roughness: 0.5, roughness: 0.5,
metalness: 0.3, metalness: 0.3,
transparent: true, transparent: true,
@ -294,18 +399,38 @@ function build3DCluster(data) {
platformMesh.position.set(centerX, 0.75, centerZ); platformMesh.position.set(centerX, 0.75, centerZ);
platformMesh.receiveShadow = true; platformMesh.receiveShadow = true;
const nodeUserData = {
type: 'node',
name: n.data.name,
isControlPlane: isControlPlane,
roles: n.data.roles || (isControlPlane ? ['control-plane'] : ['worker']),
internalIp: n.data.internalIp || n.data.internal_ip || 'N/A',
osImage: n.data.osImage || n.data.os_image || 'N/A'
};
platformMesh.userData = nodeUserData;
// Platform Border Frame // Platform Border Frame
const edges = new THREE.EdgesGeometry(platformGeo); const edges = new THREE.EdgesGeometry(platformGeo);
const lineMat = new THREE.LineBasicMaterial({ color: 0x3b82f6, linewidth: 2 }); const frameColor = isControlPlane ? 0xa855f7 : 0x3b82f6;
const lineMat = new THREE.LineBasicMaterial({ color: frameColor, linewidth: isControlPlane ? 3 : 2 });
const wireframe = new THREE.LineSegments(edges, lineMat); const wireframe = new THREE.LineSegments(edges, lineMat);
platformMesh.add(wireframe); platformMesh.add(wireframe);
scene.add(platformMesh); scene.add(platformMesh);
nodeMeshes.push(platformMesh); nodeMeshes.push(platformMesh);
// Node Label Sprite // Node Label Sprite (Larger, bold font for control-plane/master nodes)
const sprite = createTextSprite(n.data.name, 26, '#f8fafc'); const fontSize = isControlPlane ? 34 : 24;
sprite.position.set(centerX, 12, centerZ - nh / 2 - 4); const textColor = isControlPlane ? '#f3e8ff' : '#f8fafc';
const bgColor = isControlPlane ? 'rgba(46, 16, 101, 0.92)' : 'rgba(15, 23, 42, 0.85)';
const sprite = createTextSprite(n.data.name, fontSize, textColor, bgColor, true, isControlPlane);
sprite.position.set(centerX, isControlPlane ? 16 : 14, centerZ - nh / 2 - 2);
sprite.userData = nodeUserData;
const labelHit = sprite.getObjectByName('labelHitMesh');
if (labelHit) {
labelHit.userData = nodeUserData;
}
scene.add(sprite); scene.add(sprite);
nodeMeshes.push(sprite); nodeMeshes.push(sprite);
}); });
@ -432,106 +557,133 @@ function onPointerMove(event) {
mouse.y = -((event.clientY - rect.top) / container.clientHeight) * 2 + 1; mouse.y = -((event.clientY - rect.top) / container.clientHeight) * 2 + 1;
raycaster.setFromCamera(mouse, camera); raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObjects(podMeshes); const intersects = raycaster.intersectObjects([...podMeshes, ...nodeMeshes], true);
const tooltip = document.getElementById('tooltip'); const tooltip = document.getElementById('tooltip');
if (intersects.length > 0) { if (intersects.length > 0) {
const hitMesh = intersects[0].object; const hitMesh = intersects[0].object;
const data = (hitMesh.userData && hitMesh.userData.type) ? hitMesh.userData : (hitMesh.parent && hitMesh.parent.userData ? hitMesh.parent.userData : {});
if (hoveredPod !== hitMesh) { if (data && data.type) {
// Reset previous hovered pod container.style.cursor = 'pointer';
if (hoveredPod) {
hoveredPod.material.emissive.setHex(hoveredPod.originalEmissive); if (hoveredPod !== hitMesh) {
hoveredPod.material.emissiveIntensity = hoveredPod.originalEmissiveIntensity; // Reset previous hovered pod
if (hoveredPod && hoveredPod.userData && hoveredPod.userData.type !== 'node' && hoveredPod.material && hoveredPod.material.emissive) {
hoveredPod.material.emissive.setHex(hoveredPod.originalEmissive);
hoveredPod.material.emissiveIntensity = hoveredPod.originalEmissiveIntensity;
}
hoveredPod = hitMesh;
// Highlight hovered pod tower
if (hoveredPod.userData && hoveredPod.userData.type !== 'node' && hoveredPod.material && hoveredPod.material.emissive) {
hoveredPod.material.emissive.setHex(0x60a5fa);
hoveredPod.material.emissiveIntensity = 0.6;
}
} }
hoveredPod = hitMesh; // Display Tooltip
// Highlight hovered pod tower tooltip.classList.remove('hidden');
hoveredPod.material.emissive.setHex(0x60a5fa);
hoveredPod.material.emissiveIntensity = 0.6; // Boundary handling
const tWidth = 260;
let left = event.pageX + 15;
let top = event.pageY + 15;
if (left + tWidth > window.innerWidth) {
left = event.pageX - tWidth - 15;
}
tooltip.style.left = left + 'px';
tooltip.style.top = top + 'px';
if (data.type === 'node') {
const isCP = data.isControlPlane || (data.roles && (data.roles.includes('control-plane') || data.roles.includes('master')));
const badgeLabel = data.roles && data.roles.length ? data.roles.join(', ') : (isCP ? 'control-plane' : 'Node');
tooltip.innerHTML = `
<div class="tooltip-title">
<span>${data.name}</span>
<span style="font-size: 0.7rem; background: ${isCP ? 'rgba(168, 85, 247, 0.35)' : 'rgba(59, 130, 246, 0.3)'}; color: ${isCP ? '#e9d5ff' : '#60a5fa'}; padding: 2px 6px; border-radius: 4px; font-weight: 700;">${badgeLabel}</span>
</div>
<div class="tooltip-row">
<span class="tooltip-label">Internal IP:</span>
<span class="tooltip-value">${data.internalIp || 'N/A'}</span>
</div>
<div class="tooltip-row">
<span class="tooltip-label">OS Image:</span>
<span class="tooltip-value">${data.osImage || 'N/A'}</span>
</div>
`;
} else if (data.type === 'unused') {
const valCpu = formatCpu(data.cpu_raw);
const valMem = formatBytes(data.memory_raw);
tooltip.innerHTML = `
<div class="tooltip-title">
<span>Available Capacity</span>
<span class="legend-dot unused"></span>
</div>
<div class="tooltip-row">
<span class="tooltip-label">Node:</span>
<span class="tooltip-value">${data.nodeName}</span>
</div>
<div class="tooltip-row">
<span class="tooltip-label">Unused CPU:</span>
<span class="tooltip-value">${valCpu}</span>
</div>
<div class="tooltip-row">
<span class="tooltip-label">Unused Memory:</span>
<span class="tooltip-value">${valMem}</span>
</div>
`;
} else {
const valCpu = formatCpu(data.cpu_raw);
const valMem = formatBytes(data.memory_raw);
const statusColor = data.status === 'Running' ? '#10b981' : (data.status === 'Pending' ? '#f59e0b' : (data.status === 'Succeeded' ? '#94a3b8' : '#ef4444'));
tooltip.innerHTML = `
<div class="tooltip-title">
<span>${data.name}</span>
</div>
<div class="tooltip-row">
<span class="tooltip-label">Namespace:</span>
<span class="tooltip-value">${data.namespace}</span>
</div>
<div class="tooltip-row">
<span class="tooltip-label">Node:</span>
<span class="tooltip-value">${data.nodeName}</span>
</div>
<div class="tooltip-row">
<span class="tooltip-label">Status:</span>
<span class="tooltip-value" style="color: ${statusColor}; font-weight: 700;">${data.status}</span>
</div>
<div class="tooltip-row">
<span class="tooltip-label">CPU Usage:</span>
<span class="tooltip-value ${currentMetric === 'cpu' ? 'active-metric' : ''}">${valCpu}</span>
</div>
<div class="tooltip-row">
<span class="tooltip-label">Memory Usage:</span>
<span class="tooltip-value ${currentMetric === 'memory' ? 'active-metric' : ''}">${valMem}</span>
</div>
`;
}
return;
} }
}
// Display Tooltip container.style.cursor = 'grab';
tooltip.classList.remove('hidden'); if (hoveredPod) {
if (hoveredPod.userData && hoveredPod.userData.type !== 'node' && hoveredPod.material && hoveredPod.material.emissive) {
// Boundary handling
const tWidth = 260;
let left = event.pageX + 15;
let top = event.pageY + 15;
if (left + tWidth > window.innerWidth) {
left = event.pageX - tWidth - 15;
}
tooltip.style.left = left + 'px';
tooltip.style.top = top + 'px';
const data = hitMesh.userData;
const valCpu = formatCpu(data.cpu_raw);
const valMem = formatBytes(data.memory_raw);
if (data.type === 'unused') {
tooltip.innerHTML = `
<div class="tooltip-title">
<span>Available Capacity</span>
<span class="legend-dot unused"></span>
</div>
<div class="tooltip-row">
<span class="tooltip-label">Node:</span>
<span class="tooltip-value">${data.nodeName}</span>
</div>
<div class="tooltip-row">
<span class="tooltip-label">Unused CPU:</span>
<span class="tooltip-value">${valCpu}</span>
</div>
<div class="tooltip-row">
<span class="tooltip-label">Unused Memory:</span>
<span class="tooltip-value">${valMem}</span>
</div>
`;
} else {
const statusColor = data.status === 'Running' ? '#10b981' : (data.status === 'Pending' ? '#f59e0b' : (data.status === 'Succeeded' ? '#94a3b8' : '#ef4444'));
tooltip.innerHTML = `
<div class="tooltip-title">
<span>${data.name}</span>
</div>
<div class="tooltip-row">
<span class="tooltip-label">Namespace:</span>
<span class="tooltip-value">${data.namespace}</span>
</div>
<div class="tooltip-row">
<span class="tooltip-label">Node:</span>
<span class="tooltip-value">${data.nodeName}</span>
</div>
<div class="tooltip-row">
<span class="tooltip-label">Status:</span>
<span class="tooltip-value" style="color: ${statusColor}; font-weight: 700;">${data.status}</span>
</div>
<div class="tooltip-row">
<span class="tooltip-label">CPU Usage:</span>
<span class="tooltip-value ${currentMetric === 'cpu' ? 'active-metric' : ''}">${valCpu}</span>
</div>
<div class="tooltip-row">
<span class="tooltip-label">Memory Usage:</span>
<span class="tooltip-value ${currentMetric === 'memory' ? 'active-metric' : ''}">${valMem}</span>
</div>
`;
}
} else {
if (hoveredPod) {
hoveredPod.material.emissive.setHex(hoveredPod.originalEmissive); hoveredPod.material.emissive.setHex(hoveredPod.originalEmissive);
hoveredPod.material.emissiveIntensity = hoveredPod.originalEmissiveIntensity; hoveredPod.material.emissiveIntensity = hoveredPod.originalEmissiveIntensity;
hoveredPod = null;
} }
tooltip.classList.add('hidden'); hoveredPod = null;
} }
tooltip.classList.add('hidden');
} }
// Click to focus camera on selected Pod // Click to focus camera on selected Pod
function onPointerClick(event) { function onPointerClick(event) {
if (!hoveredPod) return; if (!hoveredPod || hoveredPod.userData.type === 'node') return;
let targetPos = hoveredPod.position.clone(); let targetPos = hoveredPod.position.clone();

View File

@ -1,2 +1,2 @@
[metadata] [metadata]
version = 0.6 version = 0.7