From 2f858481b7e6910161d1e0f738ddafd00fe7d696 Mon Sep 17 00:00:00 2001 From: Aleksei Fedorov Date: Mon, 11 May 2026 18:59:10 +0000 Subject: [PATCH] kernel DNA plus network subsystem --- static/js/kernel-dna.js | 467 +++++++++++++++++--- static/js/network-stack.js | 867 +++++++++++++++++++++++++++++++++++-- static/js/ui-chrome.js | 44 -- 3 files changed, 1232 insertions(+), 146 deletions(-) diff --git a/static/js/kernel-dna.js b/static/js/kernel-dna.js index 847990d..a34d05f 100755 --- a/static/js/kernel-dna.js +++ b/static/js/kernel-dna.js @@ -24,8 +24,12 @@ class KernelDNAVisualization { this.mutationAnimations = []; // Track mutation animations to stop them this.isAnimating = false; // Prevent multiple animation loops this.timelineMode = false; // Timeline mode: growing helix over time + this.timelineBranchMode = false; // Multi-process branch timeline mode this.timelineData = []; // Process timeline events + this.timelineBranches = []; // Multi-branch process timelines this.selectedPid = null; // Selected process PID for timeline + this.timelineWindowS = 30; // Timeline zoom window in seconds + this.timelineWindowOptions = [5, 30, 120]; this.timeStart = null; // Start time for timeline this.maxTimelineHeight = 30; // Maximum height for timeline helix this.raycaster = null; @@ -39,6 +43,8 @@ class KernelDNAVisualization { this.exitButton = null; // Store exit button reference this._loadingOverlay = null; this._uxStylesInjected = false; + this.pinnedTimelineLabels = []; + this.pinnedLabelLayer = null; // Color palette - New design system this.colors = { @@ -702,15 +708,30 @@ class KernelDNAVisualization { async renderTimeline() { if (!this.selectedPid) { - this._showLoadingOverlay('Loading processes'); - this.clear(); + this._showLoadingOverlay('Loading process branches'); try { - await this.addTimelineLabels(null); + const response = await fetch(`/api/proc-timeline-branches?limit=6&events=10&window_s=${this.timelineWindowS}`); + const data = await response.json(); + if (data.error) { + console.error('❌ Branch timeline error:', data.error); + this.clear(); + await this.addTimelineLabels(null); + return; + } + this.timelineBranches = data.branches || []; + this.clear(); + this.renderTimelineBranches(this.timelineBranches); + await this.addTimelineBranchLabels(data); + if (!this.isAnimating) { + this.isAnimating = true; + this.animate(); + } } finally { await this._hideLoadingOverlay(); } return; } + this.timelineBranchMode = false; // Save current rotation state to prevent jitter during update let savedLeftRotation = 0; @@ -725,7 +746,7 @@ class KernelDNAVisualization { this._showLoadingOverlay('Loading timeline'); // Load timeline data try { - const response = await fetch(`/api/proc-timeline?pid=${this.selectedPid}`); + const response = await fetch(`/api/proc-timeline?pid=${this.selectedPid}&window_s=${this.timelineWindowS}`); const data = await response.json(); if (data.error) { @@ -734,16 +755,10 @@ class KernelDNAVisualization { } this.timelineData = data.timeline || []; + this.timelineWindowS = Number(data.window_s || this.timelineWindowS || 30); - // Set start time from first event - if (this.timelineData.length > 0 && !this.timeStart) { - this.timeStart = new Date(this.timelineData[0].timestamp).getTime(); - } - - // Calculate current timeline height based on time elapsed - const now = Date.now(); - const elapsed = this.timeStart ? (now - this.timeStart) / 1000 : 0; // seconds - this.currentTimelineHeight = Math.min(elapsed * 0.5, this.maxTimelineHeight); // Grow 0.5 units per second + // In focused timeline mode we render full selected window. + this.currentTimelineHeight = this.maxTimelineHeight; // Clear previous visualization this.clear(); @@ -762,27 +777,18 @@ class KernelDNAVisualization { this.scene.add(this.helixRight); // Map timeline events to nucleotides - const eventToNucleotide = { - 'exec': { code: 'A', type: 'syscall' }, - 'fork': { code: 'A', type: 'syscall' }, - 'mmap': { code: 'A', type: 'syscall' }, - 'read': { code: 'A', type: 'syscall' }, - 'write': { code: 'A', type: 'syscall' }, - 'connect': { code: 'A', type: 'syscall' }, - 'accept': { code: 'A', type: 'syscall' }, - 'exit': { code: 'C', type: 'context_switch' } - }; - // Position events on helix based on timestamp this.timelineEvents = []; this.timelineData.forEach((event, i) => { - const eventTime = new Date(event.timestamp).getTime(); - const timeProgress = this.timeStart ? (eventTime - this.timeStart) / (now - this.timeStart) : i / this.timelineData.length; - const t = Math.min(timeProgress, 1.0); // Clamp to 0-1 + const rel = Number(event.relative_s); + let t = Number.isFinite(rel) + ? (rel / Math.max(1, Number(this.timelineWindowS))) + : (i / Math.max(1, this.timelineData.length - 1)); + t = Math.max(0, Math.min(1, t)); // Only show events that have occurred (within current timeline height) if (t <= this.currentTimelineHeight / this.maxTimelineHeight) { - const nucleotideData = eventToNucleotide[event.type] || { code: 'A', type: 'syscall' }; + const nucleotideData = this.getTimelineNucleotideForEvent(event.type); // Position on left helix (userspace) const leftPos = leftHelix.curve.getPoint(t); @@ -799,6 +805,7 @@ class KernelDNAVisualization { this.helixLeft.add(leftNuc); this.nucleotides.push(leftNuc); this.timelineEvents.push(leftNuc); + this.addPinnedTimelineLabel(leftNuc); } // Position on right helix (kernel space) @@ -846,6 +853,13 @@ class KernelDNAVisualization { mapEventToSubsystem(eventType) { const subsystemMap = { + 'syscall': 'sched', + 'context switch': 'sched', + 'interrupt': 'kernel', + 'scheduler tick': 'sched', + 'i/o': 'fs', + 'network packet': 'net', + 'lock/unlock': 'kernel', 'exec': 'sched', 'fork': 'sched', 'mmap': 'mm', @@ -858,6 +872,177 @@ class KernelDNAVisualization { return subsystemMap[eventType] || 'kernel'; } + getTimelineNucleotideForEvent(eventType) { + const eventToNucleotide = { + 'syscall': { code: 'A', type: 'syscall' }, + 'context switch': { code: 'C', type: 'context_switch' }, + 'interrupt': { code: 'T', type: 'interrupt' }, + 'scheduler tick': { code: 'C', type: 'context_switch' }, + 'i/o': { code: 'G', type: 'lock' }, + 'network packet': { code: 'T', type: 'interrupt' }, + 'lock/unlock': { code: 'G', type: 'lock' }, + // Backward compatibility with older timeline event naming. + 'exec': { code: 'A', type: 'syscall' }, + 'fork': { code: 'A', type: 'syscall' }, + 'mmap': { code: 'A', type: 'syscall' }, + 'read': { code: 'A', type: 'syscall' }, + 'write': { code: 'A', type: 'syscall' }, + 'connect': { code: 'A', type: 'syscall' }, + 'accept': { code: 'A', type: 'syscall' }, + 'exit': { code: 'C', type: 'context_switch' } + }; + return eventToNucleotide[eventType] || { code: 'A', type: 'syscall' }; + } + + buildEventTooltipHtml(userData) { + const subsystem = userData?.subsystem || 'kernel'; + const eventType = userData?.event?.type || userData?.type || 'event'; + const processLabel = userData?.process_name + ? `${userData.process_name}${userData.pid ? ` (PID: ${userData.pid})` : ''}` + : ''; + return ` +
+ ${String(eventType).toUpperCase()} +
+
${userData?.event?.name || userData?.name || 'Event'}
+ ${processLabel ? `
Process: ${processLabel}
` : ''} +
Subsystem: ${subsystem}
+
+ Time: ${userData?.timestamp ? new Date(userData.timestamp).toLocaleTimeString() : 'n/a'} +
+ `; + } + + ensurePinnedLabelLayer() { + if (this.pinnedLabelLayer && this.pinnedLabelLayer.parentNode) return this.pinnedLabelLayer; + const layer = document.createElement('div'); + layer.className = 'dna-pinned-label-layer'; + layer.style.cssText = ` + position: absolute; + inset: 0; + pointer-events: none; + z-index: 1001; + `; + this.container.appendChild(layer); + this.pinnedLabelLayer = layer; + return layer; + } + + clearPinnedTimelineLabels() { + this.pinnedTimelineLabels.forEach((item) => { + if (item.node && item.node.parentNode) { + item.node.parentNode.removeChild(item.node); + } + }); + this.pinnedTimelineLabels = []; + } + + addPinnedTimelineLabel(targetObject) { + if (!targetObject || !targetObject.userData || !targetObject.userData.event) return; + const layer = this.ensurePinnedLabelLayer(); + const node = document.createElement('div'); + node.style.cssText = ` + position: absolute; + background: #13171B; + border: 1px solid #8A8F95; + color: #D0D3D6; + font-family: 'Share Tech Mono', monospace; + font-size: 10px; + padding: 6px 8px; + border-radius: 4px; + white-space: nowrap; + transform: translate(-9999px, -9999px); + opacity: 0.92; + `; + window.setSafeHtml(node, this.buildEventTooltipHtml(targetObject.userData)); + layer.appendChild(node); + this.pinnedTimelineLabels.push({ object: targetObject, node }); + } + + updatePinnedTimelineLabels() { + if (!this.isActive || !this.camera || !this.renderer || !this.pinnedTimelineLabels.length) return; + const rect = this.renderer.domElement.getBoundingClientRect(); + this.pinnedTimelineLabels.forEach((entry) => { + const obj = entry.object; + const node = entry.node; + if (!obj || !node) return; + const worldPos = new THREE.Vector3(); + obj.getWorldPosition(worldPos); + const screenPos = worldPos.clone().project(this.camera); + if (screenPos.z < -1 || screenPos.z > 1) { + node.style.display = 'none'; + return; + } + node.style.display = 'block'; + const x = (screenPos.x * 0.5 + 0.5) * rect.width; + const y = (-screenPos.y * 0.5 + 0.5) * rect.height; + node.style.transform = `translate(${Math.round(x + 12)}px, ${Math.round(y - 10)}px)`; + }); + } + + renderTimelineBranches(branches) { + if (!Array.isArray(branches) || branches.length === 0) { + return; + } + this.timelineBranchMode = true; + this.helixLeft = null; + this.helixRight = null; + + const group = new THREE.Group(); + const branchCount = branches.length; + const xStart = -7; + const xEnd = 7; + const yTop = 4; + const yBottom = -4; + const yStep = branchCount > 1 ? (yTop - yBottom) / (branchCount - 1) : 0; + + branches.forEach((branch, idx) => { + const y = yTop - (idx * yStep); + const start = new THREE.Vector3(xStart, y, 0); + const end = new THREE.Vector3(xEnd, y, 0); + const lineGeom = new THREE.BufferGeometry().setFromPoints([start, end]); + const lineMat = new THREE.LineBasicMaterial({ + color: this.colors.lineSecondary, + transparent: true, + opacity: 0.5 + }); + const branchLine = new THREE.Line(lineGeom, lineMat); + group.add(branchLine); + + const events = Array.isArray(branch.timeline) ? branch.timeline : []; + const evCount = Math.max(events.length, 1); + events.forEach((ev, evIdx) => { + const rel = Number(ev.relative_s); + let t = Number.isFinite(rel) + ? (rel / Math.max(1, Number(this.timelineWindowS))) + : (evCount === 1 ? 0.5 : (evIdx / (evCount - 1))); + t = Math.max(0, Math.min(1, t)); + const x = xStart + (xEnd - xStart) * t; + const z = ((evIdx % 2 === 0) ? 1 : -1) * 0.12; // tiny depth jitter for readability + const nuc = this.getTimelineNucleotideForEvent(ev.type); + const point = this.createNucleotide( + { + ...nuc, + name: ev.name || ev.type || 'event', + count: ev.count || ev.bytes || 0, + subsystem: this.mapEventToSubsystem(ev.type || '') + }, + new THREE.Vector3(x, y, z), + true + ); + if (!point) return; + point.userData.event = ev; + point.userData.timestamp = ev.timestamp; + point.userData.pid = branch.pid; + point.userData.process_name = branch.name; + this.nucleotides.push(point); + group.add(point); + }); + }); + + this.scene.add(group); + } + addTimelineMarkers(curve) { // Add simple time markers (beads) along the helix - neutral gray const markerGroup = new THREE.Group(); @@ -886,7 +1071,7 @@ class KernelDNAVisualization { async addTimelineLabels(data) { // Remove old labels - const oldLabels = this.container.querySelectorAll('.dna-title, .dna-timeline-subtitle, .dna-legend, .dna-dev-label, .dna-timeline-info, .dna-process-selector'); + const oldLabels = this.container.querySelectorAll('.dna-title, .dna-timeline-subtitle, .dna-legend, .dna-dev-label, .dna-timeline-info, .dna-process-selector, .dna-window-selector'); oldLabels.forEach(label => label.remove()); // Add title @@ -906,7 +1091,7 @@ class KernelDNAVisualization { this.container.appendChild(titleDiv); const sub = document.createElement('div'); sub.className = 'dna-timeline-subtitle'; - sub.textContent = 'single-process timeline'; + sub.textContent = 'process branch timeline'; sub.style.cssText = ` position: absolute; top: 72px; @@ -921,10 +1106,6 @@ class KernelDNAVisualization { this.container.appendChild(sub); const devLabel = this.appendInDevelopmentLabel(52); - // Add process selector - await this.addProcessSelector(); - const selectorEl = this.container.querySelector('.dna-process-selector'); - // Add timeline info const infoDiv = document.createElement('div'); infoDiv.className = 'dna-timeline-info'; @@ -935,9 +1116,98 @@ class KernelDNAVisualization {
Process: ${processName} ${this.selectedPid ? `(PID: ${this.selectedPid})` : ''}
Events: ${eventCount}
+
Window: ${this.timelineWindowS}s
Timeline: ${heightPercent}%
-
Time → Y axis (growing upward)
+
1 branch = selected process
+
points = kernel/runtime events
+
+
+ `); + infoDiv.style.cssText = ` + position: absolute; + top: 20px; + left: 20px; + z-index: 1001; + `; + this.container.appendChild(infoDiv); + + const legendDiv = document.createElement('div'); + legendDiv.className = 'dna-legend'; + window.setSafeHtml(legendDiv, ` +
+
A = syscall
+
C = context switch / scheduler tick
+
T = interrupt / network packet
+
G = I/O / lock-unlock
+
+ `); + legendDiv.style.cssText = ` + position: absolute; + top: 20px; + left: 20px; + z-index: 1001; + `; + this.container.appendChild(legendDiv); + + this.addTimelineWindowSelector([sub, infoDiv]); + await this.addProcessSelector(); + const selectorEl = this.container.querySelector('.dna-process-selector'); + const windowEl = this.container.querySelector('.dna-window-selector'); + + this._applyStaggeredReveal([titleDiv, sub, devLabel, selectorEl, windowEl, infoDiv, legendDiv]); + } + + async addTimelineBranchLabels(data) { + const oldLabels = this.container.querySelectorAll('.dna-title, .dna-timeline-subtitle, .dna-legend, .dna-dev-label, .dna-timeline-info, .dna-process-selector, .dna-window-selector'); + oldLabels.forEach(label => label.remove()); + + const titleDiv = document.createElement('div'); + titleDiv.className = 'dna-title'; + titleDiv.textContent = 'KERNEL DNA'; + titleDiv.style.cssText = ` + position: absolute; + top: 20px; + left: 50%; + transform: translateX(-50%); + color: #c8ccd4; + font-family: 'Share Tech Mono', monospace; + font-size: 24px; + z-index: 1001; + `; + this.container.appendChild(titleDiv); + + const sub = document.createElement('div'); + sub.className = 'dna-timeline-subtitle'; + sub.textContent = 'process branches timeline'; + sub.style.cssText = ` + position: absolute; + top: 72px; + left: 50%; + transform: translateX(-50%); + color: rgba(88, 182, 216, 0.85); + font-family: 'Share Tech Mono', monospace; + font-size: 11px; + letter-spacing: 0.4px; + z-index: 1001; + `; + this.container.appendChild(sub); + const devLabel = this.appendInDevelopmentLabel(52); + + const branchCount = Number(data?.meta?.branch_count || (this.timelineBranches || []).length || 0); + const totalEvents = (this.timelineBranches || []).reduce((acc, row) => acc + Number(row?.event_count || 0), 0); + + const infoDiv = document.createElement('div'); + infoDiv.className = 'dna-timeline-info'; + window.setSafeHtml(infoDiv, ` +
+
Branches: ${branchCount}
+
Events: ${totalEvents}
+
Window: ${this.timelineWindowS}s
+
+
1 branch = process timeline
+
X axis = relative time within window
+
click process on right to focus helix view
`); @@ -949,7 +1219,92 @@ class KernelDNAVisualization { `; this.container.appendChild(infoDiv); - this._applyStaggeredReveal([titleDiv, sub, devLabel, selectorEl, infoDiv]); + const legendDiv = document.createElement('div'); + legendDiv.className = 'dna-legend'; + window.setSafeHtml(legendDiv, ` +
+
A = syscall
+
C = context switch / scheduler tick
+
T = interrupt / network packet
+
G = I/O / lock-unlock
+
+ `); + legendDiv.style.cssText = ` + position: absolute; + top: 20px; + left: 20px; + z-index: 1001; + `; + this.container.appendChild(legendDiv); + + this.addTimelineWindowSelector([sub, infoDiv]); + await this.addProcessSelector(); + const selectorEl = this.container.querySelector('.dna-process-selector'); + const windowEl = this.container.querySelector('.dna-window-selector'); + this._applyStaggeredReveal([titleDiv, sub, devLabel, infoDiv, legendDiv, windowEl, selectorEl]); + } + + addTimelineWindowSelector(anchorNodes = []) { + const existing = this.container.querySelectorAll('.dna-window-selector'); + existing.forEach((node) => node.remove()); + + const containerRect = this.container ? this.container.getBoundingClientRect() : { top: 0 }; + let topPx = 124; + anchorNodes.forEach((node) => { + if (!node || typeof node.getBoundingClientRect !== 'function') return; + const rect = node.getBoundingClientRect(); + if (!rect || !Number.isFinite(rect.bottom)) return; + const candidate = Math.round(rect.bottom - containerRect.top + 10); + topPx = Math.max(topPx, candidate); + }); + + const panel = document.createElement('div'); + panel.className = 'dna-window-selector'; + panel.style.cssText = ` + position: absolute; + top: ${topPx}px; + left: 20px; + display: flex; + align-items: center; + gap: 6px; + z-index: 1001; + font-family: 'Share Tech Mono', monospace; + `; + + const label = document.createElement('span'); + label.textContent = 'window'; + label.style.cssText = ` + color: #9aa2aa; + font-size: 10px; + letter-spacing: 0.3px; + margin-right: 4px; + `; + panel.appendChild(label); + + this.timelineWindowOptions.forEach((sec) => { + const active = Number(sec) === Number(this.timelineWindowS); + const btn = document.createElement('button'); + btn.type = 'button'; + btn.textContent = `${sec}s`; + btn.style.cssText = ` + padding: 3px 8px; + background: ${active ? 'rgba(37, 58, 92, 0.72)' : 'rgba(12, 18, 28, 0.88)'}; + border: 1px solid ${active ? 'rgba(120, 170, 245, 0.72)' : 'rgba(150, 164, 188, 0.35)'}; + color: ${active ? '#e1eeff' : '#bcc8db'}; + border-radius: 3px; + font-family: 'Share Tech Mono', monospace; + font-size: 10px; + cursor: pointer; + `; + btn.onclick = async () => { + if (Number(this.timelineWindowS) === Number(sec)) return; + this.timelineWindowS = Number(sec); + await this.renderTimeline(); + }; + panel.appendChild(btn); + }); + + this.container.appendChild(panel); } async addProcessSelector() { @@ -1136,7 +1491,7 @@ class KernelDNAVisualization { async addLabels(data) { // Remove old labels first - const oldLabels = this.container.querySelectorAll('.dna-title, .dna-legend, .dna-dev-label, .dna-process-selector'); + const oldLabels = this.container.querySelectorAll('.dna-title, .dna-legend, .dna-dev-label, .dna-process-selector, .dna-window-selector'); oldLabels.forEach(label => label.remove()); // Add title @@ -1224,11 +1579,13 @@ class KernelDNAVisualization { // Rotate helix - smooth, frame-rate independent rotation const rotationSpeed = 0.5; // radians per second - if (this.helixLeft) { - this.helixLeft.rotation.y += rotationSpeed * deltaTime; - } - if (this.helixRight) { - this.helixRight.rotation.y -= rotationSpeed * deltaTime; + if (!this.timelineBranchMode) { + if (this.helixLeft) { + this.helixLeft.rotation.y += rotationSpeed * deltaTime; + } + if (this.helixRight) { + this.helixRight.rotation.y -= rotationSpeed * deltaTime; + } } // Animate mutations (pulsing effect) - smooth animation @@ -1244,7 +1601,10 @@ class KernelDNAVisualization { // Rotate camera around helix - smooth camera movement // In timeline mode, camera looks from side to see growth - if (this.timelineMode) { + if (this.timelineBranchMode) { + this.camera.position.set(0, 0.5, 16); + this.camera.lookAt(0, 0, 0); + } else if (this.timelineMode) { // Side view for timeline (better to see growth along Y axis) this.camera.position.set(10, 5, 0); this.camera.lookAt(0, 0, 0); @@ -1257,11 +1617,14 @@ class KernelDNAVisualization { this.camera.position.y = 5; this.camera.lookAt(0, 0, 0); } - + + this.updatePinnedTimelineLabels(); this.renderer.render(this.scene, this.camera); } clear() { + this.clearPinnedTimelineLabels(); + // Properly dispose of geometries and materials to prevent memory leaks const disposeObject = (obj) => { if (obj.geometry) { @@ -1303,7 +1666,7 @@ class KernelDNAVisualization { } // Remove labels (but keep exit button) - const labels = this.container.querySelectorAll('.dna-title, .dna-timeline-subtitle, .dna-legend, .dna-dev-label, .dna-timeline-info, .dna-process-selector'); + const labels = this.container.querySelectorAll('.dna-title, .dna-timeline-subtitle, .dna-legend, .dna-dev-label, .dna-timeline-info, .dna-process-selector, .dna-window-selector'); labels.forEach(label => label.remove()); } @@ -1381,6 +1744,7 @@ class KernelDNAVisualization { this.isActive = false; this.isAnimating = false; // Stop animation loop this.timelineMode = false; // Reset timeline mode + this.timelineBranchMode = false; this.selectedPid = null; this.timeStart = null; this.currentTimelineHeight = 0; @@ -1560,18 +1924,7 @@ class KernelDNAVisualization { // Check if this is a timeline event if (userData.event && userData.timestamp) { - const subsystem = userData.subsystem || 'kernel'; - const eventType = userData.event.type || 'event'; - tooltipContent = ` -
- ${eventType.toUpperCase()} -
-
${userData.event.name || 'Event'}
-
Subsystem: ${subsystem}
-
- Time: ${new Date(userData.timestamp).toLocaleTimeString()} -
- `; + tooltipContent = this.buildEventTooltipHtml(userData); } else if (userData.code && userData.type) { // Regular nucleotide - yellow accent on hover const subsystem = userData.subsystem || 'kernel'; diff --git a/static/js/network-stack.js b/static/js/network-stack.js index 67d48ed..1ce67d8 100644 --- a/static/js/network-stack.js +++ b/static/js/network-stack.js @@ -44,6 +44,101 @@ class NetworkStackVisualization { this.lineParticles = []; this.microPackets = []; this.metricChips = {}; + this.kpiNodes = {}; + this.layersPanelNode = null; + this.chipLayerNode = null; + this.viewModeButton = null; + this.puzzleModeButton = null; + this.viewDensityMode = 'detailed'; + this.puzzleDetailMode = 'overview'; + this.galaxyPanelNode = null; + this.galaxyNodes = {}; + this.galaxyExplainNode = null; + this.selectedGalaxy = 'state'; + this.galaxyStateData = null; + this.lifecyclePanelNode = null; + this.packetLifecycleStages = [ + 'NIC RX', + 'IRQ', + 'NAPI', + 'SKB', + 'XDP/TC', + 'PREROUTING', + 'CONNTRACK', + 'ROUTING', + 'TCP/UDP', + 'SOCKET', + 'PROCESS' + ]; + this.txLifecycleStages = [ + 'PROCESS', + 'SOCKET', + 'TCP/UDP', + 'ROUTING', + 'CONNTRACK', + 'POSTROUTING', + 'TC EGRESS', + 'NIC TX' + ]; + this.puzzleCoreNodes = [ + { id: 'hardware', label: 'Hardware', tags: 'NIC/RXTX/DMA/PHY' }, + { id: 'interrupt', label: 'Interrupt', tags: 'hardirq/softirq' }, + { id: 'napi', label: 'NAPI', tags: 'net_rx_action/poll' }, + { id: 'skb', label: 'sk_buff', tags: 'packet+metadata' }, + { id: 'xdp', label: 'XDP', tags: 'AF_XDP/eBPF fast path' }, + { id: 'tc', label: 'TC', tags: 'qdisc/classifier' }, + { id: 'netfilter', label: 'Netfilter', tags: 'PREROUTING..POSTROUTING' }, + { id: 'conntrack', label: 'nf_conntrack', tags: 'NEW/ESTABLISHED' }, + { id: 'routing', label: 'Routing', tags: 'FIB/policy/ECMP' }, + { id: 'ip', label: 'IP', tags: 'IPv4/IPv6/ICMP' }, + { id: 'transport', label: 'TCP/UDP', tags: 'cwnd/rtt/retrans' }, + { id: 'socket', label: 'Socket', tags: 'sock/socket lookup' }, + { id: 'process', label: 'Process', tags: 'epoll/fd/wakeup' } + ]; + this.puzzleSideNodes = [ + { id: 'l2', label: 'Bridge/VLAN/Neighbor', tags: 'bridge/FDB/ARP/NDP' }, + { id: 'tunnel', label: 'Tunnel', tags: 'VXLAN/GRE/GENEVE/WG' }, + { id: 'namespace', label: 'Namespace', tags: 'netns/veth/CNI' }, + { id: 'cgroup', label: 'cgroups net', tags: 'limits/accounting' }, + { id: 'ebpf', label: 'eBPF', tags: 'XDP/TC/socket/tracing' }, + { id: 'security', label: 'Security', tags: 'SELinux/AppArmor/seccomp' }, + { id: 'crypto', label: 'Crypto', tags: 'TLS/IPsec/WireGuard' }, + { id: 'observability', label: 'Observability', tags: 'tracepoints/perf/netlink' }, + { id: 'userspace', label: 'Userspace Interfaces', tags: 'netlink/sysctl/procfs/sysfs' } + ]; + this.lifecycleStageToNode = { + 'NIC RX': 'hardware', + 'IRQ': 'interrupt', + 'NAPI': 'napi', + 'SKB': 'skb', + 'XDP/TC': 'xdp', + 'PREROUTING': 'netfilter', + 'CONNTRACK': 'conntrack', + 'ROUTING': 'routing', + 'TCP/UDP': 'transport', + 'SOCKET': 'socket', + 'PROCESS': 'process' + }; + this.coreToSideLinks = { + hardware: ['l2', 'observability'], + interrupt: ['ebpf', 'observability'], + napi: ['ebpf', 'observability'], + skb: ['l2', 'tunnel', 'namespace'], + xdp: ['ebpf', 'security'], + tc: ['ebpf', 'cgroup', 'security'], + netfilter: ['security', 'crypto', 'userspace'], + conntrack: ['security', 'userspace', 'observability'], + routing: ['namespace', 'tunnel', 'userspace'], + ip: ['l2', 'tunnel', 'crypto'], + transport: ['cgroup', 'security', 'observability'], + socket: ['namespace', 'cgroup', 'userspace'], + process: ['userspace', 'security', 'observability'] + }; + this.sideClusters = [ + { id: 'data', label: 'Data plane', nodes: ['l2', 'tunnel', 'namespace', 'cgroup'] }, + { id: 'policy', label: 'Policy plane', nodes: ['security', 'crypto', 'ebpf'] }, + { id: 'control', label: 'Control/Insight', nodes: ['observability', 'userspace'] } + ]; this.raycaster = null; this.mouse = new THREE.Vector2(); this.mouseMoveHandler = null; @@ -254,13 +349,14 @@ class NetworkStackVisualization { const title = document.createElement('div'); title.style.cssText = ` position: absolute; - top: 20px; + top: 18px; left: 50%; transform: translateX(-50%); - color: #c8ccd4; + color: #d3d9e0; font-family: 'Share Tech Mono', monospace; - font-size: 24px; - letter-spacing: 1px; + font-size: 22px; + letter-spacing: 1.2px; + text-shadow: 0 0 10px rgba(88, 182, 216, 0.22); z-index: 1001; `; title.textContent = 'NETWORK STACK'; @@ -270,12 +366,17 @@ class NetworkStackVisualization { const flow = document.createElement('div'); flow.style.cssText = ` position: absolute; - top: 68px; + top: 58px; left: 50%; transform: translateX(-50%); - color: #9aa2aa; + color: #a7b3be; font-family: 'Share Tech Mono', monospace; font-size: 11px; + letter-spacing: 0.45px; + background: rgba(11, 16, 24, 0.62); + border: 1px solid rgba(90, 104, 120, 0.32); + border-radius: 14px; + padding: 4px 12px; z-index: 1001; `; flow.textContent = 'process -> syscall -> socket -> TCP -> IP -> NIC -> wire -> remote'; @@ -283,32 +384,91 @@ class NetworkStackVisualization { this.overlayNodes.push(flow); this.flowNode = flow; + const kpiBar = document.createElement('div'); + kpiBar.style.cssText = ` + position: absolute; + top: 90px; + left: 50%; + transform: translateX(-50%); + z-index: 1001; + display: flex; + gap: 8px; + align-items: stretch; + pointer-events: none; + `; + this.container.appendChild(kpiBar); + this.overlayNodes.push(kpiBar); + + const kpiSpec = [ + { id: 'flow', label: 'FLOW' }, + { id: 'rtt', label: 'RTT' }, + { id: 'drop', label: 'DROPS' }, + { id: 'retrans', label: 'RETRANS' } + ]; + kpiSpec.forEach((spec) => { + const card = document.createElement('div'); + card.style.cssText = ` + min-width: 116px; + background: rgba(13, 18, 28, 0.88); + border: 1px solid rgba(108, 122, 142, 0.32); + border-radius: 6px; + padding: 6px 9px 7px; + color: #c8d0da; + box-shadow: 0 3px 10px rgba(0, 0, 0, 0.22); + `; + const label = document.createElement('div'); + label.style.cssText = ` + font-family: 'Share Tech Mono', monospace; + font-size: 9px; + letter-spacing: 0.7px; + color: #8391a1; + margin-bottom: 3px; + `; + label.textContent = spec.label; + const value = document.createElement('div'); + value.style.cssText = ` + font-family: 'Share Tech Mono', monospace; + font-size: 12px; + color: #d8e0ea; + line-height: 1.2; + `; + value.textContent = '--'; + card.appendChild(label); + card.appendChild(value); + kpiBar.appendChild(card); + this.kpiNodes[spec.id] = { card, label, value }; + }); + const layersPanel = document.createElement('div'); layersPanel.style.cssText = ` position: absolute; - top: 110px; + top: 148px; left: 24px; z-index: 1001; - color: #c8ccd4; + color: #d1d8e0; font-family: 'Share Tech Mono', monospace; - font-size: 11px; + font-size: 12px; line-height: 1.5; - background: rgba(12, 18, 28, 0.82); - border: 1px solid rgba(160, 170, 190, 0.25); + background: rgba(10, 15, 24, 0.84); + border: 1px solid rgba(129, 145, 168, 0.32); border-radius: 6px; - padding: 10px 12px; + padding: 11px 13px; + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.24); + backdrop-filter: blur(1px); `; window.setSafeHtml(layersPanel, [ + 'LAYERS', 'Userspace', - '-> Socket API', - '-> TCP/UDP', - '-> IP', - '-> Netfilter', - '-> Driver', - '-> NIC' + '→ Socket API', + '→ TCP/UDP', + '→ IP', + '→ Netfilter', + '→ Driver', + '→ NIC' ].join('
')); this.container.appendChild(layersPanel); this.overlayNodes.push(layersPanel); + this.layersPanelNode = layersPanel; // Layer metrics as subtle chips aligned with layers (not a table). const chipLayer = document.createElement('div'); @@ -320,14 +480,15 @@ class NetworkStackVisualization { `; this.container.appendChild(chipLayer); this.overlayNodes.push(chipLayer); + this.chipLayerNode = chipLayer; const chipSpec = [ - { id: 'userspace', top: '24%' }, - { id: 'socket', top: '34%' }, - { id: 'tcp', top: '44%' }, - { id: 'ip', top: '54%' }, - { id: 'netfilter', top: '64%' }, - { id: 'driver', top: '74%' }, - { id: 'nic', top: '84%' } + { id: 'userspace', top: '22%' }, + { id: 'socket', top: '30%' }, + { id: 'tcp', top: '38%' }, + { id: 'ip', top: '46%' }, + { id: 'netfilter', top: '54%' }, + { id: 'driver', top: '62%' }, + { id: 'nic', top: '70%' } ]; chipSpec.forEach(spec => { const chip = document.createElement('div'); @@ -336,15 +497,18 @@ class NetworkStackVisualization { right: 2.4%; top: ${spec.top}; transform: translateY(-50%); - color: #aeb4bc; + color: #bac4cf; font-family: 'Share Tech Mono', monospace; - font-size: 10px; - letter-spacing: 0.4px; - background: rgba(20, 26, 36, 0.38); - border: 1px solid rgba(120, 130, 145, 0.18); + font-size: 11px; + letter-spacing: 0.42px; + background: rgba(16, 22, 32, 0.68); + border: 1px solid rgba(115, 128, 145, 0.32); border-radius: 4px; - padding: 2px 7px; + padding: 4px 9px; white-space: nowrap; + min-width: 196px; + text-align: left; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.16); `; chip.textContent = ''; chipLayer.appendChild(chip); @@ -367,6 +531,114 @@ class NetworkStackVisualization { this.overlayNodes.push(err); this.telemetryErrorNode = err; + const galaxyPanel = document.createElement('div'); + galaxyPanel.style.cssText = ` + position: absolute; + left: 24px; + bottom: 18px; + z-index: 1001; + width: 360px; + max-width: 30vw; + background: rgba(10, 15, 24, 0.84); + border: 1px solid rgba(129, 145, 168, 0.32); + border-radius: 6px; + padding: 10px 12px; + color: #c7d0da; + font-family: 'Share Tech Mono', monospace; + font-size: 10px; + line-height: 1.45; + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.24); + overflow: hidden; + `; + this.container.appendChild(galaxyPanel); + this.overlayNodes.push(galaxyPanel); + this.galaxyPanelNode = galaxyPanel; + const galaxyTitle = document.createElement('div'); + galaxyTitle.style.cssText = ` + font-size: 10px; + color: #7f8fa2; + letter-spacing: 0.6px; + margin-bottom: 6px; + `; + galaxyTitle.textContent = 'NETWORK GALAXIES (METABOLISM VIEW)'; + galaxyPanel.appendChild(galaxyTitle); + + const galaxyButtonRow = document.createElement('div'); + galaxyButtonRow.style.cssText = ` + display: flex; + flex-wrap: wrap; + gap: 6px; + margin-bottom: 7px; + `; + galaxyPanel.appendChild(galaxyButtonRow); + + const galaxyDefs = [ + { id: 'physical', label: 'Physical' }, + { id: 'packet', label: 'Packet' }, + { id: 'state', label: 'State' }, + { id: 'security', label: 'Security' }, + { id: 'observability', label: 'Observability' } + ]; + galaxyDefs.forEach((def) => { + const chip = document.createElement('button'); + chip.style.cssText = ` + border-radius: 4px; + border: 1px solid rgba(115, 128, 145, 0.32); + background: rgba(16, 22, 32, 0.68); + color: #bac4cf; + font-family: 'Share Tech Mono', monospace; + font-size: 10px; + padding: 3px 8px; + cursor: pointer; + transition: all 0.2s ease; + `; + chip.textContent = `${def.label}: --`; + chip.onclick = () => this.selectGalaxy(def.id); + galaxyButtonRow.appendChild(chip); + this.galaxyNodes[def.id] = chip; + }); + + const galaxyExplain = document.createElement('div'); + galaxyExplain.style.cssText = ` + color: #aeb8c3; + font-size: 10px; + line-height: 1.45; + min-height: 34px; + border-top: 1px solid rgba(115, 128, 145, 0.24); + padding-top: 6px; + `; + galaxyExplain.textContent = 'Select a galaxy to inspect subsystem health.'; + galaxyPanel.appendChild(galaxyExplain); + this.galaxyExplainNode = galaxyExplain; + + const lifecyclePanel = document.createElement('div'); + lifecyclePanel.style.cssText = ` + position: absolute; + right: 20px; + bottom: 18px; + left: auto; + transform: none; + z-index: 1001; + width: 760px; + max-width: calc(100vw - 430px); + min-width: 520px; + max-height: 36vh; + background: rgba(10, 15, 24, 0.84); + border: 1px solid rgba(129, 145, 168, 0.32); + border-radius: 6px; + padding: 10px 12px; + color: #c7d0da; + font-family: 'Share Tech Mono', monospace; + font-size: 9px; + line-height: 1.5; + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.24); + pointer-events: auto; + overflow: auto; + `; + this.container.appendChild(lifecyclePanel); + this.overlayNodes.push(lifecyclePanel); + this.lifecyclePanelNode = lifecyclePanel; + const layerTip = document.createElement('div'); layerTip.style.cssText = ` position: absolute; @@ -387,6 +659,413 @@ class NetworkStackVisualization { this.container.appendChild(layerTip); this.overlayNodes.push(layerTip); this.layerTooltipNode = layerTip; + + const viewModeBtn = document.createElement('button'); + viewModeBtn.textContent = 'MODE: DETAILED'; + viewModeBtn.style.cssText = ` + position: absolute; + top: 58px; + right: 20px; + padding: 7px 10px; + background: rgba(12, 18, 28, 0.88); + border: 1px solid rgba(125, 138, 156, 0.34); + color: #c6d0db; + font-family: 'Share Tech Mono', monospace; + font-size: 10px; + letter-spacing: 0.5px; + cursor: pointer; + z-index: 1002; + transition: all 0.2s ease; + `; + viewModeBtn.onmouseenter = () => { + viewModeBtn.style.background = 'rgba(19, 28, 40, 0.95)'; + viewModeBtn.style.color = '#edf2f8'; + }; + viewModeBtn.onmouseleave = () => { + viewModeBtn.style.background = 'rgba(12, 18, 28, 0.88)'; + viewModeBtn.style.color = '#c6d0db'; + }; + viewModeBtn.onclick = () => { + this.toggleViewDensityMode(); + }; + this.container.appendChild(viewModeBtn); + this.overlayNodes.push(viewModeBtn); + this.viewModeButton = viewModeBtn; + + const puzzleModeBtn = document.createElement('button'); + puzzleModeBtn.textContent = 'PUZZLE: OVERVIEW'; + puzzleModeBtn.style.cssText = ` + position: absolute; + top: 94px; + right: 20px; + padding: 7px 10px; + background: rgba(12, 18, 28, 0.88); + border: 1px solid rgba(125, 138, 156, 0.34); + color: #c6d0db; + font-family: 'Share Tech Mono', monospace; + font-size: 10px; + letter-spacing: 0.45px; + cursor: pointer; + z-index: 1002; + transition: all 0.2s ease; + `; + puzzleModeBtn.onmouseenter = () => { + puzzleModeBtn.style.background = 'rgba(19, 28, 40, 0.95)'; + puzzleModeBtn.style.color = '#edf2f8'; + }; + puzzleModeBtn.onmouseleave = () => { + puzzleModeBtn.style.background = 'rgba(12, 18, 28, 0.88)'; + puzzleModeBtn.style.color = '#c6d0db'; + }; + puzzleModeBtn.onclick = () => { + this.togglePuzzleDetailMode(); + }; + this.container.appendChild(puzzleModeBtn); + this.overlayNodes.push(puzzleModeBtn); + this.puzzleModeButton = puzzleModeBtn; + this.updateBottomPanelsLayout(); + this.updatePacketLifecycleUI(); + this.applyViewDensityMode(); + } + + updateBottomPanelsLayout() { + const w = window.innerWidth || 1280; + if (this.galaxyPanelNode) { + if (w <= 1180) { + this.galaxyPanelNode.style.width = '300px'; + this.galaxyPanelNode.style.maxWidth = '34vw'; + } else if (w <= 1440) { + this.galaxyPanelNode.style.width = '330px'; + this.galaxyPanelNode.style.maxWidth = '31vw'; + } else { + this.galaxyPanelNode.style.width = '360px'; + this.galaxyPanelNode.style.maxWidth = '30vw'; + } + } + if (this.lifecyclePanelNode) { + if (w <= 1180) { + this.lifecyclePanelNode.style.minWidth = '420px'; + this.lifecyclePanelNode.style.width = 'calc(100vw - 360px)'; + this.lifecyclePanelNode.style.maxWidth = 'calc(100vw - 340px)'; + this.lifecyclePanelNode.style.maxHeight = '30vh'; + } else if (w <= 1440) { + this.lifecyclePanelNode.style.minWidth = '520px'; + this.lifecyclePanelNode.style.width = 'calc(100vw - 430px)'; + this.lifecyclePanelNode.style.maxWidth = 'calc(100vw - 410px)'; + this.lifecyclePanelNode.style.maxHeight = '31vh'; + } else { + this.lifecyclePanelNode.style.minWidth = '620px'; + this.lifecyclePanelNode.style.width = '760px'; + this.lifecyclePanelNode.style.maxWidth = 'calc(100vw - 430px)'; + this.lifecyclePanelNode.style.maxHeight = '32vh'; + } + } + } + + applyViewDensityMode() { + const minimal = this.viewDensityMode === 'minimal'; + if (this.layersPanelNode) { + this.layersPanelNode.style.display = minimal ? 'none' : 'block'; + } + if (this.chipLayerNode) { + this.chipLayerNode.style.display = minimal ? 'none' : 'block'; + } + if (this.galaxyPanelNode) { + this.galaxyPanelNode.style.display = minimal ? 'none' : 'block'; + } + if (this.lifecyclePanelNode) { + this.lifecyclePanelNode.style.display = minimal ? 'none' : 'block'; + } + if (this.layerTooltipNode) { + this.layerTooltipNode.style.display = 'none'; + } + if (this.viewModeButton) { + this.viewModeButton.textContent = minimal ? 'MODE: MINIMAL' : 'MODE: DETAILED'; + this.viewModeButton.style.borderColor = minimal + ? 'rgba(230, 193, 90, 0.58)' + : 'rgba(125, 138, 156, 0.34)'; + this.viewModeButton.style.color = minimal ? '#f0dca2' : '#c6d0db'; + } + if (this.puzzleModeButton) { + this.puzzleModeButton.style.display = minimal ? 'none' : 'block'; + } + } + + toggleViewDensityMode() { + this.viewDensityMode = this.viewDensityMode === 'minimal' ? 'detailed' : 'minimal'; + this.applyViewDensityMode(); + } + + togglePuzzleDetailMode() { + this.puzzleDetailMode = this.puzzleDetailMode === 'overview' ? 'deep-dive' : 'overview'; + if (this.puzzleModeButton) { + const isOverview = this.puzzleDetailMode === 'overview'; + this.puzzleModeButton.textContent = isOverview ? 'PUZZLE: OVERVIEW' : 'PUZZLE: DEEP DIVE'; + this.puzzleModeButton.style.borderColor = isOverview + ? 'rgba(125, 138, 156, 0.34)' + : 'rgba(230, 193, 90, 0.58)'; + this.puzzleModeButton.style.color = isOverview ? '#c6d0db' : '#f0dca2'; + } + this.updatePacketLifecycleUI(); + } + + selectGalaxy(id) { + if (!id || !this.galaxyNodes[id]) return; + this.selectedGalaxy = id; + this.refreshGalaxySelectionUI(); + } + + refreshGalaxySelectionUI() { + const data = this.galaxyStateData || {}; + Object.entries(this.galaxyNodes).forEach(([id, node]) => { + const item = data[id]; + if (!item) return; + const tone = this.getHealthTone(item.level || 'normal'); + const selected = this.selectedGalaxy === id; + node.style.background = selected ? tone.bg.replace('0.68', '0.9') : tone.bg; + node.style.borderColor = selected ? '#d9e4f0' : tone.border; + node.style.color = tone.text; + node.style.boxShadow = selected ? '0 0 0 1px rgba(170, 188, 206, 0.45)' : 'none'; + node.textContent = `${item.label}: ${item.value}`; + }); + + if (this.galaxyExplainNode) { + const active = data[this.selectedGalaxy]; + if (active) { + window.setSafeHtml(this.galaxyExplainNode, ` + ${active.label}: + ${active.explain} + `); + } else { + this.galaxyExplainNode.textContent = 'Select a galaxy to inspect subsystem health.'; + } + } + } + + getPacketLifecycleIndex() { + if (!this.packet || !this.layerMap || !Number.isFinite(this.packet.position.y)) return 0; + const yTop = Number(this.layerMap.userspace ?? 3.5) + 0.5; + const yBottom = Number(this.layerMap.nic ?? -3.4) - 0.75; + const range = Math.max(0.001, yTop - yBottom); + const progress = (yTop - this.packet.position.y) / range; + const clamped = Math.max(0, Math.min(0.999, progress)); + return Math.floor(clamped * this.packetLifecycleStages.length); + } + + updatePacketLifecycleUI() { + if (!this.lifecyclePanelNode) return; + const idx = this.getPacketLifecycleIndex(); + const stage = this.packetLifecycleStages[Math.max(0, Math.min(this.packetLifecycleStages.length - 1, idx))] || 'NIC RX'; + const activeCoreNode = this.lifecycleStageToNode[stage] || 'skb'; + const focusByGalaxy = { + physical: new Set(['hardware', 'interrupt', 'napi']), + packet: new Set(['skb', 'xdp', 'tc', 'netfilter']), + state: new Set(['conntrack', 'routing', 'transport', 'socket']), + security: new Set(['netfilter', 'conntrack', 'security', 'crypto']), + observability: new Set(['observability', 'ebpf', 'userspace']) + }; + const focused = focusByGalaxy[this.selectedGalaxy] || new Set(['conntrack', 'routing', 'transport']); + const activeLinkedSide = new Set(this.coreToSideLinks[activeCoreNode] || []); + const sideNodeById = this.puzzleSideNodes.reduce((acc, node) => { + acc[node.id] = node; + return acc; + }, {}); + const coreNodeById = this.puzzleCoreNodes.reduce((acc, node) => { + acc[node.id] = node; + return acc; + }, {}); + + const renderPuzzleNode = (node, active = false, softActive = false, emphasized = false) => { + const bg = active + ? 'rgba(88, 182, 216, 0.34)' + : (softActive ? 'rgba(230, 193, 90, 0.14)' : 'rgba(16, 22, 32, 0.72)'); + const border = active + ? 'rgba(159, 233, 255, 0.88)' + : (softActive ? 'rgba(230, 193, 90, 0.5)' : 'rgba(115, 128, 145, 0.34)'); + const text = active ? '#ecfbff' : (softActive ? '#f2e2b5' : '#bac4cf'); + return ` + + ${node.label} + ${node.tags} + + `; + }; + + const rxPath = this.packetLifecycleStages.map((item, i) => ( + i === idx ? `${item}` : `${item}` + )).join(' → '); + const txPath = this.txLifecycleStages.map((item) => `${item}`).join(' → '); + const renderCoreRow = (nodes) => nodes.map((node, i) => { + const next = i < nodes.length - 1 ? '' : ''; + const isActive = node.id === activeCoreNode; + const soft = focused.has(node.id) && !isActive; + return `${renderPuzzleNode(node, isActive, soft, isActive)}${next}`; + }).join(''); + const coreTop = renderCoreRow(this.puzzleCoreNodes.slice(0, 7)); + const coreBottom = renderCoreRow(this.puzzleCoreNodes.slice(7)); + + const renderCluster = (cluster) => { + const items = cluster.nodes.map((id) => { + const node = sideNodeById[id]; + if (!node) return ''; + const linked = activeLinkedSide.has(id); + const soft = focused.has(id) || linked; + return renderPuzzleNode(node, false, soft, linked); + }).join(''); + return ` +
+ ${cluster.label}${items} +
+ `; + }; + const clusteredSidePuzzle = this.sideClusters.map((cluster) => renderCluster(cluster)).join(''); + + const interactionLinks = (this.coreToSideLinks[activeCoreNode] || []) + .map((sideId) => { + const side = sideNodeById[sideId]; + const core = coreNodeById[activeCoreNode]; + if (!side || !core) return ''; + return `${core.label} ${side.label}`; + }) + .filter(Boolean) + .join(' | '); + const selectedLinks = [...focused] + .map((id) => sideNodeById[id] || coreNodeById[id]) + .filter(Boolean) + .map((node) => `${node.label}`) + .join(', '); + + const isOverview = this.puzzleDetailMode === 'overview'; + const summarySide = (this.coreToSideLinks[activeCoreNode] || []) + .map((id) => sideNodeById[id]?.label || '') + .filter(Boolean) + .join(', '); + + if (isOverview) { + window.setSafeHtml(this.lifecyclePanelNode, ` +
+ LINUX NETWORKING PUZZLE ARCHITECTURE +
+
+ ${coreTop} +
+
+ ${coreBottom} +
+
+ Active interactions: ${interactionLinks || 'none'} +
+
+ Linked subsystems: ${summarySide || 'none'} +
+
RX: ${rxPath}
+
TX: ${txPath}
+
+ Active puzzle: ${stage} +
+ `); + return; + } + + window.setSafeHtml(this.lifecyclePanelNode, ` +
+ LINUX NETWORKING PUZZLE ARCHITECTURE +
+
+ ${coreTop} +
+
+ ${coreBottom} +
+
+ ${clusteredSidePuzzle} +
+
+ Active interactions: ${interactionLinks || 'none'} +
+
+ Galaxy focus: ${selectedLinks || 'none'} +
+
RX flow: ${rxPath}
+
TX flow: ${txPath}
+
+ Current active puzzle: ${stage} +
+ `); + } + + updateGalaxyPanel(m, flow) { + if (!this.galaxyPanelNode) return; + const safeNum = (value) => { + const n = Number(value); + return Number.isFinite(n) ? n : 0; + }; + const nicErr = safeNum(m.nic?.rx_errors) + safeNum(m.nic?.tx_errors); + const txq = safeNum(m.driver?.tx_queue); + const drops = safeNum(m.netfilter?.drop_per_sec); + const dropRatio = safeNum(m.netfilter?.drop_ratio); + const rtt = safeNum(m.tcp_udp?.rtt_ms); + const retrans = safeNum(m.tcp_udp?.retrans_per_sec ?? m.socket_api?.retransmits_per_sec); + const established = safeNum(m.socket_api?.established); + const flowType = String(flow?.type || 'TCP').toUpperCase(); + const flowState = String(flow?.state_name || 'NO_FLOW'); + + const health = (warn, crit, value) => (value >= crit ? 'critical' : (value >= warn ? 'warn' : 'normal')); + + const physicalLevel = health(120, 300, txq + nicErr * 8); + const packetLevel = (dropRatio > 0.2 || drops > 20) ? 'critical' : ((dropRatio > 0.05 || drops > 7) ? 'warn' : 'normal'); + const stateLevel = health(8, 25, retrans + rtt / 20); + const securityLevel = packetLevel; + const observabilityLevel = flow ? 'normal' : 'warn'; + this.galaxyStateData = { + physical: { + label: 'Physical', + value: `txq ${txq} err ${nicErr}`, + level: physicalLevel, + explain: `NIC/driver pressure. High txq or NIC errors means hardware queues are saturated or unstable.` + }, + packet: { + label: 'Packet', + value: `drop ${drops.toFixed(1)}/s`, + level: packetLevel, + explain: `Packet metabolism quality. Drop rate shows where the flow is being lost before delivery.` + }, + state: { + label: 'State', + value: `rtt ${rtt.toFixed(1)}ms rt ${retrans.toFixed(1)}/s`, + level: stateLevel, + explain: `Transport/conntrack dynamics. RTT and retransmits indicate congestion and connection stress.` + }, + security: { + label: 'Security', + value: `ratio ${(dropRatio * 100).toFixed(1)}%`, + level: securityLevel, + explain: `Netfilter behavior. Higher drop ratio may be policy enforcement, attack filtering, or misconfiguration.` + }, + observability: { + label: 'Observability', + value: `${flowType} ${flowState} est ${established}`, + level: observabilityLevel, + explain: `Current flow visibility. Shows active connection state and whether telemetry sees stable sessions.` + } + }; + + if (!this.galaxyStateData[this.selectedGalaxy]) { + this.selectedGalaxy = 'state'; + } + this.refreshGalaxySelectionUI(); } addExitButton() { @@ -505,33 +1184,123 @@ class NetworkStackVisualization { } } + getHealthTone(level) { + if (level === 'critical') { + return { bg: 'rgba(65, 20, 24, 0.74)', border: 'rgba(226, 106, 118, 0.65)', text: '#ffb8c0' }; + } + if (level === 'warn') { + return { bg: 'rgba(64, 52, 22, 0.72)', border: 'rgba(226, 193, 102, 0.64)', text: '#f2d89b' }; + } + return { bg: 'rgba(16, 22, 32, 0.68)', border: 'rgba(115, 128, 145, 0.32)', text: '#bac4cf' }; + } + + updateKpiCard(id, value, level = 'normal') { + const node = this.kpiNodes[id]; + if (!node) return; + const tone = this.getHealthTone(level); + node.value.textContent = String(value ?? '--'); + node.card.style.background = tone.bg; + node.card.style.borderColor = tone.border; + node.value.style.color = tone.text; + } + + setMetricChip(id, label, value, level = 'normal') { + const chip = this.metricChips[id]; + if (!chip) return; + const tone = this.getHealthTone(level); + chip.textContent = `${label} ${value}`; + chip.style.background = tone.bg; + chip.style.borderColor = tone.border; + chip.style.color = tone.text; + } + updateTelemetryUI() { if (!this.telemetryData) return; const m = this.telemetryData.layer_metrics || {}; - const sig = this.telemetryData.signals || {}; const a = this.telemetryData.layer_activity || {}; const flow = this.telemetryData.flow; + const flowType = String(flow?.type || 'TCP').toUpperCase(); + const flowState = String(flow?.state_name || ''); + const safeNum = (value) => { + const n = Number(value); + return Number.isFinite(n) ? n : 0; + }; + + const rttMs = safeNum(m.tcp_udp?.rtt_ms ?? 0); + const retransPerSec = safeNum(m.tcp_udp?.retrans_per_sec ?? m.socket_api?.retransmits_per_sec ?? 0); + const dropPerSec = safeNum(m.netfilter?.drop_per_sec ?? 0); + const dropRatio = safeNum(m.netfilter?.drop_ratio ?? 0); + const driverTxQ = safeNum(m.driver?.tx_queue ?? 0); + const nicErrRx = safeNum(m.nic?.rx_errors ?? 0); + const nicErrTx = safeNum(m.nic?.tx_errors ?? 0); + const nicErrTotal = nicErrRx + nicErrTx; + + const classify = (value, warnThreshold, critThreshold) => { + if (value >= critThreshold) return 'critical'; + if (value >= warnThreshold) return 'warn'; + return 'normal'; + }; + const dropLevel = (dropRatio >= 0.2 || dropPerSec >= 25) + ? 'critical' + : ((dropRatio >= 0.05 || dropPerSec >= 8) ? 'warn' : 'normal'); + const retransLevel = classify(retransPerSec, 8, 25); + const rttLevel = classify(rttMs, 90, 200); + const driverLevel = classify(driverTxQ, 120, 300); + const nicLevel = classify(nicErrTotal, 5, 20); if (this.flowNode) { if (flow) { - this.flowNode.textContent = `process -> syscall -> socket -> ${flow.type || 'TCP'} ${flow.state_name || ''} -> IP -> NIC -> wire -> ${flow.remote || 'remote'}`; + this.flowNode.textContent = `process -> syscall -> socket -> ${flowType} ${flowState} -> IP -> NIC -> wire -> ${flow.remote || 'remote'}`; } else { this.flowNode.textContent = 'process -> syscall -> socket -> TCP -> IP -> NIC -> wire -> remote (no active flow)'; } } - const set = (key, text) => { - if (this.metricChips[key]) { - this.metricChips[key].textContent = text; - } - }; - set('userspace', `Userspace procs ${m.userspace?.active_processes ?? 0}`); - set('socket', `Socket est ${m.socket_api?.established ?? 0} retrans ${m.socket_api?.retransmits_per_sec ?? 0}/s`); - set('tcp', `TCP cwnd ${m.tcp_udp?.cwnd ?? 0} rtt ${m.tcp_udp?.rtt_ms ?? 0}ms retrans ${m.tcp_udp?.retrans_per_sec ?? 0}/s`); - set('ip', `IP in ${m.ip?.in_packets_per_sec ?? 0}/s out ${m.ip?.out_packets_per_sec ?? 0}/s`); - set('netfilter', `Netfilter drop ${(m.netfilter?.drop_per_sec ?? 0)}/s`); - set('driver', `Driver txq ${m.driver?.tx_queue ?? 0} drops ${(m.driver?.drops_per_sec ?? 0)}/s`); - set('nic', `NIC ${m.nic?.iface ?? 'n/a'} err ${m.nic?.rx_errors ?? 0}/${m.nic?.tx_errors ?? 0}`); + this.updateKpiCard('flow', `${flowType}${flowState ? ` ${flowState}` : ''}`, flow ? 'normal' : 'warn'); + this.updateKpiCard('rtt', `${rttMs.toFixed(1)} ms`, rttLevel); + this.updateKpiCard('drop', `${dropPerSec.toFixed(1)}/s`, dropLevel); + this.updateKpiCard('retrans', `${retransPerSec.toFixed(1)}/s`, retransLevel); + + this.setMetricChip('userspace', 'USERSPACE', `procs ${m.userspace?.active_processes ?? 0}`); + this.setMetricChip( + 'socket', + 'SOCKET', + `est ${m.socket_api?.established ?? 0} retrans ${safeNum(m.socket_api?.retransmits_per_sec ?? 0).toFixed(1)}/s`, + retransLevel + ); + this.setMetricChip( + 'tcp', + 'TCP', + `cwnd ${m.tcp_udp?.cwnd ?? 0} rtt ${rttMs.toFixed(1)}ms retrans ${retransPerSec.toFixed(1)}/s`, + rttLevel === 'critical' || retransLevel === 'critical' + ? 'critical' + : (rttLevel === 'warn' || retransLevel === 'warn' ? 'warn' : 'normal') + ); + this.setMetricChip( + 'ip', + 'IP', + `in ${safeNum(m.ip?.in_packets_per_sec ?? 0).toFixed(1)}/s out ${safeNum(m.ip?.out_packets_per_sec ?? 0).toFixed(1)}/s` + ); + this.setMetricChip( + 'netfilter', + 'NETFILTER', + `drop ${dropPerSec.toFixed(1)}/s ratio ${(dropRatio * 100).toFixed(1)}%`, + dropLevel + ); + this.setMetricChip( + 'driver', + 'DRIVER', + `txq ${driverTxQ} drops ${safeNum(m.driver?.drops_per_sec ?? 0).toFixed(1)}/s`, + driverLevel + ); + this.setMetricChip( + 'nic', + 'NIC', + `${m.nic?.iface ?? 'n/a'} err ${nicErrRx}/${nicErrTx}`, + nicLevel + ); + + this.updateGalaxyPanel(m, flow); this.layerActivityTarget = { userspace: Number(a.userspace ?? this.layerActivityTarget.userspace), @@ -671,6 +1440,12 @@ class NetworkStackVisualization { onMouseMove(event) { if (!this.isActive || !this.raycaster || !this.camera || !this.renderer) return; + if (this.viewDensityMode === 'minimal') { + if (this.layerTooltipNode) { + this.layerTooltipNode.style.display = 'none'; + } + return; + } const rect = this.renderer.domElement.getBoundingClientRect(); this.mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1; this.mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1; @@ -712,6 +1487,7 @@ class NetworkStackVisualization { this.updateEffects(dt); this.updateFlowParticles(dt); this.updateLayerStrips(dt); + this.updatePacketLifecycleUI(); // Gentle camera drift for cinematic depth. const t = now * 0.00025; @@ -777,6 +1553,7 @@ class NetworkStackVisualization { this.camera.aspect = window.innerWidth / window.innerHeight; this.camera.updateProjectionMatrix(); this.renderer.setSize(window.innerWidth, window.innerHeight); + this.updateBottomPanelsLayout(); } } diff --git a/static/js/ui-chrome.js b/static/js/ui-chrome.js index bb03127..767c23c 100644 --- a/static/js/ui-chrome.js +++ b/static/js/ui-chrome.js @@ -1,48 +1,6 @@ // UI chrome module extracted from main.js (function initUiChrome(){ const svg = d3.select("svg"); -let backendStatusNode = null; - -function ensureBackendStatusNode() { - if (backendStatusNode && backendStatusNode.parentNode) return backendStatusNode; - const node = document.createElement('div'); - node.id = 'backend-status-chip'; - node.style.cssText = [ - 'position: fixed', - 'left: 14px', - 'bottom: 14px', - 'padding: 5px 8px', - 'border-radius: 5px', - 'background: rgba(12, 18, 28, 0.88)', - 'border: 1px solid rgba(145, 180, 220, 0.4)', - 'color: #cfe2fa', - 'font-family: "Share Tech Mono", monospace', - 'font-size: 10px', - 'letter-spacing: 0.2px', - 'pointer-events: none', - 'z-index: 1100', - 'opacity: 0.85' - ].join(';'); - node.textContent = 'backend: connecting...'; - document.body.appendChild(node); - backendStatusNode = node; - return node; -} - -function setBackendStatus(online, details = '') { - const node = ensureBackendStatusNode(); - if (online) { - node.textContent = 'backend: online'; - node.style.background = 'rgba(8, 24, 18, 0.88)'; - node.style.borderColor = 'rgba(106, 210, 160, 0.58)'; - node.style.color = '#bbf1d9'; - } else { - node.textContent = details ? `backend: degraded (${details})` : 'backend: degraded'; - node.style.background = 'rgba(34, 20, 10, 0.9)'; - node.style.borderColor = 'rgba(244, 201, 119, 0.62)'; - node.style.color = '#ffe7ba'; - } -} // Update panel with real data from API function updatePanelData() { @@ -63,7 +21,6 @@ function updatePanelData() { toastMessage: 'Kernel summary is temporarily unavailable' }) .then(data => { - setBackendStatus(true); // Update processes count const processesText = d3.select('#panel-value-2'); if (!processesText.empty() && data.processes) { @@ -83,7 +40,6 @@ function updatePanelData() { } }) .catch(error => { - setBackendStatus(false, error && error.message ? error.message : 'request failed'); const processesText = d3.select('#panel-value-2'); if (!processesText.empty()) { processesText.text('N/A');