0.5 - Now in 3D
Build and Publish Docker Image / Build and Push Docker Image (push) Successful in 1m10s Details

This commit is contained in:
Isaac Johnson 2026-07-24 07:57:12 -05:00
parent c421e84eb1
commit 9e0d7d0726
4 changed files with 686 additions and 204 deletions

View File

@ -1,8 +1,21 @@
let currentMetric = 'cpu'; let currentMetric = 'cpu';
let clusterData = null; let clusterData = null;
// Three.js Core Variables
let scene, camera, renderer, controls;
let podMeshes = [];
let nodeMeshes = [];
let raycaster = new THREE.Raycaster();
let mouse = new THREE.Vector2();
let hoveredPod = null;
let maxCpuInCluster = 1;
let maxMemInCluster = 1;
let autoRotateEnabled = false;
// Formatters
const formatBytes = (bytes) => { const formatBytes = (bytes) => {
if (bytes === 0) return '0 B'; if (!bytes || bytes === 0) return '0 B';
const k = 1024; const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB', 'TB']; const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k)); const i = Math.floor(Math.log(bytes) / Math.log(k));
@ -10,193 +23,534 @@ const formatBytes = (bytes) => {
}; };
const formatCpu = (millicores) => { const formatCpu = (millicores) => {
if (millicores === 0) return '0 m'; if (!millicores || millicores === 0) return '0 m';
return `${millicores} m`; return `${millicores} m`;
}; };
document.getElementById('btn-cpu').addEventListener('click', (e) => { // UI Controls Setup
document.getElementById('btn-cpu').addEventListener('click', () => {
document.getElementById('btn-cpu').classList.add('active'); document.getElementById('btn-cpu').classList.add('active');
document.getElementById('btn-mem').classList.remove('active'); document.getElementById('btn-mem').classList.remove('active');
currentMetric = 'cpu'; currentMetric = 'cpu';
if (clusterData) renderChart(clusterData); updatePodHeights();
}); });
document.getElementById('btn-mem').addEventListener('click', (e) => { document.getElementById('btn-mem').addEventListener('click', () => {
document.getElementById('btn-mem').classList.add('active'); document.getElementById('btn-mem').classList.add('active');
document.getElementById('btn-cpu').classList.remove('active'); document.getElementById('btn-cpu').classList.remove('active');
currentMetric = 'memory'; currentMetric = 'memory';
if (clusterData) renderChart(clusterData); updatePodHeights();
}); });
async function fetchData() { document.getElementById('btn-reset-cam').addEventListener('click', () => {
try { resetCameraView();
const response = await fetch('/api/cluster-data'); });
if (!response.ok) throw new Error('Failed to fetch data');
clusterData = await response.json();
document.getElementById('loading').classList.add('hidden'); document.getElementById('btn-auto-rotate').addEventListener('click', () => {
renderChart(clusterData); autoRotateEnabled = !autoRotateEnabled;
} catch (error) { controls.autoRotate = autoRotateEnabled;
console.error(error); const btn = document.getElementById('btn-auto-rotate');
document.getElementById('loading').innerHTML = `<p style="color: #ef4444;">Error loading data: ${error.message}</p>`; const textSpan = document.getElementById('auto-rotate-text');
if (autoRotateEnabled) {
btn.classList.add('active');
textSpan.innerText = 'Rotating...';
} else {
btn.classList.remove('active');
textSpan.innerText = 'Auto Rotate';
} }
} });
function renderChart(data) { // Initialize 3D Engine
function init3D() {
const container = document.getElementById('chart-container'); const container = document.getElementById('chart-container');
const width = container.clientWidth; const width = container.clientWidth;
const height = container.clientHeight; const height = container.clientHeight;
// Clear previous // Scene
d3.select('#chart-container').selectAll('*').remove(); scene = new THREE.Scene();
scene.background = new THREE.Color(0x090d16);
scene.fog = new THREE.FogExp2(0x090d16, 0.0012);
const svg = d3.select('#chart-container') // Camera
.append('svg') camera = new THREE.PerspectiveCamera(45, width / height, 1, 2000);
.attr('width', width) camera.position.set(0, 140, 210);
.attr('height', height) camera.lookAt(0, 0, 0);
.style('font-family', 'Inter, sans-serif');
// Renderer
renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
renderer.setSize(width, height);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
// Clear previous elements if any
container.innerHTML = '';
container.appendChild(renderer.domElement);
// Lights
const ambientLight = new THREE.AmbientLight(0xffffff, 0.65);
scene.add(ambientLight);
const dirLight = new THREE.DirectionalLight(0xffffff, 0.85);
dirLight.position.set(120, 220, 120);
dirLight.castShadow = true;
dirLight.shadow.mapSize.width = 2048;
dirLight.shadow.mapSize.height = 2048;
scene.add(dirLight);
const bluePointLight = new THREE.PointLight(0x3b82f6, 1.2, 600);
bluePointLight.position.set(-150, 150, -150);
scene.add(bluePointLight);
const purplePointLight = new THREE.PointLight(0x8b5cf6, 1.0, 600);
purplePointLight.position.set(150, 150, 150);
scene.add(purplePointLight);
// Grid Floor
const gridHelper = new THREE.GridHelper(600, 60, 0x3b82f6, 0x1e293b);
gridHelper.position.y = -0.5;
scene.add(gridHelper);
// Orbit Controls
controls = new THREE.OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.05;
controls.maxPolarAngle = Math.PI / 2 - 0.02; // Don't go below ground
controls.minDistance = 20;
controls.maxDistance = 800;
controls.autoRotate = autoRotateEnabled;
controls.autoRotateSpeed = 1.0;
// Events
window.addEventListener('resize', onWindowResize);
container.addEventListener('mousemove', onPointerMove);
container.addEventListener('click', onPointerClick);
// Start animation loop
animate();
}
function resetCameraView() {
let startPos = camera.position.clone();
let targetPos = new THREE.Vector3(0, 140, 210);
let startLookAt = controls.target.clone();
let targetLookAt = new THREE.Vector3(0, 0, 0);
let duration = 600;
let startTime = performance.now();
function step(now) {
let progress = Math.min((now - startTime) / duration, 1);
// Ease out cubic
let ease = 1 - Math.pow(1 - progress, 3);
camera.position.lerpVectors(startPos, targetPos, ease);
controls.target.lerpVectors(startLookAt, targetLookAt, ease);
controls.update();
if (progress < 1) {
requestAnimationFrame(step);
}
}
requestAnimationFrame(step);
}
function onWindowResize() {
const container = document.getElementById('chart-container');
if (!container || !renderer || !camera) return;
const width = container.clientWidth;
const height = container.clientHeight;
camera.aspect = width / height;
camera.updateProjectionMatrix();
renderer.setSize(width, height);
}
// Fetch Cluster Data
async function fetchData() {
try {
const response = await fetch('/api/cluster-data');
if (!response.ok) throw new Error('Failed to fetch cluster data');
clusterData = await response.json();
document.getElementById('loading').classList.add('hidden');
if (!scene) {
init3D();
}
build3DCluster(clusterData);
} catch (error) {
console.error(error);
document.getElementById('loading').innerHTML = `<p style="color: #ef4444; font-weight: 600;">Error loading cluster data: ${error.message}</p>`;
}
}
// Helper to create 3D Canvas Text Sprite for Node Labels
function createTextSprite(text, fontSize = 28, textColor = '#f8fafc', bgColor = 'rgba(15, 23, 42, 0.85)') {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
canvas.width = 512;
canvas.height = 128;
// Pill background
ctx.fillStyle = bgColor;
ctx.roundRect(16, 16, canvas.width - 32, canvas.height - 32, 24);
ctx.fill();
// Border
ctx.strokeStyle = 'rgba(59, 130, 246, 0.6)';
ctx.lineWidth = 6;
ctx.stroke();
// Text
ctx.font = `700 ${fontSize}px Inter, sans-serif`;
ctx.fillStyle = textColor;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(text, canvas.width / 2, canvas.height / 2);
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);
return sprite;
}
// Build 3D Topology from Data
function build3DCluster(data) {
// Clear existing node & pod meshes
podMeshes.forEach(mesh => {
scene.remove(mesh);
mesh.geometry.dispose();
if (mesh.material) mesh.material.dispose();
});
podMeshes = [];
nodeMeshes.forEach(obj => {
scene.remove(obj);
if (obj.geometry) obj.geometry.dispose();
if (obj.material) obj.material.dispose();
});
nodeMeshes = [];
if (!data || !data.children || data.children.length === 0) return;
// 1. Compute cluster max metrics for scaling height
maxCpuInCluster = 1;
maxMemInCluster = 1;
data.children.forEach(node => {
if (node.children) {
node.children.forEach(pod => {
if (pod.type === 'pod') {
if (pod.cpu_raw > maxCpuInCluster) maxCpuInCluster = pod.cpu_raw;
if (pod.memory_raw > maxMemInCluster) maxMemInCluster = pod.memory_raw;
}
});
}
});
// 2. Layout on X-Z floor using D3 Treemap
const BOUNDS_SIZE = 220; // 3D world dimension for ground layout
// Create hierarchy
const root = d3.hierarchy(data) const root = d3.hierarchy(data)
.sum(d => { .sum(d => {
if (d.type === 'node') return 0; if (d.type === 'node') return 0;
return d[currentMetric] || 0; // Ensure minimum footprint for every pod block
return Math.max(d.cpu_raw || 0, d.memory_raw ? d.memory_raw / 1000000 : 0, 15);
}) })
.sort((a, b) => b.value - a.value); .sort((a, b) => b.value - a.value);
// Create Treemap
d3.treemap() d3.treemap()
.size([width, height]) .size([BOUNDS_SIZE, BOUNDS_SIZE])
.paddingTop(28) .paddingTop(36)
.paddingRight(8) .paddingRight(10)
.paddingInner(4) .paddingInner(6)
.paddingBottom(8) .paddingBottom(10)
.paddingLeft(8) .paddingLeft(10)(root);
.round(true)(root);
// Get nodes and pods // 3. Render Nodes and Pods
const nodes = root.descendants().filter(d => d.depth === 1); // Nodes const nodes = root.descendants().filter(d => d.depth === 1);
const pods = root.leaves(); // Pods and Unused const pods = root.leaves();
// Color scale for pods // Render Node Platforms
const getColor = (d) => { nodes.forEach(n => {
if (d.data.type === 'unused') return 'var(--color-unused)'; const nw = Math.max(10, n.x1 - n.x0);
if (d.data.status === 'Running') return 'var(--color-running)'; const nh = Math.max(10, n.y1 - n.y0);
if (d.data.status === 'Pending') return 'var(--color-pending)'; const centerX = (n.x0 + n.x1) / 2 - BOUNDS_SIZE / 2;
return 'var(--color-failed)'; const centerZ = (n.y0 + n.y1) / 2 - BOUNDS_SIZE / 2;
};
// Draw Node Groups const platformGeo = new THREE.BoxGeometry(nw, 1.5, nh);
const nodeGroups = svg.selectAll('.node-group') const platformMat = new THREE.MeshStandardMaterial({
.data(nodes) color: 0x1e293b,
.enter() roughness: 0.5,
.append('g') metalness: 0.3,
.attr('class', 'node-group') transparent: true,
.attr('transform', d => `translate(${d.x0},${d.y0})`); opacity: 0.95
});
const platformMesh = new THREE.Mesh(platformGeo, platformMat);
platformMesh.position.set(centerX, 0.75, centerZ);
platformMesh.receiveShadow = true;
// Node Background // Platform Border Frame
nodeGroups.append('rect') const edges = new THREE.EdgesGeometry(platformGeo);
.attr('class', 'node-rect') const lineMat = new THREE.LineBasicMaterial({ color: 0x3b82f6, linewidth: 2 });
.attr('width', d => Math.max(0, d.x1 - d.x0)) const wireframe = new THREE.LineSegments(edges, lineMat);
.attr('height', d => Math.max(0, d.y1 - d.y0)); platformMesh.add(wireframe);
// Node Label scene.add(platformMesh);
nodeGroups.append('text') nodeMeshes.push(platformMesh);
.attr('class', 'node-label')
.attr('x', 8)
.attr('y', 20)
.text(d => d.data.name);
// Draw Pods // Node Label Sprite
const podGroups = svg.selectAll('.pod-group') const sprite = createTextSprite(n.data.name, 26, '#f8fafc');
.data(pods) sprite.position.set(centerX, 12, centerZ - nh / 2 - 4);
.enter() scene.add(sprite);
.append('g') nodeMeshes.push(sprite);
.attr('class', 'pod-group') });
.attr('transform', d => `translate(${d.x0},${d.y0})`);
// Pod Rect // Render Pod Towers
podGroups.append('rect') pods.forEach(p => {
.attr('class', 'pod-rect') const pw = Math.max(2, (p.x1 - p.x0) * 0.88);
.attr('width', d => Math.max(0, d.x1 - d.x0)) const pd = Math.max(2, (p.y1 - p.y0) * 0.88);
.attr('height', d => Math.max(0, d.y1 - d.y0)) const centerX = (p.x0 + p.x1) / 2 - BOUNDS_SIZE / 2;
.attr('fill', d => getColor(d)) const centerZ = (p.y0 + p.y1) / 2 - BOUNDS_SIZE / 2;
.on('mousemove', function(event, d) {
const tooltip = document.getElementById('tooltip');
tooltip.classList.remove('hidden');
// Boundary detection for tooltip // Calculate height
const tWidth = 240; const targetHeight = getTargetHeight(p.data);
let left = event.pageX + 15;
let top = event.pageY + 15;
if (left + tWidth > window.innerWidth) { // Box geometry: 1 unit height base, scaled in animation loop
left = event.pageX - tWidth - 15; const podGeo = new THREE.BoxGeometry(pw, 1, pd);
}
tooltip.style.left = left + 'px'; // Color & Material based on Pod Status
tooltip.style.top = top + 'px'; let color = 0x10b981; // Running emerald
let emissive = 0x059669;
let opacity = 0.9;
let transparent = false;
let val = currentMetric === 'cpu' ? formatCpu(d.data[`${currentMetric}_raw`]) : formatBytes(d.data[`${currentMetric}_raw`]); if (p.data.type === 'unused') {
color = 0x64748b;
emissive = 0x334155;
opacity = 0.25;
transparent = true;
} else if (p.data.status === 'Pending') {
color = 0xf59e0b;
emissive = 0xd97706;
} else if (p.data.status !== 'Running') {
color = 0xef4444;
emissive = 0xdc2626;
}
if (d.data.type === 'unused') { const podMat = new THREE.MeshStandardMaterial({
tooltip.innerHTML = ` color: color,
<div class="tooltip-title">Available Capacity</div> emissive: emissive,
<div class="tooltip-row"> emissiveIntensity: p.data.type === 'unused' ? 0.05 : 0.25,
<span class="tooltip-label">${currentMetric.toUpperCase()}:</span> roughness: 0.3,
<span class="tooltip-value">${val}</span> metalness: 0.4,
</div> transparent: transparent,
`; opacity: opacity
} else {
tooltip.innerHTML = `
<div class="tooltip-title">${d.data.name}</div>
<div class="tooltip-row">
<span class="tooltip-label">Namespace:</span>
<span class="tooltip-value">${d.data.namespace}</span>
</div>
<div class="tooltip-row">
<span class="tooltip-label">Status:</span>
<span class="tooltip-value">${d.data.status}</span>
</div>
<div class="tooltip-row">
<span class="tooltip-label">${currentMetric.toUpperCase()}:</span>
<span class="tooltip-value">${val}</span>
</div>
`;
}
})
.on('mouseout', function() {
document.getElementById('tooltip').classList.add('hidden');
}); });
// Pod Label (only if there is space) const podMesh = new THREE.Mesh(podGeo, podMat);
podGroups.append('text') podMesh.castShadow = true;
.attr('class', 'pod-label') podMesh.receiveShadow = true;
.attr('x', 4)
.attr('y', 14) // Initial positions & properties
.text(d => { podMesh.position.set(centerX, 1.5 + 0.1 / 2, centerZ);
if (d.data.type === 'unused') return ''; podMesh.scale.set(1, 0.1, 1);
const width = d.x1 - d.x0;
const height = d.y1 - d.y0; podMesh.userData = {
if (width > 60 && height > 20) { ...p.data,
let txt = d.data.name; nodeName: p.parent ? p.parent.data.name : 'Unknown Node'
if (txt.length * 5 > width) { };
return txt.substring(0, Math.floor(width/5) - 2) + '..'; podMesh.targetHeight = targetHeight;
} podMesh.currentHeight = 0.1;
return txt; podMesh.baseWidth = pw;
} podMesh.baseDepth = pd;
return ''; podMesh.baseCenterX = centerX;
}); podMesh.baseCenterZ = centerZ;
podMesh.originalColor = color;
podMesh.originalEmissive = emissive;
podMesh.originalEmissiveIntensity = p.data.type === 'unused' ? 0.05 : 0.25;
scene.add(podMesh);
podMeshes.push(podMesh);
});
} }
// Handle resize // Calculate target 3D tower height based on current metric
window.addEventListener('resize', () => { function getTargetHeight(data) {
if (clusterData) { if (data.type === 'unused') return 0.5; // low plate for available capacity
renderChart(clusterData);
}
});
// Initial fetch const MAX_HEIGHT = 50; // Maximum tower height units
const MIN_HEIGHT = 1.5; // Base minimum height for visibility
if (currentMetric === 'cpu') {
const val = data.cpu_raw || 0;
return MIN_HEIGHT + (val / (maxCpuInCluster || 1)) * MAX_HEIGHT;
} else {
const val = data.memory_raw || 0;
return MIN_HEIGHT + (val / (maxMemInCluster || 1)) * MAX_HEIGHT;
}
}
// Update heights smoothly when toggling metric
function updatePodHeights() {
podMeshes.forEach(mesh => {
mesh.targetHeight = getTargetHeight(mesh.userData);
});
}
// Main Render & Animation Loop
function animate() {
requestAnimationFrame(animate);
// Smoothly interpolate pod tower heights (lerp)
podMeshes.forEach(mesh => {
if (Math.abs(mesh.currentHeight - mesh.targetHeight) > 0.01) {
mesh.currentHeight += (mesh.targetHeight - mesh.currentHeight) * 0.1;
mesh.scale.y = mesh.currentHeight;
// Keep tower base resting on the node platform at y = 1.5
mesh.position.y = 1.5 + mesh.currentHeight / 2;
}
});
controls.update();
renderer.render(scene, camera);
}
// Mouse Pointer Raycasting & Hover Tooltip
function onPointerMove(event) {
const container = document.getElementById('chart-container');
const rect = container.getBoundingClientRect();
mouse.x = ((event.clientX - rect.left) / container.clientWidth) * 2 - 1;
mouse.y = -((event.clientY - rect.top) / container.clientHeight) * 2 + 1;
raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObjects(podMeshes);
const tooltip = document.getElementById('tooltip');
if (intersects.length > 0) {
const hitMesh = intersects[0].object;
if (hoveredPod !== hitMesh) {
// Reset previous hovered pod
if (hoveredPod) {
hoveredPod.material.emissive.setHex(hoveredPod.originalEmissive);
hoveredPod.material.emissiveIntensity = hoveredPod.originalEmissiveIntensity;
}
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';
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' : '#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.emissiveIntensity = hoveredPod.originalEmissiveIntensity;
hoveredPod = null;
}
tooltip.classList.add('hidden');
}
}
// Click to focus camera on selected Pod
function onPointerClick(event) {
if (!hoveredPod) return;
let targetPos = hoveredPod.position.clone();
let startLookAt = controls.target.clone();
let duration = 500;
let startTime = performance.now();
function step(now) {
let progress = Math.min((now - startTime) / duration, 1);
let ease = 1 - Math.pow(1 - progress, 3);
controls.target.lerpVectors(startLookAt, targetPos, ease);
controls.update();
if (progress < 1) {
requestAnimationFrame(step);
}
}
requestAnimationFrame(step);
}
// Initial fetch and auto-polling every 30s
fetchData(); fetchData();
// Poll every 30 seconds
setInterval(fetchData, 30000); setInterval(fetchData, 30000);

View File

@ -4,14 +4,14 @@
--text-muted: #94a3b8; --text-muted: #94a3b8;
--accent: #3b82f6; --accent: #3b82f6;
--accent-hover: #2563eb; --accent-hover: #2563eb;
--glass-bg: rgba(30, 41, 59, 0.7); --glass-bg: rgba(30, 41, 59, 0.75);
--glass-border: rgba(255, 255, 255, 0.1); --glass-border: rgba(255, 255, 255, 0.12);
/* Chart Colors */ /* Chart Colors */
--color-running: rgba(16, 185, 129, 0.8); --color-running: #10b981;
--color-pending: rgba(245, 158, 11, 0.8); --color-pending: #f59e0b;
--color-failed: rgba(239, 68, 68, 0.8); --color-failed: #ef4444;
--color-unused: rgba(148, 163, 184, 0.15); --color-unused: rgba(148, 163, 184, 0.2);
--color-node: rgba(30, 41, 59, 0.9); --color-node: rgba(30, 41, 59, 0.9);
} }
@ -36,14 +36,14 @@ body {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: 100vh; height: 100vh;
padding: 1.5rem; padding: 1.25rem;
gap: 1.5rem; gap: 1rem;
} }
.glass-panel { .glass-panel {
background: var(--glass-bg); background: var(--glass-bg);
backdrop-filter: blur(12px); backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(16px);
border: 1px solid var(--glass-border); border: 1px solid var(--glass-border);
border-radius: 1rem; border-radius: 1rem;
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.5), 0 8px 10px -6px rgba(0, 0, 0, 0.1); box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.5), 0 8px 10px -6px rgba(0, 0, 0, 0.1);
@ -53,7 +53,8 @@ body {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
padding: 1rem 2rem; padding: 0.85rem 1.75rem;
z-index: 20;
} }
.logo { .logo {
@ -68,6 +69,26 @@ body {
font-weight: 700; font-weight: 700;
color: var(--text-main); color: var(--text-main);
letter-spacing: -0.025em; letter-spacing: -0.025em;
display: flex;
align-items: center;
gap: 0.5rem;
}
.badge-3d {
background: linear-gradient(135deg, #3b82f6, #8b5cf6);
color: white;
font-size: 0.65rem;
font-weight: 800;
padding: 0.15rem 0.45rem;
border-radius: 0.375rem;
letter-spacing: 0.05em;
box-shadow: 0 2px 8px rgba(59, 130, 246, 0.4);
}
.controls {
display: flex;
align-items: center;
gap: 1rem;
} }
.toggle-container { .toggle-container {
@ -93,7 +114,39 @@ body {
.toggle-btn.active { .toggle-btn.active {
background: var(--accent); background: var(--accent);
color: white; color: white;
box-shadow: 0 2px 4px rgba(0,0,0,0.2); box-shadow: 0 2px 6px rgba(59, 130, 246, 0.4);
}
.action-buttons {
display: flex;
gap: 0.5rem;
}
.icon-btn {
display: flex;
align-items: center;
gap: 0.4rem;
background: rgba(15, 23, 42, 0.6);
border: 1px solid var(--glass-border);
color: var(--text-muted);
padding: 0.5rem 0.85rem;
font-size: 0.825rem;
font-weight: 600;
border-radius: 0.5rem;
cursor: pointer;
transition: all 0.2s ease;
}
.icon-btn:hover {
background: rgba(30, 41, 59, 0.9);
color: var(--text-main);
border-color: rgba(255, 255, 255, 0.25);
}
.icon-btn.active {
background: rgba(59, 130, 246, 0.2);
color: var(--accent);
border-color: var(--accent);
} }
.main-content { .main-content {
@ -114,7 +167,7 @@ body {
justify-content: center; justify-content: center;
gap: 1rem; gap: 1rem;
padding: 3rem; padding: 3rem;
z-index: 10; z-index: 30;
transition: opacity 0.3s ease; transition: opacity 0.3s ease;
} }
@ -139,46 +192,76 @@ body {
.chart-container { .chart-container {
width: 100%; width: 100%;
height: 100%; height: 100%;
cursor: grab;
} }
.node-group { .chart-container:active {
transition: all 0.3s ease; cursor: grabbing;
} }
.node-rect { .navigation-hint {
fill: var(--color-node); position: absolute;
stroke: var(--glass-border); top: 1rem;
stroke-width: 1px; left: 1rem;
rx: 8; display: flex;
ry: 8; gap: 1rem;
padding: 0.6rem 1rem;
font-size: 0.75rem;
color: var(--text-muted);
pointer-events: none;
z-index: 10;
} }
.pod-rect { .navigation-hint kbd {
stroke: rgba(0,0,0,0.3); background: rgba(255, 255, 255, 0.1);
stroke-width: 1px; border: 1px solid rgba(255, 255, 255, 0.2);
rx: 4; border-radius: 0.25rem;
ry: 4; padding: 0.1rem 0.35rem;
transition: fill 0.3s ease, opacity 0.3s ease, stroke 0.3s ease; font-family: inherit;
cursor: pointer;
}
.pod-rect:hover {
opacity: 0.9;
stroke: white;
}
.node-label {
fill: var(--text-main);
font-size: 0.875rem;
font-weight: 600; font-weight: 600;
color: var(--text-main);
}
.legend-panel {
position: absolute;
bottom: 1rem;
right: 1rem;
display: flex;
flex-direction: column;
gap: 0.4rem;
padding: 0.75rem 1.1rem;
font-size: 0.775rem;
z-index: 10;
pointer-events: none; pointer-events: none;
} }
.pod-label { .legend-item {
fill: white; display: flex;
font-size: 0.7rem; align-items: center;
pointer-events: none; gap: 0.5rem;
text-shadow: 0 1px 2px rgba(0,0,0,0.8); color: var(--text-muted);
}
.legend-dot {
width: 0.65rem;
height: 0.65rem;
border-radius: 50%;
}
.legend-dot.running { background-color: var(--color-running); box-shadow: 0 0 6px var(--color-running); }
.legend-dot.pending { background-color: var(--color-pending); box-shadow: 0 0 6px var(--color-pending); }
.legend-dot.failed { background-color: var(--color-failed); box-shadow: 0 0 6px var(--color-failed); }
.legend-dot.unused { background-color: #64748b; opacity: 0.5; }
.legend-divider {
height: 1px;
background: var(--glass-border);
margin: 0.25rem 0;
}
.legend-note {
font-size: 0.725rem;
color: var(--accent);
} }
.tooltip { .tooltip {
@ -186,9 +269,10 @@ body {
padding: 1rem; padding: 1rem;
pointer-events: none; pointer-events: none;
z-index: 100; z-index: 100;
font-size: 0.875rem; font-size: 0.85rem;
transition: opacity 0.2s ease, transform 0.1s ease; transition: opacity 0.2s ease, transform 0.1s ease;
min-width: 240px; min-width: 250px;
box-shadow: 0 12px 30px rgba(0, 0, 0, 0.6);
} }
.tooltip.hidden { .tooltip.hidden {
@ -201,12 +285,16 @@ body {
border-bottom: 1px solid var(--glass-border); border-bottom: 1px solid var(--glass-border);
padding-bottom: 0.5rem; padding-bottom: 0.5rem;
word-break: break-all; word-break: break-all;
color: var(--text-main);
display: flex;
justify-content: space-between;
align-items: center;
} }
.tooltip-row { .tooltip-row {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
margin-bottom: 0.25rem; margin-bottom: 0.3rem;
gap: 1rem; gap: 1rem;
} }
@ -216,4 +304,6 @@ body {
.tooltip-value { .tooltip-value {
font-weight: 600; font-weight: 600;
color: var(--text-main);
} }

View File

@ -3,12 +3,16 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Kubernetes Cluster Visualization</title> <title>Kubernetes Cluster Visualization - K8s Pulse 3D</title>
<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600;700&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/static/style.css"> <link rel="stylesheet" href="/static/style.css">
<!-- D3.js for 2D floor layout computation -->
<script src="https://d3js.org/d3.v7.min.js"></script> <script src="https://d3js.org/d3.v7.min.js"></script>
<!-- Three.js & OrbitControls for 3D rendering and mouse navigation -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/controls/OrbitControls.js"></script>
</head> </head>
<body> <body>
<div class="app-container"> <div class="app-container">
@ -19,12 +23,27 @@
<path d="M2 17L12 22L22 17" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> <path d="M2 17L12 22L22 17" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M2 12L12 17L22 12" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> <path d="M2 12L12 17L22 12" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg> </svg>
<h1>K8s Pulse</h1> <h1>K8s Pulse <span class="badge-3d">3D</span></h1>
</div> </div>
<div class="controls"> <div class="controls">
<div class="toggle-container"> <div class="toggle-container">
<button class="toggle-btn active" id="btn-cpu">CPU</button> <button class="toggle-btn active" id="btn-cpu">CPU Consumption</button>
<button class="toggle-btn" id="btn-mem">Memory</button> <button class="toggle-btn" id="btn-mem">Memory Consumption</button>
</div>
<div class="action-buttons">
<button class="icon-btn" id="btn-reset-cam" title="Reset Camera View">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/>
<path d="M3 3v5h5"/>
</svg>
<span>Reset View</span>
</button>
<button class="icon-btn" id="btn-auto-rotate" title="Toggle 3D Auto-Rotation">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M21.5 2v6h-6M21.34 15.57a10 10 0 1 1-.57-8.38l5.67-5.67"/>
</svg>
<span id="auto-rotate-text">Auto Rotate</span>
</button>
</div> </div>
</div> </div>
</header> </header>
@ -32,9 +51,27 @@
<main class="main-content"> <main class="main-content">
<div id="loading" class="loading glass-panel"> <div id="loading" class="loading glass-panel">
<div class="spinner"></div> <div class="spinner"></div>
<p>Analyzing Cluster...</p> <p>Building 3D Pod Topology...</p>
</div> </div>
<div id="chart-container" class="chart-container"></div> <div id="chart-container" class="chart-container"></div>
<!-- 3D Navigation Hint -->
<div class="navigation-hint glass-panel">
<span><kbd>Left Drag</kbd> Rotate</span>
<span><kbd>Right Drag</kbd> Pan</span>
<span><kbd>Scroll</kbd> Zoom</span>
</div>
<!-- Legend Panel -->
<div class="legend-panel glass-panel">
<div class="legend-item"><span class="legend-dot running"></span> Running Pod</div>
<div class="legend-item"><span class="legend-dot pending"></span> Pending Pod</div>
<div class="legend-item"><span class="legend-dot failed"></span> Failed Pod</div>
<div class="legend-item"><span class="legend-dot unused"></span> Available Capacity</div>
<div class="legend-divider"></div>
<div class="legend-note">📐 <strong>Tower Height</strong> = Resource Usage</div>
</div>
</main> </main>
</div> </div>
@ -43,3 +80,4 @@
<script src="/static/app.js"></script> <script src="/static/app.js"></script>
</body> </body>
</html> </html>

View File

@ -1,2 +1,2 @@
[metadata] [metadata]
version = 0.4 version = 0.5