LIVE ACTIVITY
@@ -3085,19 +4087,682 @@ class NetworkStackVisualization {
${metricCells}
${info.what}
-
WATCH · ${info.watch}
- ${this.buildIpLayerMapHtml({ compact: false })}
+
WATCH · ${info.watch}
+ ${morph}
+ ${this.buildTcpLayerMapHtml({ compact: false })}
`;
window.setSafeHtml(this.drillPanel, html);
const closeBtn = this.drillPanel.querySelector('.ns-ov-close');
if (closeBtn) closeBtn.addEventListener('click', () => this.closeLayerDrilldown());
- this.renderIpLayerMapPanel();
+ this.bindTcpMapInteractions(this.drillPanel);
+ }
+ }
+
+ openTcpKernelMorph(target) {
+ this.freezeTcpSnapshot();
+ this.tcpMorphTarget = target || { focus: 'flow' };
+ this.tcpMapPinned = true;
+ // Clear other pins so the right panel shows TCP map only.
+ this.ipMapPinned = false;
+ this.ipMorphTarget = null;
+ this.nfMapPinned = false;
+ this.nfMorphTarget = null;
+ this.sockMapPinned = false;
+ this.sockMorphTarget = null;
+ if (this.lifecyclePanelNode) this.renderTcpLayerMapPanel();
+ if (this.drillLayerId !== 'tcp') {
+ this.openLayerDrilldown('tcp');
+ return;
+ }
+ this.refreshTcpMapViews({ drillOnly: true });
+ }
+
+ getNfSnap() {
+ if (this.nfFrozen) return this.nfFrozen;
+ return {
+ netfilter: { ...(this.telemetryData?.layer_metrics?.netfilter || {}) },
+ flow: { ...(this.telemetryData?.flow || {}) },
+ };
+ }
+
+ freezeNfSnapshot() {
+ const snap = this.getNfSnap();
+ try {
+ this.nfFrozen = JSON.parse(JSON.stringify(snap));
+ } catch (_) {
+ this.nfFrozen = snap;
+ }
+ }
+
+ buildNfKernelMorphHtml(target = {}) {
+ const snap = this.getNfSnap();
+ const nf = snap.netfilter || {};
+ const flow = snap.flow || {};
+ const n = (v, d = 0) => (Number.isFinite(Number(v)) ? Number(v) : d);
+ const esc = (v) => this.escapeHtml(v);
+ const drops = n(nf.drop_per_sec);
+ const ratio = n(nf.drop_ratio);
+ const ct = n(nf.conntrack_count);
+ const ctMax = n(nf.conntrack_max);
+ const ctUsage = n(nf.conntrack_usage);
+ const engine = nf.nft ? 'nftables' : (nf.nf_conntrack ? 'iptables+ct' : 'netfilter');
+ const verdict = drops > 0.5 || ratio > 0.01 ? 'NF_DROP pressure' : 'NF_ACCEPT path';
+ const verdictCol = drops > 0.5 || ratio > 0.01 ? '#e69696' : '#96ffbe';
+ const focus = target.focus || 'hook';
+ const hi = (key, accent) => (focus === key ? accent : 'rgba(96,110,128,0.32)');
+ const step = (sym, title, body, accent) => (
+ '
'
+ + '
' + esc(title) + '
'
+ + '
' + body + '
'
+ + '
' + esc(sym) + '
'
+ + '
'
+ );
+ const arrow = '
↓
';
+ const flowName = esc(flow.state_name || '-');
+ const stepsHtml = [
+ step(
+ 'NF_HOOK / nf_hook_slow',
+ '1 · HOOK',
+ '
PREROUTING → LOCAL_IN / FORWARD → POSTROUTING '
+ + '
engine ' + esc(engine) + ' · flow ' + flowName + ' ',
+ hi('hook', 'rgba(232,96,104,0.45)')
+ ),
+ arrow,
+ step(
+ 'nf_conntrack_in',
+ '2 · CONNTRACK',
+ 'ct entries
' + esc(ct) + ' '
+ + ' /
' + esc(ctMax || '-') + ' '
+ + ' ·
' + esc((ctUsage * 100).toFixed(2)) + '% '
+ + '
tuple → nf_conn · NEW / ESTABLISHED ',
+ hi('ct', 'rgba(103,190,224,0.45)')
+ ),
+ arrow,
+ step(
+ 'nft_do_chain / iptable_*',
+ '3 · CHAIN',
+ 'rules walk the skb at the active hook
'
+ + '
match → target (filter / nat / mangle) ',
+ hi('chain', 'rgba(230,193,90,0.45)')
+ ),
+ arrow,
+ step(
+ 'nft_verdict / NF_DROP',
+ '4 · VERDICT',
+ '
' + esc(verdict) + ' '
+ + '
drop/s ' + esc(drops) + ' · ratio ' + (ratio * 100).toFixed(2) + '% ',
+ hi('verdict', 'rgba(232,96,104,0.5)')
+ ),
+ arrow,
+ step(
+ 'okfn / skb continue',
+ '5 · CONTINUE',
+ 'ACCEPT → stack continues · DROP → kfree_skb
'
+ + '
surviving skb → TCP/IP path ',
+ hi('continue', 'rgba(150,255,190,0.35)')
+ ),
+ ].join('');
+
+ return (
+ '
'
+ + '
'
+ + '
'
+ + '
NETFILTER → KERNEL TRANSLATION
'
+ + '
policy is not a black box — hooks, conntrack tuples, then a verdict on the skb
'
+ + '
'
+ + '
CLOSE '
+ + '
'
+ + '
'
+ + stepsHtml
+ + '
'
+ + '
'
+ + 'symbols: nf_hook_slow → nf_conntrack_in → nft_do_chain → nft_verdict → okfn'
+ + '
'
+ );
+ }
+
+ animateNfMorph(root) {
+ if (!root) return;
+ const steps = [...root.querySelectorAll('.ns-nf-morph-step')];
+ const arrows = [...root.querySelectorAll('.ns-nf-morph-arrow')];
+ steps.forEach((el, i) => {
+ setTimeout(() => {
+ el.style.opacity = '1';
+ el.style.transform = 'translateY(0)';
+ if (arrows[i]) arrows[i].style.opacity = '1';
+ }, 90 + i * 160);
+ });
+ }
+
+ buildNfLayerMapHtml({ compact = false } = {}) {
+ const snap = this.getNfSnap();
+ const nf = snap.netfilter || {};
+ const n = (v, d = 0) => (Number.isFinite(Number(v)) ? Number(v) : d);
+ const esc = (v) => this.escapeHtml(v);
+ const drops = n(nf.drop_per_sec);
+ const ratio = n(nf.drop_ratio);
+ const ct = n(nf.conntrack_count);
+ const ctMax = n(nf.conntrack_max);
+ const engine = nf.nft ? 'nftables' : (nf.nf_conntrack ? 'iptables+ct' : 'netfilter');
+ const focus = this.nfMorphTarget?.focus || '';
+ const row = (key, label, value, color) => {
+ const active = focus === key;
+ return `
+ ${label}
+ ${esc(value)}
+
`;
+ };
+ return `
+
+
+
NETFILTER MAP · L03 · ${esc(engine)}
+
hooks · nf_conntrack · verdict
+
+ ${compact ? `
← PUZZLE ` : ''}
+
+ ${compact ? '
click a field → Netfilter→kernel morph (center)
' : ''}
+
+
+ ${row('hook', 'HOOKS', 'PREROUTING…POSTROUTING', '#e69696')}
+ ${row('ct', 'CONNTRACK', `${ct} / ${ctMax || '—'}`, '#a9d4e8')}
+ ${row('verdict', 'DROPS', `${drops}/s · ${(ratio * 100).toFixed(2)}%`, drops > 0 ? '#e69696' : '#8d99a7')}
+ ${row('chain', 'ENGINE', engine, '#e6c15a')}
+
+
+
RX PATH
+
NIC → PREROUTING
+
→ nf_conntrack_in
+
→ nft_do_chain
+
→ verdict → TCP/IP or drop
+ ${!compact ? `
+ ▸ NETFILTER → KERNEL TRANSLATION
+
` : ''}
+
+
`;
+ }
+
+ bindNfMapInteractions(root) {
+ if (!root) return;
+ root.querySelectorAll('.ns-nf-row').forEach((el) => {
+ el.addEventListener('click', (e) => {
+ e.stopPropagation();
+ this.openNfKernelMorph({ focus: el.getAttribute('data-nf-focus') || 'hook' });
+ });
+ });
+ const openMorph = root.querySelector('.ns-nf-open-morph');
+ if (openMorph) {
+ openMorph.addEventListener('click', (e) => {
+ e.stopPropagation();
+ this.openNfKernelMorph({ focus: 'hook' });
+ });
+ }
+ const back = root.querySelector('.ns-nf-back-puzzle');
+ if (back) {
+ back.addEventListener('click', () => {
+ this.nfMapPinned = false;
+ this.nfMorphTarget = null;
+ this.nfFrozen = null;
+ if (this.drillLayerId === 'netfilter') this.closeLayerDrilldown();
+ else this.updatePacketLifecycleUI();
+ });
+ }
+ const morphClose = root.querySelector('.ns-nf-morph-close');
+ if (morphClose) {
+ morphClose.addEventListener('click', (e) => {
+ e.stopPropagation();
+ this.nfMorphTarget = null;
+ this.renderNfLayerMapPanel();
+ this.refreshNfMapViews({ drillOnly: true });
+ });
+ }
+ const morphRoot = root.querySelector('.ns-nf-morph');
+ if (morphRoot) this.animateNfMorph(morphRoot);
+ }
+
+ renderNfLayerMapPanel() {
+ if (!this.lifecyclePanelNode) return;
+ window.setSafeHtml(this.lifecyclePanelNode, this.buildNfLayerMapHtml({ compact: true }));
+ this.bindNfMapInteractions(this.lifecyclePanelNode);
+ }
+
+ refreshNfMapViews({ drillOnly = false } = {}) {
+ if (!drillOnly && this.lifecyclePanelNode && (this.nfMapPinned || this.drillLayerId === 'netfilter')) {
+ this.renderNfLayerMapPanel();
+ }
+ if (this.drillLayerId === 'netfilter' && this.drillScrim?.style.display === 'block') {
+ const info = this.getLayerDrillInfo('netfilter');
+ if (!info || !this.drillPanel) return;
+ const act = Math.round(Math.max(0, Math.min(1, Number(this.layerActivity.netfilter ?? 0))) * 100);
+ const actCol = act > 80 ? 'rgba(232,96,104,0.95)' : (act > 55 ? 'rgba(230,193,90,0.95)' : 'rgba(103,190,224,0.95)');
+ const metrics = this.getLayerDrillMetrics('netfilter');
+ const metricCells = metrics.map(([k, v]) => `
+
`).join('');
+ const morph = this.nfMorphTarget
+ ? this.buildNfKernelMorphHtml(this.nfMorphTarget)
+ : `
+ tip: click hooks / conntrack / verdict
+
`;
+ const html = `
+
+
+
STACK LAYER · FIREWALL · L03
+
NETFILTER · HOOK / CT / VERDICT
+
+
+
LIVE ACTIVITY
+
${act}%
+
+
✕
+
+
+
${metricCells}
+
${info.what}
+
WATCH · ${info.watch}
+ ${morph}
+ ${this.buildNfLayerMapHtml({ compact: false })}
+
`;
+ window.setSafeHtml(this.drillPanel, html);
+ const closeBtn = this.drillPanel.querySelector('.ns-ov-close');
+ if (closeBtn) closeBtn.addEventListener('click', () => this.closeLayerDrilldown());
+ this.bindNfMapInteractions(this.drillPanel);
+ }
+ }
+
+ openNfKernelMorph(target) {
+ this.freezeNfSnapshot();
+ this.nfMorphTarget = target || { focus: 'hook' };
+ this.nfMapPinned = true;
+ this.ipMapPinned = false;
+ this.ipMorphTarget = null;
+ this.tcpMapPinned = false;
+ this.tcpMorphTarget = null;
+ this.sockMapPinned = false;
+ this.sockMorphTarget = null;
+ if (this.lifecyclePanelNode) this.renderNfLayerMapPanel();
+ if (this.drillLayerId !== 'netfilter') {
+ this.openLayerDrilldown('netfilter');
+ return;
+ }
+ this.refreshNfMapViews({ drillOnly: true });
+ }
+
+ getSockSnap() {
+ if (this.sockFrozen) return this.sockFrozen;
+ return {
+ socket_api: { ...(this.telemetryData?.layer_metrics?.socket_api || {}) },
+ flow: { ...(this.telemetryData?.flow || {}) },
+ };
+ }
+
+ freezeSockSnapshot() {
+ const snap = this.getSockSnap();
+ try {
+ this.sockFrozen = JSON.parse(JSON.stringify(snap));
+ } catch (_) {
+ this.sockFrozen = snap;
+ }
+ }
+
+ buildSockKernelMorphHtml(target = {}) {
+ const snap = this.getSockSnap();
+ const sa = snap.socket_api || {};
+ const flow = snap.flow || {};
+ const ex = sa.example || {};
+ const n = (v, d = 0) => (Number.isFinite(Number(v)) ? Number(v) : d);
+ const esc = (v) => this.escapeHtml(v);
+ const fd = ex.fd != null ? ex.fd : (flow.fd != null ? flow.fd : null);
+ const inode = n(ex.inode != null ? ex.inode : flow.inode);
+ const pid = ex.pid != null ? ex.pid : (flow.pid != null ? flow.pid : null);
+ const proc = ex.process || flow.process || 'process';
+ const local = ex.local || flow.local || 'local';
+ const remote = ex.remote || flow.remote || 'remote';
+ const state = ex.state_name || flow.state_name || 'ESTABLISHED';
+ const est = n(sa.established);
+ const active = n(sa.active_sockets);
+ const focus = target.focus || 'fd';
+ const hi = (key, accent) => (focus === key ? accent : 'rgba(96,110,128,0.32)');
+ const fdLabel = fd != null ? String(fd) : 'fd?';
+ const inodeLabel = inode > 0 ? String(inode) : 'inode?';
+ const pidLabel = pid != null ? String(pid) : 'pid?';
+ const skHint = inode > 0 ? ('sk@inode:' + inode) : 'struct sock *';
+
+ const step = (sym, title, body, accent) => (
+ '
'
+ + '
' + esc(title) + '
'
+ + '
' + body + '
'
+ + '
' + esc(sym) + '
'
+ + '
'
+ );
+ const arrow = '
↓
';
+ const stepsHtml = [
+ step(
+ 'fget / fdtable',
+ '1 · FILE DESCRIPTOR',
+ 'fd
' + esc(fdLabel) + ' '
+ + ' · pid
' + esc(pidLabel) + ' '
+ + '
(' + esc(proc) + ') '
+ + '
userspace handle → files_struct ',
+ hi('fd', 'rgba(150,255,190,0.45)')
+ ),
+ arrow,
+ step(
+ 'SOCKET_I(inode)',
+ '2 · INODE → socket',
+ 'inode
' + esc(inodeLabel) + ' '
+ + ' · uid
' + esc(ex.uid != null ? ex.uid : (flow.uid || 0)) + ' '
+ + '
file → dentry → inode → struct socket ',
+ hi('inode', 'rgba(230,193,90,0.45)')
+ ),
+ arrow,
+ step(
+ 'sock_alloc / socket->sk',
+ '3 · struct sock *',
+ '
' + esc(skHint) + ' '
+ + '
' + esc(local) + ' '
+ + '
→ '
+ + '
' + esc(remote) + ' '
+ + '
state ' + esc(state) + ' · protocol socket object ',
+ hi('sk', 'rgba(103,190,224,0.5)')
+ ),
+ arrow,
+ step(
+ 'sk_receive_queue / sk_write_queue',
+ '4 · QUEUES',
+ 'active
' + esc(active) + ' '
+ + ' · established
' + esc(est) + ' '
+ + '
recv/send buffers · accept backlog · wakeups ',
+ hi('queue', 'rgba(150,255,190,0.35)')
+ ),
+ arrow,
+ step(
+ 'sock_sendmsg / sock_recvmsg',
+ '5 · SYSCALL PATH',
+ 'sendmsg / recvmsg copy between user pages and sk_buff
'
+ + '
fd stays the only handle the process ever sees ',
+ hi('syscall', 'rgba(212,221,231,0.35)')
+ ),
+ ].join('');
+
+ return (
+ '
'
+ + '
'
+ + '
'
+ + '
SOCKET → KERNEL TRANSLATION
'
+ + '
an fd is not the socket — it is a path: fd → file → inode → socket → sock*
'
+ + '
'
+ + '
CLOSE '
+ + '
'
+ + '
'
+ + stepsHtml
+ + '
'
+ + '
'
+ + 'symbols: fget → SOCKET_I → socket->sk → sk_*_queue → sock_sendmsg/recvmsg'
+ + '
'
+ );
+ }
+
+ animateSockMorph(root) {
+ if (!root) return;
+ const steps = [...root.querySelectorAll('.ns-sock-morph-step')];
+ const arrows = [...root.querySelectorAll('.ns-sock-morph-arrow')];
+ steps.forEach((el, i) => {
+ setTimeout(() => {
+ el.style.opacity = '1';
+ el.style.transform = 'translateY(0)';
+ if (arrows[i]) arrows[i].style.opacity = '1';
+ }, 90 + i * 160);
+ });
+ }
+
+ buildSockLayerMapHtml({ compact = false } = {}) {
+ const snap = this.getSockSnap();
+ const sa = snap.socket_api || {};
+ const flow = snap.flow || {};
+ const ex = sa.example || {};
+ const n = (v, d = 0) => (Number.isFinite(Number(v)) ? Number(v) : d);
+ const esc = (v) => this.escapeHtml(v);
+ const fd = ex.fd != null ? ex.fd : flow.fd;
+ const inode = n(ex.inode != null ? ex.inode : flow.inode);
+ const pid = ex.pid != null ? ex.pid : flow.pid;
+ const proc = ex.process || flow.process || '—';
+ const local = ex.local || flow.local || '—';
+ const remote = ex.remote || flow.remote || '—';
+ const state = ex.state_name || flow.state_name || '—';
+ const focus = this.sockMorphTarget?.focus || '';
+ const row = (key, label, value, color) => {
+ const active = focus === key;
+ return (
+ '
'
+ + '' + label + ' '
+ + '' + esc(value) + ' '
+ + '
'
+ );
+ };
+ return (
+ '
'
+ + '
'
+ + '
SOCKET MAP · L06 · fd → sock*
'
+ + '
files_struct · inode · struct sock
'
+ + '
'
+ + (compact
+ ? '
← PUZZLE '
+ : '')
+ + '
'
+ + (compact ? '
click a field → Socket→kernel morph (center)
' : '')
+ + '
'
+ + '
'
+ + row('fd', 'FD', fd != null ? (fd + ' · pid ' + (pid != null ? pid : '?')) : 'n/a (no process match)', '#96ffbe')
+ + row('inode', 'INODE', inode > 0 ? String(inode) : 'n/a', '#e6c15a')
+ + row('sk', 'SOCK*', local + ' → ' + remote, '#a9d4e8')
+ + row('queue', 'QUEUES', 'est ' + n(sa.established) + ' / active ' + n(sa.active_sockets), '#96ffbe')
+ + row('syscall', 'STATE', state + ' · ' + proc, '#d4dde7')
+ + '
'
+ + '
'
+ + '
LOOKUP PATH
'
+ + '
userspace fd
'
+ + '
→ inode / SOCKET_I
'
+ + '
→ struct sock *
'
+ + '
→ queues → TCP/UDP
'
+ + (!compact
+ ? '
'
+ + '▸ SOCKET → KERNEL TRANSLATION '
+ + '
'
+ : '')
+ + '
'
+ );
+ }
+
+ bindSockMapInteractions(root) {
+ if (!root) return;
+ root.querySelectorAll('.ns-sock-row').forEach((el) => {
+ el.addEventListener('click', (e) => {
+ e.stopPropagation();
+ this.openSockKernelMorph({ focus: el.getAttribute('data-sock-focus') || 'fd' });
+ });
+ });
+ const openMorph = root.querySelector('.ns-sock-open-morph');
+ if (openMorph) {
+ openMorph.addEventListener('click', (e) => {
+ e.stopPropagation();
+ this.openSockKernelMorph({ focus: 'fd' });
+ });
+ }
+ const back = root.querySelector('.ns-sock-back-puzzle');
+ if (back) {
+ back.addEventListener('click', () => {
+ this.sockMapPinned = false;
+ this.sockMorphTarget = null;
+ this.sockFrozen = null;
+ if (this.drillLayerId === 'socket') this.closeLayerDrilldown();
+ else this.updatePacketLifecycleUI();
+ });
+ }
+ const morphClose = root.querySelector('.ns-sock-morph-close');
+ if (morphClose) {
+ morphClose.addEventListener('click', (e) => {
+ e.stopPropagation();
+ this.sockMorphTarget = null;
+ this.renderSockLayerMapPanel();
+ this.refreshSockMapViews({ drillOnly: true });
+ });
+ }
+ const morphRoot = root.querySelector('.ns-sock-morph');
+ if (morphRoot) this.animateSockMorph(morphRoot);
+ }
+
+ renderSockLayerMapPanel() {
+ if (!this.lifecyclePanelNode) return;
+ window.setSafeHtml(this.lifecyclePanelNode, this.buildSockLayerMapHtml({ compact: true }));
+ this.bindSockMapInteractions(this.lifecyclePanelNode);
+ }
+
+ refreshSockMapViews({ drillOnly = false } = {}) {
+ if (!drillOnly && this.lifecyclePanelNode && (this.sockMapPinned || this.drillLayerId === 'socket')) {
+ this.renderSockLayerMapPanel();
+ }
+ if (this.drillLayerId === 'socket' && this.drillScrim?.style.display === 'block') {
+ const info = this.getLayerDrillInfo('socket');
+ if (!info || !this.drillPanel) return;
+ const act = Math.round(Math.max(0, Math.min(1, Number(this.layerActivity.socket ?? 0))) * 100);
+ const actCol = act > 80 ? 'rgba(232,96,104,0.95)' : (act > 55 ? 'rgba(230,193,90,0.95)' : 'rgba(103,190,224,0.95)');
+ const metrics = this.getLayerDrillMetrics('socket');
+ const metricCells = metrics.map(([k, v]) => (
+ '
'
+ + '
' + this.escapeHtml(k) + '
'
+ + '
' + this.escapeHtml(v) + '
'
+ + '
'
+ )).join('');
+ const morph = this.sockMorphTarget
+ ? this.buildSockKernelMorphHtml(this.sockMorphTarget)
+ : '
'
+ + 'tip: click fd / inode / sock* '
+ + '
';
+ const html = (
+ '
'
+ + '
'
+ + '
STACK LAYER · SOCKET API · L06
'
+ + '
SOCKET · FD / INODE / SOCK*
'
+ + '
'
+ + '
'
+ + '
LIVE ACTIVITY
'
+ + '
' + act + '%
'
+ + '
'
+ + '
✕
'
+ + '
'
+ + '
'
+ + '
' + metricCells + '
'
+ + '
' + info.what + '
'
+ + '
WATCH · ' + info.watch + '
'
+ + morph
+ + this.buildSockLayerMapHtml({ compact: false })
+ + '
'
+ );
+ window.setSafeHtml(this.drillPanel, html);
+ const closeBtn = this.drillPanel.querySelector('.ns-ov-close');
+ if (closeBtn) closeBtn.addEventListener('click', () => this.closeLayerDrilldown());
+ this.bindSockMapInteractions(this.drillPanel);
+ }
+ }
+
+ openSockKernelMorph(target) {
+ this.freezeSockSnapshot();
+ this.sockMorphTarget = target || { focus: 'fd' };
+ this.sockMapPinned = true;
+ this.ipMapPinned = false;
+ this.ipMorphTarget = null;
+ this.tcpMapPinned = false;
+ this.tcpMorphTarget = null;
+ this.nfMapPinned = false;
+ this.nfMorphTarget = null;
+ if (this.lifecyclePanelNode) this.renderSockLayerMapPanel();
+ if (this.drillLayerId !== 'socket') {
+ this.openLayerDrilldown('socket');
+ return;
+ }
+ this.refreshSockMapViews({ drillOnly: true });
+ }
+
+ clearLayerPinsExcept(keep) {
+ if (keep !== 'ip') {
+ this.ipMapPinned = false;
+ this.ipMorphTarget = null;
+ this.ipMapFrozen = null;
+ }
+ if (keep !== 'tcp') {
+ this.tcpMapPinned = false;
+ this.tcpMorphTarget = null;
+ this.tcpFrozen = null;
+ }
+ if (keep !== 'netfilter') {
+ this.nfMapPinned = false;
+ this.nfMorphTarget = null;
+ this.nfFrozen = null;
+ }
+ if (keep !== 'socket') {
+ this.sockMapPinned = false;
+ this.sockMorphTarget = null;
+ this.sockFrozen = null;
+ }
+ }
+
+ openLayerDrilldown(layerId) {
+ if (!this.drillScrim || !this.drillPanel) return;
+ const info = this.getLayerDrillInfo(layerId);
+ if (!info) return;
+ this.drillLayerId = layerId;
+
+ if (layerId === 'ip') {
+ this.clearLayerPinsExcept('ip');
+ this.ipMapPinned = true;
+ if (!this.ipMapFrozen) this.freezeIpMapSnapshot();
+ this.drillPanel.style.width = 'min(980px, 92vw)';
this.drillScrim.style.display = 'block';
if (this.layerTooltipNode) this.layerTooltipNode.style.display = 'none';
+ this.refreshIpMapViews();
return;
}
- this.ipMapPinned = false;
+ if (layerId === 'tcp') {
+ this.clearLayerPinsExcept('tcp');
+ this.tcpMapPinned = true;
+ if (!this.tcpFrozen) this.freezeTcpSnapshot();
+ this.drillPanel.style.width = 'min(980px, 92vw)';
+ this.drillScrim.style.display = 'block';
+ if (this.layerTooltipNode) this.layerTooltipNode.style.display = 'none';
+ this.refreshTcpMapViews();
+ return;
+ }
+
+ if (layerId === 'netfilter') {
+ this.clearLayerPinsExcept('netfilter');
+ this.nfMapPinned = true;
+ if (!this.nfFrozen) this.freezeNfSnapshot();
+ this.drillPanel.style.width = 'min(980px, 92vw)';
+ this.drillScrim.style.display = 'block';
+ if (this.layerTooltipNode) this.layerTooltipNode.style.display = 'none';
+ this.refreshNfMapViews();
+ return;
+ }
+
+ if (layerId === 'socket') {
+ this.clearLayerPinsExcept('socket');
+ this.sockMapPinned = true;
+ if (!this.sockFrozen) this.freezeSockSnapshot();
+ this.drillPanel.style.width = 'min(980px, 92vw)';
+ this.drillScrim.style.display = 'block';
+ if (this.layerTooltipNode) this.layerTooltipNode.style.display = 'none';
+ this.refreshSockMapViews();
+ return;
+ }
+
+ this.clearLayerPinsExcept(null);
this.drillPanel.style.width = 'min(680px, 78vw)';
const act = Math.round(Math.max(0, Math.min(1, Number(this.layerActivity[layerId] ?? 0))) * 100);
const actCol = act > 80 ? 'rgba(232,96,104,0.95)' : (act > 55 ? 'rgba(230,193,90,0.95)' : 'rgba(103,190,224,0.95)');
@@ -3130,25 +4795,59 @@ class NetworkStackVisualization {
KEY SUBSYSTEMS
${subs}