diff --git a/static/app.js b/static/app.js
index ac3d275..95aa07c 100644
--- a/static/app.js
+++ b/static/app.js
@@ -1,8 +1,21 @@
let currentMetric = 'cpu';
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) => {
- if (bytes === 0) return '0 B';
+ if (!bytes || bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
@@ -10,193 +23,534 @@ const formatBytes = (bytes) => {
};
const formatCpu = (millicores) => {
- if (millicores === 0) return '0 m';
+ if (!millicores || millicores === 0) return '0 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-mem').classList.remove('active');
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-cpu').classList.remove('active');
currentMetric = 'memory';
- if (clusterData) renderChart(clusterData);
+ updatePodHeights();
});
-async function fetchData() {
- try {
- 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');
- renderChart(clusterData);
- } catch (error) {
- console.error(error);
- document.getElementById('loading').innerHTML = `
Error loading data: ${error.message}
`;
- }
-}
+document.getElementById('btn-reset-cam').addEventListener('click', () => {
+ resetCameraView();
+});
-function renderChart(data) {
+document.getElementById('btn-auto-rotate').addEventListener('click', () => {
+ autoRotateEnabled = !autoRotateEnabled;
+ controls.autoRotate = autoRotateEnabled;
+ const btn = document.getElementById('btn-auto-rotate');
+ 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';
+ }
+});
+
+// Initialize 3D Engine
+function init3D() {
const container = document.getElementById('chart-container');
const width = container.clientWidth;
const height = container.clientHeight;
-
- // Clear previous
- d3.select('#chart-container').selectAll('*').remove();
-
- const svg = d3.select('#chart-container')
- .append('svg')
- .attr('width', width)
- .attr('height', height)
- .style('font-family', 'Inter, sans-serif');
- // Create hierarchy
+ // Scene
+ scene = new THREE.Scene();
+ scene.background = new THREE.Color(0x090d16);
+ scene.fog = new THREE.FogExp2(0x090d16, 0.0012);
+
+ // Camera
+ camera = new THREE.PerspectiveCamera(45, width / height, 1, 2000);
+ camera.position.set(0, 140, 210);
+ camera.lookAt(0, 0, 0);
+
+ // 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 = `Error loading cluster data: ${error.message}
`;
+ }
+}
+
+// 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
+
const root = d3.hierarchy(data)
.sum(d => {
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);
- // Create Treemap
d3.treemap()
- .size([width, height])
- .paddingTop(28)
- .paddingRight(8)
- .paddingInner(4)
- .paddingBottom(8)
- .paddingLeft(8)
- .round(true)(root);
+ .size([BOUNDS_SIZE, BOUNDS_SIZE])
+ .paddingTop(36)
+ .paddingRight(10)
+ .paddingInner(6)
+ .paddingBottom(10)
+ .paddingLeft(10)(root);
- // Get nodes and pods
- const nodes = root.descendants().filter(d => d.depth === 1); // Nodes
- const pods = root.leaves(); // Pods and Unused
+ // 3. Render Nodes and Pods
+ const nodes = root.descendants().filter(d => d.depth === 1);
+ const pods = root.leaves();
- // Color scale for pods
- const getColor = (d) => {
- if (d.data.type === 'unused') return 'var(--color-unused)';
- if (d.data.status === 'Running') return 'var(--color-running)';
- if (d.data.status === 'Pending') return 'var(--color-pending)';
- return 'var(--color-failed)';
- };
+ // Render Node Platforms
+ nodes.forEach(n => {
+ const nw = Math.max(10, n.x1 - n.x0);
+ const nh = Math.max(10, n.y1 - n.y0);
+ const centerX = (n.x0 + n.x1) / 2 - BOUNDS_SIZE / 2;
+ const centerZ = (n.y0 + n.y1) / 2 - BOUNDS_SIZE / 2;
- // Draw Node Groups
- const nodeGroups = svg.selectAll('.node-group')
- .data(nodes)
- .enter()
- .append('g')
- .attr('class', 'node-group')
- .attr('transform', d => `translate(${d.x0},${d.y0})`);
+ const platformGeo = new THREE.BoxGeometry(nw, 1.5, nh);
+ const platformMat = new THREE.MeshStandardMaterial({
+ color: 0x1e293b,
+ roughness: 0.5,
+ metalness: 0.3,
+ transparent: true,
+ opacity: 0.95
+ });
+ const platformMesh = new THREE.Mesh(platformGeo, platformMat);
+ platformMesh.position.set(centerX, 0.75, centerZ);
+ platformMesh.receiveShadow = true;
- // Node Background
- nodeGroups.append('rect')
- .attr('class', 'node-rect')
- .attr('width', d => Math.max(0, d.x1 - d.x0))
- .attr('height', d => Math.max(0, d.y1 - d.y0));
+ // Platform Border Frame
+ const edges = new THREE.EdgesGeometry(platformGeo);
+ const lineMat = new THREE.LineBasicMaterial({ color: 0x3b82f6, linewidth: 2 });
+ const wireframe = new THREE.LineSegments(edges, lineMat);
+ platformMesh.add(wireframe);
- // Node Label
- nodeGroups.append('text')
- .attr('class', 'node-label')
- .attr('x', 8)
- .attr('y', 20)
- .text(d => d.data.name);
+ scene.add(platformMesh);
+ nodeMeshes.push(platformMesh);
- // Draw Pods
- const podGroups = svg.selectAll('.pod-group')
- .data(pods)
- .enter()
- .append('g')
- .attr('class', 'pod-group')
- .attr('transform', d => `translate(${d.x0},${d.y0})`);
+ // Node Label Sprite
+ const sprite = createTextSprite(n.data.name, 26, '#f8fafc');
+ sprite.position.set(centerX, 12, centerZ - nh / 2 - 4);
+ scene.add(sprite);
+ nodeMeshes.push(sprite);
+ });
- // Pod Rect
- podGroups.append('rect')
- .attr('class', 'pod-rect')
- .attr('width', d => Math.max(0, d.x1 - d.x0))
- .attr('height', d => Math.max(0, d.y1 - d.y0))
- .attr('fill', d => getColor(d))
- .on('mousemove', function(event, d) {
- const tooltip = document.getElementById('tooltip');
- tooltip.classList.remove('hidden');
-
- // Boundary detection for tooltip
- const tWidth = 240;
- 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';
-
- let val = currentMetric === 'cpu' ? formatCpu(d.data[`${currentMetric}_raw`]) : formatBytes(d.data[`${currentMetric}_raw`]);
-
- if (d.data.type === 'unused') {
- tooltip.innerHTML = `
- Available Capacity
-
- ${currentMetric.toUpperCase()}:
- ${val}
-
- `;
- } else {
- tooltip.innerHTML = `
- ${d.data.name}
-
- Namespace:
- ${d.data.namespace}
-
-
- Status:
- ${d.data.status}
-
-
- ${currentMetric.toUpperCase()}:
- ${val}
-
- `;
- }
- })
- .on('mouseout', function() {
- document.getElementById('tooltip').classList.add('hidden');
+ // Render Pod Towers
+ pods.forEach(p => {
+ const pw = Math.max(2, (p.x1 - p.x0) * 0.88);
+ const pd = Math.max(2, (p.y1 - p.y0) * 0.88);
+ const centerX = (p.x0 + p.x1) / 2 - BOUNDS_SIZE / 2;
+ const centerZ = (p.y0 + p.y1) / 2 - BOUNDS_SIZE / 2;
+
+ // Calculate height
+ const targetHeight = getTargetHeight(p.data);
+
+ // Box geometry: 1 unit height base, scaled in animation loop
+ const podGeo = new THREE.BoxGeometry(pw, 1, pd);
+
+ // Color & Material based on Pod Status
+ let color = 0x10b981; // Running emerald
+ let emissive = 0x059669;
+ let opacity = 0.9;
+ let transparent = false;
+
+ 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;
+ }
+
+ const podMat = new THREE.MeshStandardMaterial({
+ color: color,
+ emissive: emissive,
+ emissiveIntensity: p.data.type === 'unused' ? 0.05 : 0.25,
+ roughness: 0.3,
+ metalness: 0.4,
+ transparent: transparent,
+ opacity: opacity
});
- // Pod Label (only if there is space)
- podGroups.append('text')
- .attr('class', 'pod-label')
- .attr('x', 4)
- .attr('y', 14)
- .text(d => {
- if (d.data.type === 'unused') return '';
- const width = d.x1 - d.x0;
- const height = d.y1 - d.y0;
- if (width > 60 && height > 20) {
- let txt = d.data.name;
- if (txt.length * 5 > width) {
- return txt.substring(0, Math.floor(width/5) - 2) + '..';
- }
- return txt;
- }
- return '';
- });
+ const podMesh = new THREE.Mesh(podGeo, podMat);
+ podMesh.castShadow = true;
+ podMesh.receiveShadow = true;
+
+ // Initial positions & properties
+ podMesh.position.set(centerX, 1.5 + 0.1 / 2, centerZ);
+ podMesh.scale.set(1, 0.1, 1);
+
+ podMesh.userData = {
+ ...p.data,
+ nodeName: p.parent ? p.parent.data.name : 'Unknown Node'
+ };
+ podMesh.targetHeight = targetHeight;
+ podMesh.currentHeight = 0.1;
+ podMesh.baseWidth = pw;
+ podMesh.baseDepth = pd;
+ 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
-window.addEventListener('resize', () => {
- if (clusterData) {
- renderChart(clusterData);
- }
-});
+// Calculate target 3D tower height based on current metric
+function getTargetHeight(data) {
+ if (data.type === 'unused') return 0.5; // low plate for available capacity
-// 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 = `
+
+ Available Capacity
+
+
+
+ Node:
+ ${data.nodeName}
+
+
+ Unused CPU:
+ ${valCpu}
+
+
+ Unused Memory:
+ ${valMem}
+
+ `;
+ } else {
+ const statusColor = data.status === 'Running' ? '#10b981' : (data.status === 'Pending' ? '#f59e0b' : '#ef4444');
+ tooltip.innerHTML = `
+
+ ${data.name}
+
+
+ Namespace:
+ ${data.namespace}
+
+
+ Node:
+ ${data.nodeName}
+
+
+ Status:
+ ${data.status}
+
+
+ CPU Usage:
+ ${valCpu}
+
+
+ Memory Usage:
+ ${valMem}
+
+ `;
+ }
+
+ } 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();
-// Poll every 30 seconds
setInterval(fetchData, 30000);
+
diff --git a/static/style.css b/static/style.css
index a47649d..32fa1a1 100644
--- a/static/style.css
+++ b/static/style.css
@@ -4,14 +4,14 @@
--text-muted: #94a3b8;
--accent: #3b82f6;
--accent-hover: #2563eb;
- --glass-bg: rgba(30, 41, 59, 0.7);
- --glass-border: rgba(255, 255, 255, 0.1);
+ --glass-bg: rgba(30, 41, 59, 0.75);
+ --glass-border: rgba(255, 255, 255, 0.12);
/* Chart Colors */
- --color-running: rgba(16, 185, 129, 0.8);
- --color-pending: rgba(245, 158, 11, 0.8);
- --color-failed: rgba(239, 68, 68, 0.8);
- --color-unused: rgba(148, 163, 184, 0.15);
+ --color-running: #10b981;
+ --color-pending: #f59e0b;
+ --color-failed: #ef4444;
+ --color-unused: rgba(148, 163, 184, 0.2);
--color-node: rgba(30, 41, 59, 0.9);
}
@@ -36,14 +36,14 @@ body {
display: flex;
flex-direction: column;
height: 100vh;
- padding: 1.5rem;
- gap: 1.5rem;
+ padding: 1.25rem;
+ gap: 1rem;
}
.glass-panel {
background: var(--glass-bg);
- backdrop-filter: blur(12px);
- -webkit-backdrop-filter: blur(12px);
+ backdrop-filter: blur(16px);
+ -webkit-backdrop-filter: blur(16px);
border: 1px solid var(--glass-border);
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);
@@ -53,7 +53,8 @@ body {
display: flex;
justify-content: space-between;
align-items: center;
- padding: 1rem 2rem;
+ padding: 0.85rem 1.75rem;
+ z-index: 20;
}
.logo {
@@ -68,6 +69,26 @@ body {
font-weight: 700;
color: var(--text-main);
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 {
@@ -93,7 +114,39 @@ body {
.toggle-btn.active {
background: var(--accent);
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 {
@@ -114,7 +167,7 @@ body {
justify-content: center;
gap: 1rem;
padding: 3rem;
- z-index: 10;
+ z-index: 30;
transition: opacity 0.3s ease;
}
@@ -139,46 +192,76 @@ body {
.chart-container {
width: 100%;
height: 100%;
+ cursor: grab;
}
-.node-group {
- transition: all 0.3s ease;
+.chart-container:active {
+ cursor: grabbing;
}
-.node-rect {
- fill: var(--color-node);
- stroke: var(--glass-border);
- stroke-width: 1px;
- rx: 8;
- ry: 8;
+.navigation-hint {
+ position: absolute;
+ top: 1rem;
+ left: 1rem;
+ display: flex;
+ gap: 1rem;
+ padding: 0.6rem 1rem;
+ font-size: 0.75rem;
+ color: var(--text-muted);
+ pointer-events: none;
+ z-index: 10;
}
-.pod-rect {
- stroke: rgba(0,0,0,0.3);
- stroke-width: 1px;
- rx: 4;
- ry: 4;
- transition: fill 0.3s ease, opacity 0.3s ease, stroke 0.3s ease;
- cursor: pointer;
-}
-
-.pod-rect:hover {
- opacity: 0.9;
- stroke: white;
-}
-
-.node-label {
- fill: var(--text-main);
- font-size: 0.875rem;
+.navigation-hint kbd {
+ background: rgba(255, 255, 255, 0.1);
+ border: 1px solid rgba(255, 255, 255, 0.2);
+ border-radius: 0.25rem;
+ padding: 0.1rem 0.35rem;
+ font-family: inherit;
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;
}
-.pod-label {
- fill: white;
- font-size: 0.7rem;
- pointer-events: none;
- text-shadow: 0 1px 2px rgba(0,0,0,0.8);
+.legend-item {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ 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 {
@@ -186,9 +269,10 @@ body {
padding: 1rem;
pointer-events: none;
z-index: 100;
- font-size: 0.875rem;
+ font-size: 0.85rem;
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 {
@@ -201,12 +285,16 @@ body {
border-bottom: 1px solid var(--glass-border);
padding-bottom: 0.5rem;
word-break: break-all;
+ color: var(--text-main);
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
}
.tooltip-row {
display: flex;
justify-content: space-between;
- margin-bottom: 0.25rem;
+ margin-bottom: 0.3rem;
gap: 1rem;
}
@@ -216,4 +304,6 @@ body {
.tooltip-value {
font-weight: 600;
+ color: var(--text-main);
}
+
diff --git a/templates/index.html b/templates/index.html
index 2f43916..c65772d 100644
--- a/templates/index.html
+++ b/templates/index.html
@@ -3,12 +3,16 @@
- Kubernetes Cluster Visualization
+ Kubernetes Cluster Visualization - K8s Pulse 3D
+
+
+
+
@@ -19,12 +23,27 @@
-
K8s Pulse
+
K8s Pulse 3D
-
-
+
+
+
+
@@ -32,9 +51,27 @@
-
Analyzing Cluster...
+
Building 3D Pod Topology...
+
+
+
+
+ Left Drag Rotate
+ Right Drag Pan
+ Scroll Zoom
+
+
+
+
+
Running Pod
+
Pending Pod
+
Failed Pod
+
Available Capacity
+
+
📐 Tower Height = Resource Usage
+
@@ -43,3 +80,4 @@