From e73e5257c9a68ec6067840410f25b09eb5ee310a Mon Sep 17 00:00:00 2001 From: Isaac Johnson Date: Tue, 28 Jul 2026 07:05:24 -0500 Subject: [PATCH] version 0.7 - node labels, os details, master is purple --- app.py | 44 ++++++- static/app.js | 344 ++++++++++++++++++++++++++++++++++++-------------- version.ini | 2 +- 3 files changed, 290 insertions(+), 100 deletions(-) diff --git a/app.py b/app.py index fc7c0e3..b454d7e 100644 --- a/app.py +++ b/app.py @@ -93,16 +93,54 @@ def cluster_data(): for node in nodes: 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')) mem_allocatable = parse_memory(node.status.allocatable.get('memory', '0')) - + node_usage_cpu = parse_cpu(node_metrics.get(node_name, {}).get('cpu', '0')) node_usage_mem = parse_memory(node_metrics.get(node_name, {}).get('memory', '0')) - + node_data = { "name": node_name, "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, "mem_allocatable": mem_allocatable, "cpu_usage": node_usage_cpu, diff --git a/static/app.js b/static/app.js index 4633dc9..f6fc70b 100644 --- a/static/app.js +++ b/static/app.js @@ -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 clusterData = null; @@ -188,24 +268,31 @@ async function fetchData() { } // 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 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 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(); // Border - ctx.strokeStyle = 'rgba(59, 130, 246, 0.6)'; - ctx.lineWidth = 6; + ctx.strokeStyle = isControlPlane ? 'rgba(168, 85, 247, 0.95)' : 'rgba(59, 130, 246, 0.6)'; + ctx.lineWidth = isControlPlane ? 8 : 6; ctx.stroke(); - // Text - ctx.font = `700 ${fontSize}px Inter, sans-serif`; + // Text - use 900 weight for control plane / master, 700 for regular + const fontWeight = isControlPlane ? '900' : (isBold ? '800' : '700'); + ctx.font = `${fontWeight} ${fontSize}px Inter, sans-serif`; ctx.fillStyle = textColor; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; @@ -214,7 +301,21 @@ function createTextSprite(text, fontSize = 28, textColor = '#f8fafc', bgColor = const texture = new THREE.CanvasTexture(canvas); const spriteMaterial = new THREE.SpriteMaterial({ map: texture, transparent: true }); 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; } @@ -282,9 +383,13 @@ function build3DCluster(data) { const centerX = (n.x0 + n.x1) / 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 platformMat = new THREE.MeshStandardMaterial({ - color: 0x1e293b, + color: isControlPlane ? 0x1e1b4b : 0x1e293b, roughness: 0.5, metalness: 0.3, transparent: true, @@ -294,18 +399,38 @@ function build3DCluster(data) { platformMesh.position.set(centerX, 0.75, centerZ); 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 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); platformMesh.add(wireframe); scene.add(platformMesh); nodeMeshes.push(platformMesh); - // Node Label Sprite - const sprite = createTextSprite(n.data.name, 26, '#f8fafc'); - sprite.position.set(centerX, 12, centerZ - nh / 2 - 4); + // Node Label Sprite (Larger, bold font for control-plane/master nodes) + const fontSize = isControlPlane ? 34 : 24; + 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); nodeMeshes.push(sprite); }); @@ -432,106 +557,133 @@ function onPointerMove(event) { mouse.y = -((event.clientY - rect.top) / container.clientHeight) * 2 + 1; raycaster.setFromCamera(mouse, camera); - const intersects = raycaster.intersectObjects(podMeshes); + const intersects = raycaster.intersectObjects([...podMeshes, ...nodeMeshes], true); const tooltip = document.getElementById('tooltip'); if (intersects.length > 0) { 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) { - // Reset previous hovered pod - if (hoveredPod) { - hoveredPod.material.emissive.setHex(hoveredPod.originalEmissive); - hoveredPod.material.emissiveIntensity = hoveredPod.originalEmissiveIntensity; + if (data && data.type) { + container.style.cursor = 'pointer'; + + if (hoveredPod !== hitMesh) { + // 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; - // Highlight hovered pod tower - hoveredPod.material.emissive.setHex(0x60a5fa); - hoveredPod.material.emissiveIntensity = 0.6; + // Display Tooltip + tooltip.classList.remove('hidden'); + + // 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 = ` +
+ ${data.name} + ${badgeLabel} +
+
+ Internal IP: + ${data.internalIp || 'N/A'} +
+
+ OS Image: + ${data.osImage || 'N/A'} +
+ `; + } else if (data.type === 'unused') { + const valCpu = formatCpu(data.cpu_raw); + const valMem = formatBytes(data.memory_raw); + tooltip.innerHTML = ` +
+ Available Capacity + +
+
+ Node: + ${data.nodeName} +
+
+ Unused CPU: + ${valCpu} +
+
+ Unused Memory: + ${valMem} +
+ `; + } 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 = ` +
+ ${data.name} +
+
+ Namespace: + ${data.namespace} +
+
+ Node: + ${data.nodeName} +
+
+ Status: + ${data.status} +
+
+ CPU Usage: + ${valCpu} +
+
+ Memory Usage: + ${valMem} +
+ `; + } + return; } + } - // Display Tooltip - tooltip.classList.remove('hidden'); - - // 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 = ` -
- Available Capacity - -
-
- Node: - ${data.nodeName} -
-
- Unused CPU: - ${valCpu} -
-
- Unused Memory: - ${valMem} -
- `; - } else { - const statusColor = data.status === 'Running' ? '#10b981' : (data.status === 'Pending' ? '#f59e0b' : (data.status === 'Succeeded' ? '#94a3b8' : '#ef4444')); - tooltip.innerHTML = ` -
- ${data.name} -
-
- Namespace: - ${data.namespace} -
-
- Node: - ${data.nodeName} -
-
- Status: - ${data.status} -
-
- CPU Usage: - ${valCpu} -
-
- Memory Usage: - ${valMem} -
- `; - } - - } else { - if (hoveredPod) { + container.style.cursor = 'grab'; + if (hoveredPod) { + if (hoveredPod.userData && hoveredPod.userData.type !== 'node' && hoveredPod.material && hoveredPod.material.emissive) { hoveredPod.material.emissive.setHex(hoveredPod.originalEmissive); hoveredPod.material.emissiveIntensity = hoveredPod.originalEmissiveIntensity; - hoveredPod = null; } - tooltip.classList.add('hidden'); + hoveredPod = null; } + tooltip.classList.add('hidden'); } // Click to focus camera on selected Pod function onPointerClick(event) { - if (!hoveredPod) return; + if (!hoveredPod || hoveredPod.userData.type === 'node') return; let targetPos = hoveredPod.position.clone(); diff --git a/version.ini b/version.ini index c19e277..6f4c939 100644 --- a/version.ini +++ b/version.ini @@ -1,2 +1,2 @@ [metadata] -version = 0.6 +version = 0.7