diff --git a/docker-compose.yml b/docker-compose.yml index 71dfba1..bf7fca2 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,10 +4,12 @@ services: context: . dockerfile: Dockerfile container_name: k8s-pulse + user: "${UID:-1000}:${GID:-1000}" ports: - "5000:5000" environment: - - KUBECONFIG=/home/nonroot/.kube/config + - KUBECONFIG=/tmp/config + - HOME=/tmp volumes: - - ${KUBECONFIG:-~/.kube/config}:/home/nonroot/.kube/config:ro + - ${KUBECONFIG:-/home/builder/.kube/config}:/tmp/config:ro restart: unless-stopped diff --git a/static/app.js b/static/app.js index 82357b0..ab36fd2 100644 --- a/static/app.js +++ b/static/app.js @@ -78,6 +78,65 @@ }; })(); +const palettes = { + dark: { + nodeCP: 0x1e1b4b, nodeWorker: 0x1e293b, frameCP: 0xa855f7, frameWorker: 0x3b82f6, + runColor: 0x10b981, runEmissive: 0x059669, + pendColor: 0xf59e0b, pendEmissive: 0xd97706, + succColor: 0x94a3b8, succEmissive: 0x475569, + failColor: 0xef4444, failEmissive: 0xdc2626, + unusedColor: 0x64748b, unusedEmissive: 0x334155, + textColCP: '#f3e8ff', textBgCP: 'rgba(46, 16, 101, 0.92)', + textColW: '#f8fafc', textBgW: 'rgba(15, 23, 42, 0.85)', + gridMain: 0x3b82f6, gridBg: 0x1e293b + }, + neon: { + nodeCP: 0x101010, nodeWorker: 0x141414, frameCP: 0xff00ff, frameWorker: 0x00ff00, + runColor: 0x00ff00, runEmissive: 0x00aa00, + pendColor: 0xffff00, pendEmissive: 0xaaaa00, + succColor: 0x00ffff, succEmissive: 0x00aaaa, + failColor: 0xff0000, failEmissive: 0xaa0000, + unusedColor: 0x004400, unusedEmissive: 0x002200, + textColCP: '#ff00ff', textBgCP: 'rgba(20, 0, 20, 0.9)', + textColW: '#00ff00', textBgW: 'rgba(0, 20, 0, 0.9)', + gridMain: 0x00ff00, gridBg: 0x101010 + }, + light: { + nodeCP: 0xe2e8f0, nodeWorker: 0xf1f5f9, frameCP: 0x9333ea, frameWorker: 0x2563eb, + runColor: 0x059669, runEmissive: 0x047857, + pendColor: 0xd97706, pendEmissive: 0xb45309, + succColor: 0x64748b, succEmissive: 0x475569, + failColor: 0xdc2626, failEmissive: 0xb91c1c, + unusedColor: 0x94a3b8, unusedEmissive: 0x64748b, + textColCP: '#4c1d95', textBgCP: 'rgba(233, 213, 255, 0.9)', + textColW: '#0f172a', textBgW: 'rgba(241, 245, 249, 0.9)', + gridMain: 0x94a3b8, gridBg: 0xe2e8f0 + }, + gray: { + nodeCP: 0x374151, nodeWorker: 0x4b5563, frameCP: 0x9ca3af, frameWorker: 0xd1d5db, + runColor: 0x6ee7b7, runEmissive: 0x34d399, + pendColor: 0xfcd34d, pendEmissive: 0xfbbf24, + succColor: 0xe5e7eb, succEmissive: 0xd1d5db, + failColor: 0xfca5a5, failEmissive: 0xf87171, + unusedColor: 0x9ca3af, unusedEmissive: 0x6b7280, + textColCP: '#f3f4f6', textBgCP: 'rgba(55, 65, 81, 0.9)', + textColW: '#f9fafb', textBgW: 'rgba(75, 85, 99, 0.9)', + gridMain: 0x9ca3af, gridBg: 0x374151 + }, + bw: { + nodeCP: 0xeeeeee, nodeWorker: 0xf0f0f0, frameCP: 0x000000, frameWorker: 0x555555, + runColor: 0x666666, runEmissive: 0x444444, + pendColor: 0x999999, pendEmissive: 0x777777, + succColor: 0xcccccc, succEmissive: 0xaaaaaa, + failColor: 0x000000, failEmissive: 0x000000, + unusedColor: 0xdddddd, unusedEmissive: 0xbbbbbb, + textColCP: '#000000', textBgCP: 'rgba(255, 255, 255, 0.9)', + textColW: '#333333', textBgW: 'rgba(255, 255, 255, 0.9)', + gridMain: 0x000000, gridBg: 0xffffff + } +}; + +let currentTheme = 'dark'; let currentMetric = 'cpu'; let clusterData = null; @@ -140,6 +199,45 @@ document.getElementById('btn-auto-rotate').addEventListener('click', () => { } }); +document.getElementById('theme-select').addEventListener('change', (e) => { + const theme = e.target.value; + currentTheme = theme; + if (theme === 'dark') { + document.documentElement.removeAttribute('data-theme'); + if (typeof scene !== 'undefined' && scene) { + scene.background = new THREE.Color(0x090d16); + scene.fog.color = new THREE.Color(0x090d16); + } + } else { + document.documentElement.setAttribute('data-theme', theme); + if (typeof scene !== 'undefined' && scene) { + let bgColor = 0x090d16; + if (theme === 'neon') bgColor = 0x000000; + if (theme === 'light') bgColor = 0xf1f5f9; + if (theme === 'gray') bgColor = 0x4b5563; + if (theme === 'bw') bgColor = 0xffffff; + scene.background = new THREE.Color(bgColor); + scene.fog.color = new THREE.Color(bgColor); + } + } + + if (typeof scene !== 'undefined' && scene) { + const grid = scene.getObjectByName('gridHelper'); + if (grid) { + scene.remove(grid); + const p = palettes[currentTheme] || palettes.dark; + const newGrid = new THREE.GridHelper(600, 60, p.gridMain, p.gridBg); + newGrid.position.y = -0.5; + newGrid.name = 'gridHelper'; + scene.add(newGrid); + } + } + + if (clusterData) { + build3DCluster(clusterData); + } +}); + // Initialize 3D Engine function init3D() { const container = document.getElementById('chart-container'); @@ -187,8 +285,10 @@ function init3D() { scene.add(purplePointLight); // Grid Floor - const gridHelper = new THREE.GridHelper(600, 60, 0x3b82f6, 0x1e293b); + const p = palettes[currentTheme] || palettes.dark; + const gridHelper = new THREE.GridHelper(600, 60, p.gridMain, p.gridBg); gridHelper.position.y = -0.5; + gridHelper.name = 'gridHelper'; scene.add(gridHelper); // Orbit Controls @@ -387,9 +487,10 @@ function build3DCluster(data) { (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 p = palettes[currentTheme] || palettes.dark; const platformGeo = new THREE.BoxGeometry(nw, 1.5, nh); const platformMat = new THREE.MeshStandardMaterial({ - color: isControlPlane ? 0x1e1b4b : 0x1e293b, + color: isControlPlane ? p.nodeCP : p.nodeWorker, roughness: 0.5, metalness: 0.3, transparent: true, @@ -412,7 +513,7 @@ function build3DCluster(data) { // Platform Border Frame const edges = new THREE.EdgesGeometry(platformGeo); - const frameColor = isControlPlane ? 0xa855f7 : 0x3b82f6; + const frameColor = isControlPlane ? p.frameCP : p.frameWorker; const lineMat = new THREE.LineBasicMaterial({ color: frameColor, linewidth: isControlPlane ? 3 : 2 }); const wireframe = new THREE.LineSegments(edges, lineMat); platformMesh.add(wireframe); @@ -422,8 +523,8 @@ function build3DCluster(data) { // 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 textColor = isControlPlane ? p.textColCP : p.textColW; + const bgColor = isControlPlane ? p.textBgCP : p.textBgW; const sprite = createTextSprite(n.data.name, fontSize, textColor, bgColor, true, isControlPlane); sprite.position.set(centerX, isControlPlane ? 16 : 14, centerZ - nh / 2 - 2); @@ -450,25 +551,26 @@ function build3DCluster(data) { const podGeo = new THREE.BoxGeometry(pw, 1, pd); // Color & Material based on Pod Status - let color = 0x10b981; // Running emerald - let emissive = 0x059669; + const pTheme = palettes[currentTheme] || palettes.dark; + let color = pTheme.runColor; + let emissive = pTheme.runEmissive; let opacity = 0.9; let transparent = false; if (p.data.type === 'unused') { - color = 0x64748b; - emissive = 0x334155; + color = pTheme.unusedColor; + emissive = pTheme.unusedEmissive; opacity = 0.25; transparent = true; } else if (p.data.status === 'Pending') { - color = 0xf59e0b; - emissive = 0xd97706; + color = pTheme.pendColor; + emissive = pTheme.pendEmissive; } else if (p.data.status === 'Succeeded') { - color = 0x94a3b8; - emissive = 0x475569; + color = pTheme.succColor; + emissive = pTheme.succEmissive; } else if (p.data.status !== 'Running') { - color = 0xef4444; - emissive = 0xdc2626; + color = pTheme.failColor; + emissive = pTheme.failEmissive; } const podMat = new THREE.MeshStandardMaterial({ diff --git a/static/style.css b/static/style.css index 7988726..37d5b10 100644 --- a/static/style.css +++ b/static/style.css @@ -14,6 +14,93 @@ --color-failed: #ef4444; --color-unused: rgba(148, 163, 184, 0.2); --color-node: rgba(30, 41, 59, 0.9); + + --gradient-1: rgba(59, 130, 246, 0.15); + --gradient-2: rgba(139, 92, 246, 0.15); +} + +/* Neon Theme */ +:root[data-theme="neon"] { + --bg-color: #000000; + --text-main: #00ff00; + --text-muted: #008800; + --accent: #ff00ff; + --accent-hover: #ff66ff; + --glass-bg: rgba(0, 20, 0, 0.8); + --glass-border: rgba(0, 255, 0, 0.5); + + --color-running: #00ff00; + --color-pending: #ffff00; + --color-succeeded: #00ffff; + --color-failed: #ff0000; + --color-unused: rgba(0, 255, 0, 0.2); + --color-node: rgba(20, 20, 20, 0.9); + + --gradient-1: rgba(0, 255, 0, 0.2); + --gradient-2: rgba(255, 0, 255, 0.2); +} + +/* Light Theme */ +:root[data-theme="light"] { + --bg-color: #f1f5f9; + --text-main: #0f172a; + --text-muted: #475569; + --accent: #2563eb; + --accent-hover: #1d4ed8; + --glass-bg: rgba(255, 255, 255, 0.75); + --glass-border: rgba(0, 0, 0, 0.12); + + --color-running: #059669; + --color-pending: #d97706; + --color-succeeded: #64748b; + --color-failed: #dc2626; + --color-unused: rgba(100, 116, 139, 0.2); + --color-node: rgba(241, 245, 249, 0.9); + + --gradient-1: rgba(37, 99, 235, 0.1); + --gradient-2: rgba(147, 51, 234, 0.1); +} + +/* Gray Theme */ +:root[data-theme="gray"] { + --bg-color: #4b5563; + --text-main: #f9fafb; + --text-muted: #d1d5db; + --accent: #9ca3af; + --accent-hover: #d1d5db; + --glass-bg: rgba(55, 65, 81, 0.75); + --glass-border: rgba(255, 255, 255, 0.2); + + --color-running: #6ee7b7; + --color-pending: #fcd34d; + --color-succeeded: #e5e7eb; + --color-failed: #fca5a5; + --color-unused: rgba(209, 213, 219, 0.2); + --color-node: rgba(55, 65, 81, 0.9); + + --gradient-1: rgba(156, 163, 175, 0.2); + --gradient-2: rgba(209, 213, 219, 0.2); +} + +/* Black and White Theme */ +:root[data-theme="bw"] { + --bg-color: #ffffff; + --text-main: #000000; + --text-muted: #333333; + --accent: #000000; + --accent-hover: #555555; + --glass-bg: rgba(255, 255, 255, 0.9); + --glass-border: rgba(0, 0, 0, 0.3); + + --color-running: #666666; + --color-pending: #999999; + --color-succeeded: #cccccc; + --color-failed: #000000; + --color-unused: rgba(0, 0, 0, 0.1); + --color-node: rgba(240, 240, 240, 0.9); + + --gradient-1: rgba(0, 0, 0, 0.05); + --gradient-2: rgba(0, 0, 0, 0.05); } * { @@ -29,8 +116,8 @@ body { min-height: 100vh; overflow: hidden; background-image: - radial-gradient(at 0% 0%, rgba(59, 130, 246, 0.15) 0px, transparent 50%), - radial-gradient(at 100% 100%, rgba(139, 92, 246, 0.15) 0px, transparent 50%); + radial-gradient(at 0% 0%, var(--gradient-1) 0px, transparent 50%), + radial-gradient(at 100% 100%, var(--gradient-2) 0px, transparent 50%); } .app-container { @@ -124,6 +211,37 @@ body { box-shadow: 0 2px 6px rgba(59, 130, 246, 0.4); } +.theme-selector { + display: flex; + align-items: center; +} + +.theme-select { + background: rgba(15, 23, 42, 0.6); + color: var(--text-main); + border: 1px solid var(--glass-border); + padding: 0.5rem 1rem; + font-size: 0.875rem; + font-weight: 600; + border-radius: 0.5rem; + cursor: pointer; + outline: none; + appearance: none; + -webkit-appearance: none; + -moz-appearance: none; + transition: all 0.2s ease; +} + +.theme-select:hover { + background: rgba(30, 41, 59, 0.9); + border-color: rgba(255, 255, 255, 0.25); +} + +.theme-select option { + background: var(--bg-color); + color: var(--text-main); +} + .action-buttons { display: flex; gap: 0.5rem; diff --git a/templates/index.html b/templates/index.html index f740bd3..fe04119 100644 --- a/templates/index.html +++ b/templates/index.html @@ -26,6 +26,15 @@ +
+ +