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');