diff --git a/index.html b/index.html
index 994a1c8..a32b6bd 100755
--- a/index.html
+++ b/index.html
@@ -97,7 +97,7 @@
Linux Kernel Ring 0 Visualization
-
+
diff --git a/kernel_ai/services/process_timeline.py b/kernel_ai/services/process_timeline.py
index 05c3f54..39a37ba 100644
--- a/kernel_ai/services/process_timeline.py
+++ b/kernel_ai/services/process_timeline.py
@@ -2,13 +2,191 @@
from __future__ import annotations
-import os
from datetime import datetime
import psutil
-def get_proc_timeline_data(pid):
+def _clamp_window_s(window_s: int | float | None) -> int:
+ try:
+ value = int(window_s or 30)
+ except (TypeError, ValueError):
+ value = 30
+ return max(5, min(120, value))
+
+
+def _stamp_events_with_time_window(ordered_events: list[dict], proc_create_time: float, window_s: int) -> list[dict]:
+ now_ts = datetime.now().timestamp()
+ proc_age_s = max(0.1, now_ts - float(proc_create_time or now_ts))
+ span_s = min(float(window_s), proc_age_s)
+
+ if not ordered_events:
+ return []
+ if len(ordered_events) == 1:
+ row = dict(ordered_events[0])
+ event_ts = now_ts
+ row["timestamp"] = datetime.fromtimestamp(event_ts).isoformat()
+ row["relative_s"] = round(0.0, 3)
+ return [row]
+
+ start_ts = now_ts - span_s
+ timeline = []
+ count = len(ordered_events)
+ for i, ev in enumerate(ordered_events):
+ ratio = i / (count - 1)
+ event_ts = start_ts + ratio * span_s
+ row = dict(ev)
+ row["timestamp"] = datetime.fromtimestamp(event_ts).isoformat()
+ row["relative_s"] = round(event_ts - start_ts, 3)
+ timeline.append(row)
+ return timeline
+
+
+def _safe_read_text(path: str) -> str:
+ try:
+ with open(path, "r", encoding="utf-8", errors="ignore") as f:
+ return f.read()
+ except (OSError, PermissionError):
+ return ""
+
+
+def _read_proc_io_events(pid: int) -> list[dict]:
+ events = []
+ content = _safe_read_text(f"/proc/{pid}/io")
+ if not content:
+ return events
+ data = {}
+ for line in content.splitlines():
+ if ":" not in line:
+ continue
+ key, value = line.split(":", 1)
+ value = value.strip()
+ if value.isdigit():
+ data[key.strip()] = int(value)
+ read_bytes = int(data.get("read_bytes", 0) or 0)
+ write_bytes = int(data.get("write_bytes", 0) or 0)
+ if read_bytes > 0:
+ events.append({"type": "i/o", "name": "read_bytes", "bytes": read_bytes})
+ if write_bytes > 0:
+ events.append({"type": "i/o", "name": "write_bytes", "bytes": write_bytes})
+ return events
+
+
+def _read_proc_syscall_event(pid: int) -> dict | None:
+ content = _safe_read_text(f"/proc/{pid}/syscall").strip()
+ if not content or content in {"-1", "running"}:
+ return None
+ parts = content.split()
+ if not parts:
+ return None
+ try:
+ num = int(parts[0])
+ except ValueError:
+ return None
+ if num <= 0:
+ return None
+ return {"type": "syscall", "name": f"syscall_{num}", "count": 1}
+
+
+def _read_context_switch_event(pid: int) -> dict | None:
+ content = _safe_read_text(f"/proc/{pid}/status")
+ if not content:
+ return None
+ vol = 0
+ nonvol = 0
+ for line in content.splitlines():
+ if line.startswith("voluntary_ctxt_switches:"):
+ rhs = line.split(":", 1)[1].strip()
+ vol = int(rhs) if rhs.isdigit() else 0
+ elif line.startswith("nonvoluntary_ctxt_switches:"):
+ rhs = line.split(":", 1)[1].strip()
+ nonvol = int(rhs) if rhs.isdigit() else 0
+ total = vol + nonvol
+ if total <= 0:
+ return None
+ return {
+ "type": "context switch",
+ "name": "context_switches",
+ "count": total,
+ "voluntary": vol,
+ "nonvoluntary": nonvol,
+ }
+
+
+def _read_network_packet_event(pid: int) -> dict | None:
+ content = _safe_read_text(f"/proc/{pid}/net/dev")
+ if not content:
+ return None
+ rx_packets = 0
+ tx_packets = 0
+ for line in content.splitlines()[2:]:
+ if ":" not in line:
+ continue
+ _, rhs = line.split(":", 1)
+ cols = rhs.split()
+ if len(cols) < 10:
+ continue
+ try:
+ rx_packets += int(cols[1])
+ tx_packets += int(cols[9])
+ except ValueError:
+ continue
+ total_packets = rx_packets + tx_packets
+ if total_packets <= 0:
+ return None
+ return {
+ "type": "network packet",
+ "name": "net_packets",
+ "count": total_packets,
+ "rx_packets": rx_packets,
+ "tx_packets": tx_packets,
+ }
+
+
+def _read_interrupt_event() -> dict | None:
+ content = _safe_read_text("/proc/interrupts")
+ if not content:
+ return None
+ total = 0
+ for line in content.splitlines()[1:]:
+ if ":" not in line:
+ continue
+ _, rhs = line.split(":", 1)
+ for token in rhs.split():
+ if token.isdigit():
+ total += int(token)
+ if total <= 0:
+ return None
+ return {"type": "interrupt", "name": "interrupt_total", "count": total}
+
+
+def _read_scheduler_tick_event() -> dict | None:
+ content = _safe_read_text("/proc/stat")
+ if not content:
+ return None
+ for line in content.splitlines():
+ if line.startswith("ctxt "):
+ parts = line.split()
+ if len(parts) >= 2 and parts[1].isdigit():
+ return {"type": "scheduler tick", "name": "ctxt", "count": int(parts[1])}
+ return None
+
+
+def _read_lock_unlock_event(pid: int) -> dict | None:
+ content = _safe_read_text("/proc/locks")
+ if not content:
+ return None
+ pid_str = f" {pid} "
+ lock_count = 0
+ for line in content.splitlines():
+ if pid_str in f" {line} ":
+ lock_count += 1
+ if lock_count <= 0:
+ return None
+ return {"type": "lock/unlock", "name": "proc_locks", "count": lock_count}
+
+
+def get_proc_timeline_data(pid, window_s: int = 30):
"""Build timeline-like process events from /proc snapshots."""
if not pid:
raise ValueError("PID parameter required")
@@ -20,62 +198,122 @@ def get_proc_timeline_data(pid):
proc_info = proc.as_dict(["pid", "name", "create_time", "status"])
base_ts = float(proc_info["create_time"])
- ordered_events = [{"type": "exec", "pid": pid}]
+ window_s = _clamp_window_s(window_s)
+ ordered_events = [{"type": "exec", "name": "exec", "pid": pid}]
- try:
- maps_path = f"/proc/{pid}/maps"
- if os.path.exists(maps_path):
- with open(maps_path, "r", encoding="utf-8", errors="ignore") as f:
- map_count = len(f.readlines())
- if map_count > 0:
- ordered_events.append({"type": "mmap", "pid": pid, "count": map_count})
- except (IOError, PermissionError):
- pass
+ syscall_ev = _read_proc_syscall_event(pid)
+ if syscall_ev:
+ syscall_ev["pid"] = pid
+ ordered_events.append(syscall_ev)
- try:
- io_path = f"/proc/{pid}/io"
- if os.path.exists(io_path):
- with open(io_path, "r", encoding="utf-8", errors="ignore") as f:
- io_data = {}
- for line in f:
- if ":" in line:
- key, value = line.split(":", 1)
- io_data[key.strip()] = int(value.strip())
- if io_data.get("read_bytes", 0) > 0:
- ordered_events.append({"type": "read", "pid": pid, "bytes": io_data.get("read_bytes", 0)})
- if io_data.get("write_bytes", 0) > 0:
- ordered_events.append({"type": "write", "pid": pid, "bytes": io_data.get("write_bytes", 0)})
- except (IOError, PermissionError):
- pass
+ ctx_ev = _read_context_switch_event(pid)
+ if ctx_ev:
+ ctx_ev["pid"] = pid
+ ordered_events.append(ctx_ev)
- try:
- tcp_path = f"/proc/{pid}/net/tcp"
- if os.path.exists(tcp_path):
- with open(tcp_path, "r", encoding="utf-8", errors="ignore") as f:
- lines = f.readlines()
- if len(lines) > 1:
- for line in lines[1:]:
- parts = line.split()
- if len(parts) >= 4:
- state = parts[3]
- if state == "01":
- ordered_events.append({"type": "connect", "pid": pid})
- elif state == "0A":
- ordered_events.append({"type": "accept", "pid": pid})
- except (IOError, PermissionError):
- pass
-
- step = 0.35
- timeline = []
- for i, ev in enumerate(ordered_events):
- row = dict(ev)
- row["timestamp"] = datetime.fromtimestamp(base_ts + i * step).isoformat()
- timeline.append(row)
+ irq_ev = _read_interrupt_event()
+ if irq_ev:
+ irq_ev["pid"] = pid
+ ordered_events.append(irq_ev)
+
+ sched_ev = _read_scheduler_tick_event()
+ if sched_ev:
+ sched_ev["pid"] = pid
+ ordered_events.append(sched_ev)
+
+ for io_ev in _read_proc_io_events(pid):
+ io_ev["pid"] = pid
+ ordered_events.append(io_ev)
+
+ net_ev = _read_network_packet_event(pid)
+ if net_ev:
+ net_ev["pid"] = pid
+ ordered_events.append(net_ev)
+
+ lock_ev = _read_lock_unlock_event(pid)
+ if lock_ev:
+ lock_ev["pid"] = pid
+ ordered_events.append(lock_ev)
+
+ # Keep this event for backward compatibility with existing clients.
+ if proc_info.get("status"):
+ ordered_events.append({"type": "syscall", "name": "process_status", "pid": pid, "status": proc_info["status"]})
+
+ timeline = _stamp_events_with_time_window(ordered_events, base_ts, window_s)
return {
"timeline": timeline,
"pid": pid,
"name": proc_info.get("name", "unknown"),
"timestamp": datetime.now().isoformat(),
- "timeline_time_basis": "Events are ordered from process start; 0.35s steps separate rows for the helix (kernel does not expose per-event wall times for these signals).",
+ "window_s": window_s,
+ "timeline_time_basis": "Events are sampled from /proc and distributed over selected window as a relative timeline (not exact per-event kernel timestamps).",
+ }
+
+
+def get_proc_timeline_branches_data(limit: int = 6, events: int = 10, window_s: int = 30):
+ """Build process-branch timeline payload for multi-branch visualization."""
+ limit = max(1, min(12, int(limit)))
+ events = max(3, min(24, int(events)))
+ window_s = _clamp_window_s(window_s)
+
+ candidates = []
+ for proc in psutil.process_iter(["pid", "name", "cpu_percent", "memory_info"]):
+ try:
+ pid = int(proc.info.get("pid") or 0)
+ if pid <= 1:
+ continue
+ name = str(proc.info.get("name") or "").strip()
+ if not name:
+ continue
+ cpu = float(proc.info.get("cpu_percent") or 0.0)
+ mem_info = proc.info.get("memory_info")
+ mem_mb = float(getattr(mem_info, "rss", 0) or 0) / (1024 * 1024)
+ score = cpu * 2.0 + (mem_mb / 256.0)
+ candidates.append(
+ {
+ "pid": pid,
+ "name": name,
+ "cpu_percent": round(cpu, 2),
+ "memory_mb": round(mem_mb, 1),
+ "score": score,
+ }
+ )
+ except (psutil.Error, OSError, TypeError, ValueError):
+ continue
+
+ candidates.sort(key=lambda row: row.get("score", 0.0), reverse=True)
+ branches = []
+ for candidate in candidates[: max(limit * 4, limit)]:
+ if len(branches) >= limit:
+ break
+ pid = int(candidate["pid"])
+ try:
+ timeline_data = get_proc_timeline_data(pid, window_s=window_s)
+ except (ProcessLookupError, ValueError, psutil.Error, OSError):
+ continue
+ timeline = list(timeline_data.get("timeline") or [])[:events]
+ if not timeline:
+ continue
+ branches.append(
+ {
+ "pid": pid,
+ "name": str(timeline_data.get("name") or candidate["name"]),
+ "cpu_percent": float(candidate.get("cpu_percent") or 0.0),
+ "memory_mb": float(candidate.get("memory_mb") or 0.0),
+ "timeline": timeline,
+ "event_count": len(timeline),
+ }
+ )
+
+ return {
+ "branches": branches,
+ "timestamp": datetime.now().isoformat(),
+ "meta": {
+ "limit": limit,
+ "events_per_branch": events,
+ "window_s": window_s,
+ "branch_count": len(branches),
+ "mode": "multi-branch-v1",
+ },
}
diff --git a/requirements.txt b/requirements.txt
index fa2b8e5..81c01db 100755
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,5 +1,7 @@
Flask==3.1.2
+prometheus-client==0.21.1
psutil==7.0.0
openai==0.28.1
python-dotenv==1.0.0
gunicorn==21.2.0
+sentry-sdk
diff --git a/static/js/filesystem-map.js b/static/js/filesystem-map.js
index aad4fbb..daaba5b 100644
--- a/static/js/filesystem-map.js
+++ b/static/js/filesystem-map.js
@@ -21,6 +21,14 @@ class FilesystemMapVisualization {
this.legendNode = null;
this.orbHitArea = null;
this.canvasClickHandler = null;
+ this.canvasMouseMoveHandler = null;
+ this.zoneHitAreas = [];
+ this.selectedZoneId = null;
+ this.hoveredZoneId = null;
+ this.zoneSortMode = 'risk';
+ this.sortModeButton = null;
+ this.archMapMode = 'simple';
+ this.archModeButton = null;
}
init(containerId = 'filesystem-map-container') {
@@ -55,7 +63,9 @@ class FilesystemMapVisualization {
this.container.appendChild(this.canvas);
this.ctx = this.canvas.getContext('2d');
this.canvasClickHandler = (event) => this.onCanvasClick(event);
+ this.canvasMouseMoveHandler = (event) => this.onCanvasMouseMove(event);
this.canvas.addEventListener('click', this.canvasClickHandler);
+ this.canvas.addEventListener('mousemove', this.canvasMouseMoveHandler);
this.createOverlayUI();
this.addExitButton();
@@ -135,6 +145,50 @@ class FilesystemMapVisualization {
this.overlayNodes.push(modePanel);
this.setRenderMode(this.renderMode);
+ const sortBtn = document.createElement('button');
+ sortBtn.textContent = 'SORT: RISK';
+ sortBtn.style.cssText = `
+ position: absolute;
+ top: 72px;
+ left: 22px;
+ padding: 4px 8px;
+ background: rgba(12, 18, 28, 0.9);
+ border: 1px solid rgba(130, 148, 172, 0.35);
+ color: #9fb0c8;
+ font-family: 'Share Tech Mono', monospace;
+ font-size: 9px;
+ letter-spacing: 0.3px;
+ cursor: pointer;
+ z-index: 1001;
+ `;
+ sortBtn.onclick = () => this.cycleZoneSortMode();
+ this.container.appendChild(sortBtn);
+ this.overlayNodes.push(sortBtn);
+ this.sortModeButton = sortBtn;
+ this.updateSortModeButtonState();
+
+ const archBtn = document.createElement('button');
+ archBtn.textContent = 'ARCH: SIMPLE';
+ archBtn.style.cssText = `
+ position: absolute;
+ top: 72px;
+ left: 136px;
+ padding: 4px 8px;
+ background: rgba(12, 18, 28, 0.9);
+ border: 1px solid rgba(130, 148, 172, 0.35);
+ color: #9fb0c8;
+ font-family: 'Share Tech Mono', monospace;
+ font-size: 9px;
+ letter-spacing: 0.3px;
+ cursor: pointer;
+ z-index: 1001;
+ `;
+ archBtn.onclick = () => this.cycleArchMapMode();
+ this.container.appendChild(archBtn);
+ this.overlayNodes.push(archBtn);
+ this.archModeButton = archBtn;
+ this.updateArchMapModeButtonState();
+
const info = document.createElement('div');
info.style.cssText = `
position: absolute;
@@ -236,7 +290,7 @@ class FilesystemMapVisualization {
ctx.closePath();
}
- drawPackageStack(x, y, w, h, layers, accentColor, label, metaText) {
+ drawPackageStack(x, y, w, h, layers, accentColor, label, metaText, subMetaText, isSelected, isHovered) {
const ctx = this.ctx;
const stackLayers = Math.max(2, Math.min(4, layers));
for (let i = stackLayers - 1; i >= 0; i -= 1) {
@@ -245,16 +299,23 @@ class FilesystemMapVisualization {
this.drawRoundedRect(x + dx, y + dy, w, h, 5);
ctx.fillStyle = i === 0 ? 'rgba(14, 24, 36, 0.95)' : 'rgba(10, 16, 26, 0.85)';
ctx.fill();
- ctx.strokeStyle = i === 0 ? accentColor : 'rgba(72, 94, 124, 0.35)';
- ctx.lineWidth = i === 0 ? 1.0 : 0.7;
+ const baseStroke = i === 0 ? accentColor : 'rgba(72, 94, 124, 0.35)';
+ ctx.strokeStyle = isSelected ? 'rgba(149, 207, 255, 0.95)' : (isHovered ? 'rgba(122, 182, 245, 0.82)' : baseStroke);
+ ctx.lineWidth = isSelected ? 1.4 : (i === 0 ? 1.0 : 0.7);
ctx.stroke();
}
- ctx.fillStyle = '#9fc0e8';
+ ctx.fillStyle = isSelected ? '#d9ecff' : '#9fc0e8';
ctx.font = '9px "Share Tech Mono", monospace';
ctx.fillText(label, x + 8, y + 13);
- ctx.fillStyle = 'rgba(122, 141, 167, 0.85)';
+ ctx.fillStyle = isSelected ? 'rgba(190, 214, 241, 0.92)' : 'rgba(122, 141, 167, 0.85)';
ctx.font = '8px "Share Tech Mono", monospace';
ctx.fillText(metaText, x + 8, y + 24);
+ if (subMetaText) {
+ ctx.fillStyle = 'rgba(132, 156, 186, 0.76)';
+ ctx.fillText(subMetaText, x + 8, y + 34);
+ }
+ ctx.fillStyle = accentColor;
+ ctx.fillRect(x + 2, y + h - 3, Math.max(8, w - 4), 2);
}
drawParticleOrb(x, y, radius, meta) {
@@ -296,6 +357,166 @@ class FilesystemMapVisualization {
// Minimal version: no inner streaks and no labels.
}
+ drawFsArchitectureMap(x, y, w, h, meta, zones) {
+ const ctx = this.ctx;
+ const mode = this.archMapMode || 'simple';
+ const usedPercent = Math.max(0, Math.min(100, Number(meta?.used_percent || 0)));
+ const inodePressure = Math.max(0, Math.min(100, Number(meta?.inode_pressure || 0)));
+ const writingBlocks = Math.max(0, Number(meta?.writing_blocks || 0));
+ const writeBps = Math.max(0, Number(meta?.write_bps || 0));
+ const writeMBs = writeBps / (1024 * 1024);
+ const zoneList = Array.isArray(zones) ? zones : [];
+ const activeZones = zoneList.filter((z) => Number(z?.activity || 0) > 8 || Number(z?.writing_blocks || 0) > 0).length;
+ const hotZones = zoneList.filter((z) => Math.max(Number(z?.used_percent || 0), Number(z?.inode_pressure || 0)) >= 80).length;
+
+ this.drawRoundedRect(x, y, w, h, 9);
+ ctx.fillStyle = 'rgba(8, 13, 20, 0.78)';
+ ctx.fill();
+ ctx.strokeStyle = 'rgba(122, 147, 179, 0.34)';
+ ctx.lineWidth = 0.95;
+ ctx.stroke();
+
+ ctx.fillStyle = '#cde3ff';
+ ctx.font = '10px "Share Tech Mono", monospace';
+ ctx.fillText('LINUX FS ARCH MAP', x + 12, y + 18);
+ ctx.fillStyle = 'rgba(151, 173, 203, 0.86)';
+ ctx.font = '8px "Share Tech Mono", monospace';
+ ctx.fillText(`active zones ${activeZones} | hot zones ${hotZones} | write ${writeMBs.toFixed(1)} MB/s`, x + 12, y + 32);
+ ctx.fillText(`mode ${String(mode).toUpperCase()}`, x + w - 102, y + 18);
+
+ const node = (nx, ny, nw, nh, title, sub, tone, small = false) => {
+ this.drawRoundedRect(nx, ny, nw, nh, small ? 4 : 5);
+ ctx.fillStyle = 'rgba(10, 16, 24, 0.82)';
+ ctx.fill();
+ ctx.strokeStyle = tone;
+ ctx.lineWidth = small ? 0.85 : 0.95;
+ ctx.stroke();
+ ctx.fillStyle = '#cfe4ff';
+ ctx.font = small ? '8px "Share Tech Mono", monospace' : '9px "Share Tech Mono", monospace';
+ ctx.fillText(title, nx + 6, ny + (small ? 11 : 13));
+ if (sub) {
+ ctx.fillStyle = 'rgba(146, 170, 201, 0.88)';
+ ctx.font = '8px "Share Tech Mono", monospace';
+ ctx.fillText(sub, nx + 6, ny + (small ? 21 : 24));
+ }
+ };
+ const link = (x0, y0, x1, y1, tone, weight = 1) => {
+ ctx.beginPath();
+ ctx.moveTo(x0, y0);
+ ctx.bezierCurveTo((x0 + x1) / 2, y0, (x0 + x1) / 2, y1, x1, y1);
+ ctx.strokeStyle = tone;
+ ctx.lineWidth = weight;
+ ctx.stroke();
+ };
+
+ const rootX = x + 168;
+ const rootY = y + 98;
+ const syscallX = x + 12;
+ const driverX = x + w - 126;
+ const blockX = x + 152;
+ const ioX = blockX + 122;
+ const devX = ioX + 102;
+ const writeTone = writeMBs >= 80 ? 'rgba(255, 154, 118, 0.86)' : 'rgba(142, 227, 255, 0.78)';
+
+ node(syscallX, rootY - 16, 92, 34, 'SYSCALLS', `rw ${writeMBs.toFixed(1)} MB/s`, 'rgba(142, 206, 255, 0.85)');
+ node(rootX, rootY - 22, 110, 44, 'VFS CORE', `used ${usedPercent.toFixed(1)}%`, 'rgba(132, 194, 255, 0.95)');
+ node(driverX, rootY - 48, 104, 30, 'EXT4/XFS', `${activeZones} active`, 'rgba(145, 226, 255, 0.88)', true);
+ node(driverX, rootY - 12, 104, 30, 'TMPFS/PROC', `inode ${inodePressure.toFixed(0)}%`, 'rgba(148, 198, 255, 0.84)', true);
+ node(driverX, rootY + 24, 104, 30, 'OVERLAY/DM', `${writingBlocks} writing`, writingBlocks > 0 ? 'rgba(255, 164, 170, 0.86)' : 'rgba(142, 186, 230, 0.72)', true);
+ node(blockX, y + h - 58, 102, 32, 'PAGECACHE', `dirty ${writingBlocks}`, writeTone, true);
+ node(ioX, y + h - 58, 94, 32, 'BLOCK IO', `inode ${inodePressure.toFixed(0)}%`, 'rgba(132, 194, 255, 0.84)', true);
+ node(devX, y + h - 58, 86, 32, 'NVME', `util ${usedPercent.toFixed(0)}%`, usedPercent > 86 ? 'rgba(255, 146, 156, 0.88)' : 'rgba(143, 217, 252, 0.85)', true);
+
+ const flowWeight = 0.9 + Math.min(1.8, writeMBs / 70);
+ link(syscallX + 92, rootY + 1, rootX, rootY + 1, 'rgba(124, 181, 234, 0.65)', 1.1);
+ link(rootX + 110, rootY - 2, driverX, rootY - 33, 'rgba(130, 199, 245, 0.52)', 0.9);
+ link(rootX + 110, rootY + 4, driverX, rootY + 3, 'rgba(130, 199, 245, 0.52)', 0.9);
+ link(rootX + 110, rootY + 10, driverX, rootY + 39, 'rgba(130, 199, 245, 0.52)', 0.9);
+ link(rootX + 56, rootY + 22, blockX + 50, y + h - 58, writeTone, flowWeight);
+ link(blockX + 102, y + h - 42, ioX, y + h - 42, 'rgba(134, 196, 243, 0.75)', flowWeight * 0.9);
+ link(ioX + 94, y + h - 42, devX, y + h - 42, usedPercent > 86 ? 'rgba(255, 152, 162, 0.88)' : 'rgba(142, 217, 252, 0.8)', flowWeight * 0.8);
+
+ if (mode === 'detailed') {
+ const dentryX = rootX - 4;
+ const dentryY = y + 48;
+ const journalX = driverX - 12;
+ const journalY = y + h - 94;
+ const wbX = blockX - 4;
+ const wbY = y + h - 94;
+ const schedX = ioX + 2;
+ const schedY = y + h - 94;
+
+ node(dentryX, dentryY, 88, 26, 'DENTRY', `${Math.max(55, 99 - inodePressure * 0.5).toFixed(0)}% hit`, 'rgba(137, 206, 255, 0.78)', true);
+ node(dentryX + 92, dentryY, 88, 26, 'INODE', `${Math.max(48, 98 - usedPercent * 0.48).toFixed(0)}% hit`, 'rgba(132, 194, 255, 0.78)', true);
+ node(journalX, journalY, 94, 26, 'JOURNAL', `${writingBlocks} pend`, writingBlocks > 0 ? 'rgba(255, 167, 136, 0.85)' : 'rgba(132, 180, 220, 0.72)', true);
+ node(wbX, wbY, 86, 26, 'WRITEBACK', `${(writeMBs * 0.72).toFixed(1)} MB/s`, writeTone, true);
+ node(schedX, schedY, 82, 26, 'IO SCHED', inodePressure > 72 ? 'congested' : 'normal', inodePressure > 72 ? 'rgba(255, 158, 166, 0.86)' : 'rgba(138, 198, 244, 0.8)', true);
+
+ link(rootX + 60, rootY - 22, dentryX + 20, dentryY + 13, 'rgba(130, 197, 246, 0.55)', 0.9);
+ link(rootX + 84, rootY - 22, dentryX + 110, dentryY + 13, 'rgba(130, 197, 246, 0.55)', 0.9);
+ link(driverX + 52, rootY + 39, journalX + 40, journalY + 13, 'rgba(214, 148, 137, 0.46)', 0.9);
+ link(journalX + 94, journalY + 13, wbX, wbY + 13, 'rgba(182, 156, 132, 0.45)', 0.8);
+ link(wbX + 86, wbY + 13, schedX, schedY + 13, 'rgba(132, 194, 247, 0.62)', 0.85);
+ }
+ }
+
+ drawLiveBlockMatrix(x, y, w, h, blocks, zones, meta) {
+ const ctx = this.ctx;
+ const zoneMap = new Map((Array.isArray(zones) ? zones : []).map((z) => [String(z.id || ''), z]));
+ const data = Array.isArray(blocks) ? blocks : [];
+ if (!data.length) return;
+
+ this.drawRoundedRect(x, y, w, h, 7);
+ ctx.fillStyle = 'rgba(8, 13, 21, 0.72)';
+ ctx.fill();
+ ctx.strokeStyle = 'rgba(122, 146, 178, 0.28)';
+ ctx.lineWidth = 0.85;
+ ctx.stroke();
+
+ const cols = 64;
+ const rows = 20;
+ const innerX = x + 10;
+ const innerY = y + 12;
+ const innerW = w - 20;
+ const innerH = h - 26;
+ const cellW = innerW / cols;
+ const cellH = innerH / rows;
+ const inodePressure = Math.max(0, Math.min(100, Number(meta?.inode_pressure || 0)));
+ const writeHot = Number(meta?.write_bps || 0) / (1024 * 1024);
+
+ for (let i = 0; i < rows * cols; i += 1) {
+ const b = data[i % data.length] || {};
+ const zone = zoneMap.get(String(b.zone_id || '')) || {};
+ const zx = i % cols;
+ const zy = Math.floor(i / cols);
+ const px = innerX + zx * cellW;
+ const py = innerY + zy * cellH;
+ const zoneAct = Math.max(0, Math.min(100, Number(zone.activity || 0)));
+ const zInode = Math.max(0, Math.min(100, Number(zone.inode_pressure || 0)));
+ const flicker = 0.75 + 0.25 * Math.sin(this.tick * 0.05 + zx * 0.17 + zy * 0.23);
+
+ if (b.state === 'writing') {
+ const alpha = 0.46 + Math.min(0.5, (zoneAct / 130) + (writeHot > 90 ? 0.18 : 0)) * flicker;
+ ctx.fillStyle = `rgba(163, 238, 255, ${Math.min(0.95, alpha).toFixed(3)})`;
+ } else if (b.state === 'used') {
+ const alpha = 0.22 + Math.min(0.5, ((zoneAct + inodePressure * 0.35) / 220)) * flicker;
+ ctx.fillStyle = zInode >= 78
+ ? `rgba(255, 184, 132, ${Math.min(0.8, alpha + 0.08).toFixed(3)})`
+ : `rgba(96, 166, 220, ${Math.min(0.8, alpha).toFixed(3)})`;
+ } else {
+ ctx.fillStyle = `rgba(17, 30, 46, ${(0.24 + 0.08 * flicker).toFixed(3)})`;
+ }
+
+ const cw = Math.max(1.5, cellW - 1.15);
+ const ch = Math.max(1.5, cellH - 1.1);
+ ctx.fillRect(px, py, cw, ch);
+ }
+
+ ctx.fillStyle = 'rgba(166, 193, 225, 0.84)';
+ ctx.font = '8px "Share Tech Mono", monospace';
+ ctx.fillText('live block matrix: write glow / inode pressure / occupancy', x + 12, y + h - 8);
+ }
+
setRenderMode(modeKey) {
this.renderMode = ['occupancy', 'write', 'inode'].includes(modeKey) ? modeKey : 'occupancy';
this.modeButtons.forEach((btn, key) => {
@@ -322,11 +543,82 @@ class FilesystemMapVisualization {
this.setRenderMode(next);
}
+ getZoneSortScore(zone, mode) {
+ const used = Number(zone?.used_percent || 0);
+ const inode = Number(zone?.inode_pressure || 0);
+ const activity = Number(zone?.activity || 0);
+ const writing = Number(zone?.writing_blocks || 0);
+ if (mode === 'write') {
+ return writing * 100 + activity * 4 + used * 0.2;
+ }
+ if (mode === 'inode') {
+ return inode * 10 + used * 0.6 + activity * 0.5;
+ }
+ return Math.max(used, inode, writing > 0 ? 86 : 0) * 10 + activity;
+ }
+
+ cycleZoneSortMode() {
+ const order = ['risk', 'write', 'inode'];
+ const idx = order.indexOf(this.zoneSortMode);
+ const next = order[(idx + 1) % order.length];
+ this.zoneSortMode = next;
+ this.updateSortModeButtonState();
+ }
+
+ updateSortModeButtonState() {
+ if (!this.sortModeButton) return;
+ const mode = this.zoneSortMode || 'risk';
+ const label = mode === 'write' ? 'WRITE' : (mode === 'inode' ? 'INODE' : 'RISK');
+ this.sortModeButton.textContent = `SORT: ${label}`;
+ if (mode === 'write') {
+ this.sortModeButton.style.background = 'rgba(45, 57, 30, 0.92)';
+ this.sortModeButton.style.borderColor = 'rgba(168, 230, 140, 0.85)';
+ this.sortModeButton.style.color = '#e4ffd3';
+ } else if (mode === 'inode') {
+ this.sortModeButton.style.background = 'rgba(29, 43, 62, 0.92)';
+ this.sortModeButton.style.borderColor = 'rgba(127, 194, 255, 0.88)';
+ this.sortModeButton.style.color = '#dbf0ff';
+ } else {
+ this.sortModeButton.style.background = 'rgba(62, 36, 36, 0.92)';
+ this.sortModeButton.style.borderColor = 'rgba(255, 162, 170, 0.88)';
+ this.sortModeButton.style.color = '#ffe1e5';
+ }
+ }
+
+ cycleArchMapMode() {
+ this.archMapMode = this.archMapMode === 'detailed' ? 'simple' : 'detailed';
+ this.updateArchMapModeButtonState();
+ }
+
+ updateArchMapModeButtonState() {
+ if (!this.archModeButton) return;
+ const detailed = this.archMapMode === 'detailed';
+ this.archModeButton.textContent = detailed ? 'ARCH: DETAILED' : 'ARCH: SIMPLE';
+ this.archModeButton.style.background = detailed ? 'rgba(28, 45, 64, 0.92)' : 'rgba(12, 18, 28, 0.9)';
+ this.archModeButton.style.borderColor = detailed ? 'rgba(127, 194, 255, 0.88)' : 'rgba(130, 148, 172, 0.35)';
+ this.archModeButton.style.color = detailed ? '#e0f1ff' : '#9fb0c8';
+ }
+
+ getZoneHitAt(x, y) {
+ for (const hit of this.zoneHitAreas) {
+ if (x >= hit.x && x <= hit.x + hit.w && y >= hit.y && y <= hit.y + hit.h) {
+ return hit;
+ }
+ }
+ return null;
+ }
+
onCanvasClick(event) {
- if (!this.orbHitArea || !this.canvas) return;
+ if (!this.canvas) return;
const rect = this.canvas.getBoundingClientRect();
const x = event.clientX - rect.left;
const y = event.clientY - rect.top;
+ const zoneHit = this.getZoneHitAt(x, y);
+ if (zoneHit) {
+ this.selectedZoneId = this.selectedZoneId === zoneHit.zoneId ? null : zoneHit.zoneId;
+ return;
+ }
+ if (!this.orbHitArea) return;
const dx = x - this.orbHitArea.x;
const dy = y - this.orbHitArea.y;
const dist = Math.sqrt(dx * dx + dy * dy);
@@ -335,6 +627,17 @@ class FilesystemMapVisualization {
}
}
+ onCanvasMouseMove(event) {
+ if (!this.canvas) return;
+ const rect = this.canvas.getBoundingClientRect();
+ const x = event.clientX - rect.left;
+ const y = event.clientY - rect.top;
+ const zoneHit = this.getZoneHitAt(x, y);
+ this.hoveredZoneId = zoneHit ? zoneHit.zoneId : null;
+ const overOrb = this.orbHitArea && Math.hypot(x - this.orbHitArea.x, y - this.orbHitArea.y) <= this.orbHitArea.r * 1.25;
+ this.canvas.style.cursor = zoneHit || overOrb ? 'pointer' : 'default';
+ }
+
drawScene() {
if (!this.ctx || !this.canvas) return;
const w = window.innerWidth;
@@ -352,6 +655,7 @@ class FilesystemMapVisualization {
const blocks = this.telemetry.blocks;
const zones = Array.isArray(this.telemetry.zones) ? this.telemetry.zones : [];
const zoneMap = new Map(zones.map((z) => [String(z.id || ''), z]));
+ this.zoneHitAreas = [];
const cx = Math.floor(w * 0.53);
const cy = Math.floor(h * 0.63);
@@ -359,6 +663,41 @@ class FilesystemMapVisualization {
const innerR = Math.max(24, Math.floor(outerR * 0.14));
const ringGap = (outerR - innerR) / Math.max(1, rows);
const angleStep = (Math.PI * 2) / Math.max(1, cols);
+ const m = this.telemetry.meta || {};
+
+ const kpis = [
+ {
+ label: 'USED',
+ value: `${Number(m.used_percent || 0).toFixed(1)}%`,
+ tone: Number(m.used_percent || 0) >= 85 ? '#ff9ca6' : '#8fd8ff'
+ },
+ {
+ label: 'WRITE',
+ value: `${(Number(m.write_bps || 0) / (1024 * 1024)).toFixed(1)} MB/s`,
+ tone: Number(m.write_bps || 0) >= (80 * 1024 * 1024) ? '#ffd08d' : '#8ff3c0'
+ },
+ {
+ label: 'INODE',
+ value: `${Number(m.inode_pressure || 0).toFixed(1)}%`,
+ tone: Number(m.inode_pressure || 0) >= 75 ? '#ff9ca6' : '#8fd8ff'
+ }
+ ];
+ kpis.forEach((kpi, idx) => {
+ const kx = 26 + idx * 152;
+ const ky = 98;
+ this.drawRoundedRect(kx, ky, 138, 44, 6);
+ this.ctx.fillStyle = 'rgba(10, 16, 24, 0.74)';
+ this.ctx.fill();
+ this.ctx.strokeStyle = 'rgba(122, 142, 168, 0.32)';
+ this.ctx.lineWidth = 0.9;
+ this.ctx.stroke();
+ this.ctx.fillStyle = 'rgba(159, 177, 201, 0.84)';
+ this.ctx.font = '8px "Share Tech Mono", monospace';
+ this.ctx.fillText(kpi.label, kx + 10, ky + 14);
+ this.ctx.fillStyle = kpi.tone;
+ this.ctx.font = '12px "Share Tech Mono", monospace';
+ this.ctx.fillText(kpi.value, kx + 10, ky + 31);
+ });
// Background circuit lines.
this.ctx.strokeStyle = 'rgba(150, 38, 64, 0.17)';
@@ -371,6 +710,12 @@ class FilesystemMapVisualization {
this.ctx.stroke();
}
+ const matrixW = Math.min(760, Math.max(520, Math.floor(w * 0.54)));
+ const matrixH = Math.min(320, Math.max(220, Math.floor(h * 0.28)));
+ const matrixX = Math.max(24, Math.min(w - matrixW - 24, cx - Math.floor(matrixW * 0.52)));
+ const matrixY = Math.max(118, cy - outerR - Math.floor(matrixH * 0.45));
+ this.drawLiveBlockMatrix(matrixX, matrixY, matrixW, matrixH, blocks, zones, m);
+
// Main rings.
for (let i = 0; i <= 7; i += 1) {
const r = innerR + ((outerR - innerR) * i / 7);
@@ -420,7 +765,6 @@ class FilesystemMapVisualization {
this.ctx.fill();
// Alert banner.
- const m = this.telemetry.meta || {};
if (Number(m.writing_blocks || 0) > 0) {
const pulse = 0.5 + 0.5 * Math.sin(this.tick * 0.08);
const bw = 330;
@@ -439,21 +783,33 @@ class FilesystemMapVisualization {
}
// Side package stacks from zones.
- const zoneItems = zones.slice(0, 14);
+ const sortedZones = zones
+ .slice()
+ .sort((a, b) => this.getZoneSortScore(b, this.zoneSortMode) - this.getZoneSortScore(a, this.zoneSortMode));
+ const zoneItems = sortedZones.slice(0, 14);
const leftItems = zoneItems.filter((_, i) => i % 2 === 0);
const rightItems = zoneItems.filter((_, i) => i % 2 === 1);
- const cardW = 90;
- const cardH = 32;
+ const cardW = 130;
+ const cardH = 42;
const leftX = Math.max(24, cx - outerR - 220);
const rightX = Math.min(w - cardW - 24, cx + outerR + 110);
const topY = Math.max(150, cy - outerR + 12);
const drawZonePack = (list, xBase, isLeft) => {
list.forEach((z, idx) => {
- const y = topY + idx * 42;
- const active = Number(z.activity || 0) > 6 || Number(z.writing_blocks || 0) > 0;
- const accent = active ? 'rgba(118, 220, 255, 0.92)' : 'rgba(90, 132, 178, 0.7)';
+ const y = topY + idx * 50;
+ const usedPercent = Number(z.used_percent || 0);
+ const writingBlocks = Number(z.writing_blocks || 0);
+ const inodePressure = Number(z.inode_pressure || 0);
+ const riskScore = Math.max(usedPercent, inodePressure, writingBlocks > 0 ? 86 : 0);
+ const active = Number(z.activity || 0) > 6 || writingBlocks > 0;
+ const accent = riskScore >= 80
+ ? 'rgba(255, 149, 158, 0.92)'
+ : (riskScore >= 55 ? 'rgba(255, 206, 127, 0.9)' : 'rgba(118, 220, 255, 0.92)');
const layers = 2 + (active ? 2 : 1);
+ const zoneId = String(z.id || z.name || `zone-${idx}`);
+ const isSelected = this.selectedZoneId === zoneId;
+ const isHovered = this.hoveredZoneId === zoneId;
this.drawPackageStack(
xBase,
y,
@@ -462,8 +818,12 @@ class FilesystemMapVisualization {
layers,
accent,
String(z.name || z.id || 'zone'),
- `used:${Number(z.used_percent || 0).toFixed(0)}%`
+ `used ${usedPercent.toFixed(0)}% | inode ${inodePressure.toFixed(0)}%`,
+ `wr ${writingBlocks} | act ${Number(z.activity || 0).toFixed(0)}`,
+ isSelected,
+ isHovered
);
+ this.zoneHitAreas.push({ x: xBase, y, w: cardW, h: cardH, zoneId });
// Connector to central disk.
const sx = isLeft ? xBase + cardW : xBase;
@@ -480,8 +840,10 @@ class FilesystemMapVisualization {
tx,
ty
);
- this.ctx.strokeStyle = 'rgba(169, 52, 80, 0.26)';
- this.ctx.lineWidth = 1;
+ this.ctx.strokeStyle = isSelected
+ ? 'rgba(146, 204, 255, 0.86)'
+ : (riskScore >= 80 ? 'rgba(226, 92, 106, 0.44)' : 'rgba(169, 52, 80, 0.26)');
+ this.ctx.lineWidth = isSelected ? 1.5 : 1;
this.ctx.stroke();
});
};
@@ -501,6 +863,39 @@ class FilesystemMapVisualization {
this.ctx.fillText(`HOST/DATA FILE ${(m.used_gb || 0).toFixed(0)}GB`, leftX, topY - 26);
this.ctx.fillText(`HOST/DATA FILE ${(m.free_gb || 0).toFixed(0)}GB`, leftX, topY - 10);
this.ctx.fillText(`HOST/DATA WR ${(m.write_bps || 0).toFixed(0)} B/s`, rightX - 40, topY - 10);
+ this.ctx.fillStyle = 'rgba(164, 183, 209, 0.84)';
+ this.ctx.fillText(`zones sorted by ${String(this.zoneSortMode || 'risk').toUpperCase()}`, leftX, topY - 42);
+
+ const archW = Math.min(620, Math.max(470, Math.floor(w * 0.44)));
+ const archH = 228;
+ const archX = Math.max(24, Math.min(w - archW - 24, rightX - 120));
+ const archY = 96;
+ this.drawFsArchitectureMap(archX, archY, archW, archH, m, zones);
+
+ const selectedZone = zones.find((z) => String(z.id || z.name || '') === String(this.selectedZoneId || ''));
+ if (selectedZone) {
+ const panelW = 328;
+ const panelH = 96;
+ const px = 24;
+ const py = h - panelH - 26;
+ this.drawRoundedRect(px, py, panelW, panelH, 8);
+ this.ctx.fillStyle = 'rgba(9, 14, 22, 0.86)';
+ this.ctx.fill();
+ this.ctx.strokeStyle = 'rgba(132, 164, 202, 0.42)';
+ this.ctx.lineWidth = 1;
+ this.ctx.stroke();
+ this.ctx.fillStyle = '#cfe5ff';
+ this.ctx.font = '11px "Share Tech Mono", monospace';
+ this.ctx.fillText(`ZONE FOCUS: ${String(selectedZone.name || selectedZone.id || 'zone').toUpperCase()}`, px + 12, py + 20);
+ this.ctx.fillStyle = 'rgba(165, 187, 217, 0.9)';
+ this.ctx.font = '9px "Share Tech Mono", monospace';
+ this.ctx.fillText(`used ${Number(selectedZone.used_percent || 0).toFixed(1)}%`, px + 12, py + 40);
+ this.ctx.fillText(`inode ${Number(selectedZone.inode_pressure || 0).toFixed(1)}%`, px + 12, py + 56);
+ this.ctx.fillText(`activity ${Number(selectedZone.activity || 0).toFixed(1)}`, px + 12, py + 72);
+ this.ctx.fillText(`writing blocks ${Number(selectedZone.writing_blocks || 0)}`, px + 170, py + 40);
+ this.ctx.fillText(`free ${(100 - Number(selectedZone.used_percent || 0)).toFixed(1)}%`, px + 170, py + 56);
+ this.ctx.fillText('click card again to clear focus', px + 170, py + 72);
+ }
}
animate() {
diff --git a/static/js/network-stack.js b/static/js/network-stack.js
index 1ce67d8..07cbeca 100644
--- a/static/js/network-stack.js
+++ b/static/js/network-stack.js
@@ -41,6 +41,40 @@ class NetworkStackVisualization {
nic: 0.2
};
this.layerActivityTarget = { ...this.layerActivity };
+ this.layerSemanticNoiseTarget = {
+ userspace: { stress: 0.2, jitter: 0.2, branch: 0.2 },
+ socket: { stress: 0.2, jitter: 0.2, branch: 0.2 },
+ tcp: { stress: 0.2, jitter: 0.2, branch: 0.2 },
+ ip: { stress: 0.2, jitter: 0.2, branch: 0.2 },
+ netfilter: { stress: 0.2, jitter: 0.2, branch: 0.2 },
+ driver: { stress: 0.2, jitter: 0.2, branch: 0.2 },
+ nic: { stress: 0.2, jitter: 0.2, branch: 0.2 }
+ };
+ this.layerSemanticNoise = {
+ userspace: { stress: 0.2, jitter: 0.2, branch: 0.2 },
+ socket: { stress: 0.2, jitter: 0.2, branch: 0.2 },
+ tcp: { stress: 0.2, jitter: 0.2, branch: 0.2 },
+ ip: { stress: 0.2, jitter: 0.2, branch: 0.2 },
+ netfilter: { stress: 0.2, jitter: 0.2, branch: 0.2 },
+ driver: { stress: 0.2, jitter: 0.2, branch: 0.2 },
+ nic: { stress: 0.2, jitter: 0.2, branch: 0.2 }
+ };
+ this.layerBranchSignalTarget = {};
+ this.layerBranchSignalCurrent = {};
+ this.layerBranchMetricTarget = {};
+ this.layerBranchMetricCurrent = {};
+ this.layerBranchMetricDelta = {};
+ this.layerBranchMetricHistory = {};
+ this.branchDeltaWindowMs = 6000;
+ this.layerSignalLabels = {
+ userspace: ['proc', 'wakeup', 'syscall'],
+ socket: ['accept', 'queue', 'retrans'],
+ tcp: ['cwnd', 'rtt', 'retrans'],
+ ip: ['in/out', 'route', 'ttl'],
+ netfilter: ['preroute', 'drop', 'conntrack'],
+ driver: ['txq', 'drops', 'irq'],
+ nic: ['rx err', 'tx err', 'dma']
+ };
this.lineParticles = [];
this.microPackets = [];
this.metricChips = {};
@@ -49,14 +83,20 @@ class NetworkStackVisualization {
this.chipLayerNode = null;
this.viewModeButton = null;
this.puzzleModeButton = null;
+ this.noiseModeButton = null;
+ this.readModeButton = null;
this.viewDensityMode = 'detailed';
this.puzzleDetailMode = 'overview';
+ this.noiseDetailMode = 'dense';
+ this.readMode = 'forensics';
this.galaxyPanelNode = null;
this.galaxyNodes = {};
this.galaxyExplainNode = null;
this.selectedGalaxy = 'state';
this.galaxyStateData = null;
this.lifecyclePanelNode = null;
+ this.hideOsiTiles = true;
+ this.hideVerticalOrbs = true;
this.packetLifecycleStages = [
'NIC RX',
'IRQ',
@@ -234,7 +274,7 @@ class NetworkStackVisualization {
const material = new THREE.MeshPhongMaterial({
color: def.color,
transparent: true,
- opacity: 0.28,
+ opacity: 0.06,
shininess: 60,
emissive: new THREE.Color(0x58b6d8),
emissiveIntensity: 0.02
@@ -245,7 +285,7 @@ class NetworkStackVisualization {
const edge = new THREE.LineSegments(
new THREE.EdgesGeometry(new THREE.BoxGeometry(layerWidth, layerHeight, layerDepth)),
- new THREE.LineBasicMaterial({ color: 0x9aa2aa, transparent: true, opacity: 0.35 })
+ new THREE.LineBasicMaterial({ color: 0x9aa2aa, transparent: true, opacity: 0.22 })
);
edge.position.copy(mesh.position);
this.scene.add(edge);
@@ -262,10 +302,18 @@ class NetworkStackVisualization {
strip.position.set(0, def.y, layerDepth * 0.34);
this.scene.add(strip);
+ const noiseRig = this.createLayerNoiseRig(def, layerDepth);
+
mesh.userData.layerId = def.id;
this.layerMeshes.push(mesh);
- this.layers.push({ ...def, mesh, edge, strip });
+ this.layers.push({ ...def, mesh, edge, strip, noiseRig });
this.layerMap[def.id] = def.y;
+
+ if (this.hideOsiTiles) {
+ mesh.visible = false;
+ edge.visible = false;
+ strip.visible = false;
+ }
});
const flowLine = new THREE.Line(
@@ -278,6 +326,324 @@ class NetworkStackVisualization {
this.scene.add(flowLine);
}
+ createRodBetween(start, end, radius = 0.018, color = 0x8d97a3, opacity = 0.32) {
+ const dir = new THREE.Vector3().subVectors(end, start);
+ const len = dir.length();
+ if (len <= 0.001) return null;
+ const midpoint = new THREE.Vector3().addVectors(start, end).multiplyScalar(0.5);
+ const geom = new THREE.CylinderGeometry(radius, radius, len, 8, 1, false);
+ const mat = new THREE.MeshBasicMaterial({
+ color,
+ transparent: true,
+ opacity
+ });
+ const rod = new THREE.Mesh(geom, mat);
+ rod.position.copy(midpoint);
+ rod.quaternion.setFromUnitVectors(new THREE.Vector3(0, 1, 0), dir.clone().normalize());
+ this.scene.add(rod);
+ return rod;
+ }
+
+ createSignalTagSprite(initialText = 'sig --') {
+ const canvas = document.createElement('canvas');
+ const ctx = canvas.getContext('2d');
+ if (!ctx) return null;
+ canvas.width = 292;
+ canvas.height = 76;
+ const texture = new THREE.CanvasTexture(canvas);
+ texture.needsUpdate = true;
+ const material = new THREE.SpriteMaterial({
+ map: texture,
+ transparent: true,
+ depthTest: false,
+ depthWrite: false
+ });
+ const sprite = new THREE.Sprite(material);
+ sprite.scale.set(1.2, 0.29, 1);
+ sprite.renderOrder = 2;
+ const tag = { sprite, canvas, ctx, texture, lastText: '', lastIntensity: -1 };
+ this.updateSignalTagSprite(tag, initialText, 0.2, 0.0);
+ this.scene.add(sprite);
+ return tag;
+ }
+
+ updateSignalTagSprite(tag, text, intensity = 0.2, jitter = 0) {
+ if (!tag || !tag.ctx || !tag.canvas) return;
+ const safeText = String(text || 'sig --');
+ const safeIntensity = Math.max(0, Math.min(1, Number(intensity) || 0));
+ const safeJitter = Math.max(0, Math.min(1, Number(jitter) || 0));
+ const changed = Math.abs(safeIntensity - (tag.lastIntensity ?? -1)) > 0.04 || safeText !== tag.lastText;
+ if (!changed) return;
+
+ const ctx = tag.ctx;
+ const w = tag.canvas.width;
+ const h = tag.canvas.height;
+ ctx.clearRect(0, 0, w, h);
+
+ const bgAlpha = 0.46 + safeIntensity * 0.34;
+ const strokeAlpha = 0.5 + safeIntensity * 0.44;
+ ctx.fillStyle = `rgba(8, 13, 20, ${bgAlpha})`;
+ ctx.strokeStyle = `rgba(176, 228, 255, ${strokeAlpha})`;
+ ctx.lineWidth = 1.35;
+ if (typeof ctx.roundRect === 'function') {
+ ctx.beginPath();
+ ctx.roundRect(0.5, 0.5, w - 1, h - 1, 6);
+ ctx.fill();
+ ctx.stroke();
+ } else {
+ ctx.fillRect(0, 0, w, h);
+ ctx.strokeRect(0.5, 0.5, w - 1, h - 1);
+ }
+
+ const lines = safeText.split('\n').slice(0, 2);
+ ctx.font = '14px "Share Tech Mono", monospace';
+ ctx.textBaseline = 'middle';
+ ctx.fillStyle = `rgba(228, 246, 255, ${0.9 + safeIntensity * 0.1})`;
+ ctx.strokeStyle = `rgba(12, 18, 26, ${0.75 + safeIntensity * 0.2})`;
+ ctx.lineWidth = 2.6;
+ ctx.shadowColor = `rgba(120, 210, 255, ${0.2 + safeIntensity * 0.4})`;
+ ctx.shadowBlur = 6;
+ if (lines.length === 1) {
+ ctx.strokeText(lines[0], 14, h / 2);
+ ctx.fillText(lines[0], 14, h / 2);
+ } else {
+ ctx.strokeText(lines[0], 14, h * 0.35);
+ ctx.fillText(lines[0], 14, h * 0.35);
+ ctx.strokeText(lines[1], 14, h * 0.74);
+ ctx.fillText(lines[1], 14, h * 0.74);
+ }
+ ctx.shadowBlur = 0;
+
+ if (safeJitter > 0.02) {
+ const speckCount = Math.floor(4 + safeJitter * 11);
+ for (let i = 0; i < speckCount; i++) {
+ const x = Math.random() * w;
+ const y = Math.random() * h;
+ const a = 0.05 + Math.random() * 0.14 * safeJitter;
+ ctx.fillStyle = `rgba(230, 193, 90, ${a})`;
+ ctx.fillRect(x, y, 1, 1);
+ }
+ }
+
+ tag.texture.needsUpdate = true;
+ tag.lastText = safeText;
+ tag.lastIntensity = safeIntensity;
+ }
+
+ formatBranchMetric(layerId, signalIdx, value, deltaValue = null, withDelta = false) {
+ const v = Number(value);
+ const safe = Number.isFinite(v) ? v : 0;
+ const dRaw = Number(deltaValue);
+ const hasDelta = Number.isFinite(dRaw);
+ const d = hasDelta ? dRaw : 0;
+ const dSign = d > 0 ? '+' : '';
+ if (layerId === 'userspace') {
+ if (signalIdx === 0) return withDelta && hasDelta ? `proc ${Math.round(safe)}\nΔ ${dSign}${Math.round(d)}` : `proc ${Math.round(safe)}`;
+ if (signalIdx === 1) return withDelta && hasDelta ? `wake ${Math.round(safe)}%\nΔ ${dSign}${Math.round(d)}%` : `wake ${Math.round(safe)}%`;
+ return withDelta && hasDelta ? `sys ${Math.round(safe)}/s\nΔ ${dSign}${Math.round(d)}/s` : `sys ${Math.round(safe)}/s`;
+ }
+ if (layerId === 'socket') {
+ if (signalIdx === 0) return withDelta && hasDelta ? `est ${Math.round(safe)}\nΔ ${dSign}${Math.round(d)}` : `est ${Math.round(safe)}`;
+ if (signalIdx === 1) return withDelta && hasDelta ? `q ${Math.round(safe)}\nΔ ${dSign}${Math.round(d)}` : `q ${Math.round(safe)}`;
+ return withDelta && hasDelta ? `rtx ${safe.toFixed(1)}/s\nΔ ${dSign}${d.toFixed(1)}/s` : `rtx ${safe.toFixed(1)}/s`;
+ }
+ if (layerId === 'tcp') {
+ if (signalIdx === 0) return withDelta && hasDelta ? `cwnd ${Math.round(safe)}\nΔ ${dSign}${Math.round(d)}` : `cwnd ${Math.round(safe)}`;
+ if (signalIdx === 1) return withDelta && hasDelta ? `rtt ${safe.toFixed(1)}ms\nΔ ${dSign}${d.toFixed(1)}ms` : `rtt ${safe.toFixed(1)}ms`;
+ return withDelta && hasDelta ? `rtx ${safe.toFixed(1)}/s\nΔ ${dSign}${d.toFixed(1)}/s` : `rtx ${safe.toFixed(1)}/s`;
+ }
+ if (layerId === 'ip') {
+ if (signalIdx === 0) return withDelta && hasDelta ? `in ${safe.toFixed(1)}/s\nΔ ${dSign}${d.toFixed(1)}/s` : `in ${safe.toFixed(1)}/s`;
+ if (signalIdx === 1) return withDelta && hasDelta ? `out ${safe.toFixed(1)}/s\nΔ ${dSign}${d.toFixed(1)}/s` : `out ${safe.toFixed(1)}/s`;
+ return withDelta && hasDelta ? `asym ${(safe * 100).toFixed(0)}%\nΔ ${dSign}${(d * 100).toFixed(0)}%` : `asym ${(safe * 100).toFixed(0)}%`;
+ }
+ if (layerId === 'netfilter') {
+ if (signalIdx === 0) return withDelta && hasDelta ? `drop ${(safe * 100).toFixed(1)}%\nΔ ${dSign}${(d * 100).toFixed(1)}%` : `drop ${(safe * 100).toFixed(1)}%`;
+ if (signalIdx === 1) return withDelta && hasDelta ? `drop ${safe.toFixed(1)}/s\nΔ ${dSign}${d.toFixed(1)}/s` : `drop ${safe.toFixed(1)}/s`;
+ return withDelta && hasDelta ? `ct ${Math.round(safe)}%\nΔ ${dSign}${Math.round(d)}%` : `ct ${Math.round(safe)}%`;
+ }
+ if (layerId === 'driver') {
+ if (signalIdx === 0) return withDelta && hasDelta ? `txq ${Math.round(safe)}\nΔ ${dSign}${Math.round(d)}` : `txq ${Math.round(safe)}`;
+ if (signalIdx === 1) return withDelta && hasDelta ? `drop ${safe.toFixed(1)}/s\nΔ ${dSign}${d.toFixed(1)}/s` : `drop ${safe.toFixed(1)}/s`;
+ return withDelta && hasDelta ? `irq ${Math.round(safe)}\nΔ ${dSign}${Math.round(d)}` : `irq ${Math.round(safe)}`;
+ }
+ if (layerId === 'nic') {
+ if (signalIdx === 0) return withDelta && hasDelta ? `rx err ${Math.round(safe)}\nΔ ${dSign}${Math.round(d)}` : `rx err ${Math.round(safe)}`;
+ if (signalIdx === 1) return withDelta && hasDelta ? `tx err ${Math.round(safe)}\nΔ ${dSign}${Math.round(d)}` : `tx err ${Math.round(safe)}`;
+ return withDelta && hasDelta ? `dma ${Math.round(safe)}%\nΔ ${dSign}${Math.round(d)}%` : `dma ${Math.round(safe)}%`;
+ }
+ return `${Math.round(safe)}`;
+ }
+
+ updateBranchMetricHistory(tsMs) {
+ const now = Number(tsMs) || Date.now();
+ const windowMs = Math.max(1000, Number(this.branchDeltaWindowMs) || 6000);
+ const cutoff = now - windowMs;
+ const nextDelta = {};
+ Object.entries(this.layerBranchMetricTarget || {}).forEach(([layerId, values]) => {
+ if (!Array.isArray(values)) return;
+ const history = this.layerBranchMetricHistory[layerId] || [];
+ history.push({ ts: now, values: values.slice() });
+ while (history.length > 0 && history[0].ts < cutoff) {
+ history.shift();
+ }
+ this.layerBranchMetricHistory[layerId] = history;
+ const baseline = history[0]?.values || values;
+ const delta = values.map((v, idx) => (Number(v) || 0) - (Number(baseline[idx]) || 0));
+ nextDelta[layerId] = delta;
+ });
+ this.layerBranchMetricDelta = nextDelta;
+ }
+
+ createLayerNoiseRig(def, layerDepth) {
+ const controlHeavy = new Set(['userspace', 'socket', 'tcp', 'ip']);
+ const hardwareHeavy = new Set(['driver', 'nic']);
+ const isControl = controlHeavy.has(def.id);
+ const isHardware = hardwareHeavy.has(def.id);
+ const profile = {
+ type: isControl ? 'control' : (isHardware ? 'hardware' : 'mixed'),
+ hubRadius: isControl ? 0.088 : (isHardware ? 0.122 : 0.104),
+ ringRadius: isControl ? 0.21 : (isHardware ? 0.17 : 0.19),
+ armCount: isControl ? 6 : (isHardware ? 3 : 4),
+ armRadius: isControl ? 0.013 : (isHardware ? 0.022 : 0.017),
+ armOpacity: isControl ? 0.24 : (isHardware ? 0.38 : 0.3),
+ branchMin: isControl ? 2 : (isHardware ? 1 : 2),
+ branchMax: isControl ? 4 : (isHardware ? 2 : 3),
+ branchRadius: isControl ? 0.009 : (isHardware ? 0.015 : 0.011),
+ ringRotateSpeed: isControl ? 0.25 : (isHardware ? 0.07 : 0.14),
+ pulseAmp: isControl ? 0.11 : (isHardware ? 0.06 : 0.08),
+ xSpread: isControl ? 2.7 : (isHardware ? 1.95 : 2.3)
+ };
+
+ const hubXOffset = isControl
+ ? ((def.id === 'userspace' || def.id === 'tcp') ? -0.16 : 0.16)
+ : (isHardware ? ((def.id === 'nic') ? 0.2 : -0.2) : 0);
+ const hubPos = new THREE.Vector3(hubXOffset, def.y, -0.08);
+ const hub = new THREE.Mesh(
+ new THREE.SphereGeometry(profile.hubRadius, 14, 14),
+ new THREE.MeshBasicMaterial({
+ color: 0xa8b2bd,
+ transparent: true,
+ opacity: isHardware ? 0.48 : 0.4
+ })
+ );
+ hub.position.copy(hubPos);
+ this.scene.add(hub);
+
+ const ring = new THREE.Mesh(
+ new THREE.TorusGeometry(profile.ringRadius, isHardware ? 0.014 : 0.012, 8, 22),
+ new THREE.MeshBasicMaterial({
+ color: 0x657383,
+ transparent: true,
+ opacity: isControl ? 0.22 : 0.3
+ })
+ );
+ ring.position.copy(hubPos);
+ ring.rotation.x = Math.PI / 2;
+ this.scene.add(ring);
+
+ const arms = [];
+ const supportRods = [];
+ const satellites = [];
+ const armCount = profile.armCount;
+ for (let i = 0; i < armCount; i++) {
+ const side = i % 2 === 0 ? -1 : 1;
+ const lane = Math.floor(i / 2);
+ const x = hubXOffset + side * (1.1 + lane * (isControl ? 0.64 : 0.82) + Math.random() * profile.xSpread * 0.16);
+ const z = (Math.random() - 0.5) * (layerDepth * 0.72);
+ const end = new THREE.Vector3(x, def.y + (Math.random() - 0.5) * (isControl ? 0.14 : 0.06), z);
+ const armRod = this.createRodBetween(hubPos, end, profile.armRadius, 0x7d8a97, profile.armOpacity);
+ const armEntry = {
+ rod: armRod,
+ start: hubPos.clone(),
+ end: end.clone(),
+ phase: Math.random(),
+ signal: 0.2,
+ beads: [],
+ signalIdx: i,
+ signalLabel: '',
+ signalTag: null,
+ signalTagOffset: new THREE.Vector3(
+ (Math.random() - 0.5) * 0.16,
+ 0.08 + Math.random() * 0.07,
+ (Math.random() - 0.5) * 0.16
+ )
+ };
+
+ const satellite = new THREE.Mesh(
+ new THREE.SphereGeometry(
+ (isControl ? 0.058 : 0.078) + Math.random() * (isControl ? 0.016 : 0.024),
+ 10,
+ 10
+ ),
+ new THREE.MeshBasicMaterial({
+ color: 0xb5bdc6,
+ transparent: true,
+ opacity: isControl ? 0.3 : 0.4
+ })
+ );
+ satellite.position.copy(end);
+ this.scene.add(satellite);
+ satellites.push(satellite);
+
+ const beadCount = isControl ? 4 : 3;
+ for (let b = 0; b < beadCount; b++) {
+ const bead = new THREE.Mesh(
+ new THREE.SphereGeometry(0.016 + Math.random() * 0.008, 8, 8),
+ new THREE.MeshBasicMaterial({
+ color: 0x95b8cf,
+ transparent: true,
+ opacity: 0.26
+ })
+ );
+ bead.position.copy(hubPos.clone().lerp(end, (b + 1) / (beadCount + 1)));
+ this.scene.add(bead);
+ armEntry.beads.push(bead);
+ }
+ const labels = this.layerSignalLabels[def.id] || ['sig'];
+ armEntry.signalIdx = i % Math.max(1, labels.length);
+ armEntry.signalLabel = labels[armEntry.signalIdx] || `sig${armEntry.signalIdx}`;
+ armEntry.signalTag = this.createSignalTagSprite(`${armEntry.signalLabel} --`);
+ if (armEntry.signalTag?.sprite) {
+ const tagPos = hubPos.clone().lerp(end, 0.56).add(armEntry.signalTagOffset);
+ armEntry.signalTag.sprite.position.copy(tagPos);
+ }
+ arms.push(armEntry);
+
+ const miniCount = profile.branchMin + Math.floor(Math.random() * (profile.branchMax - profile.branchMin + 1));
+ for (let j = 0; j < miniCount; j++) {
+ const miniEnd = end.clone().add(new THREE.Vector3(
+ side * (0.24 + Math.random() * (isControl ? 0.56 : 0.38)),
+ (Math.random() - 0.5) * (isControl ? 0.18 : 0.08),
+ (Math.random() - 0.5) * (isControl ? 0.72 : 0.42)
+ ));
+ const miniRod = this.createRodBetween(
+ end,
+ miniEnd,
+ profile.branchRadius,
+ 0x74808d,
+ isControl ? 0.2 : 0.26
+ );
+ if (miniRod) {
+ supportRods.push(miniRod);
+ }
+ const miniDot = new THREE.Mesh(
+ new THREE.SphereGeometry((isControl ? 0.022 : 0.03) + Math.random() * 0.01, 8, 8),
+ new THREE.MeshBasicMaterial({
+ color: 0x90a0b0,
+ transparent: true,
+ opacity: isControl ? 0.2 : 0.28
+ })
+ );
+ miniDot.position.copy(miniEnd);
+ this.scene.add(miniDot);
+ satellites.push(miniDot);
+ }
+ }
+
+ return { hub, ring, arms, supportRods, satellites, profile };
+ }
+
createFlowParticles() {
// Thin particles flowing along the vertical spine.
for (let i = 0; i < 36; i++) {
@@ -297,6 +663,9 @@ class NetworkStackVisualization {
};
this.scene.add(p);
this.lineParticles.push(p);
+ if (this.hideVerticalOrbs) {
+ p.visible = false;
+ }
}
// Micro-packets falling through layers (plus occasional upward control packets).
@@ -318,6 +687,9 @@ class NetworkStackVisualization {
};
this.scene.add(m);
this.microPackets.push(m);
+ if (this.hideVerticalOrbs) {
+ m.visible = false;
+ }
}
}
@@ -343,6 +715,13 @@ class NetworkStackVisualization {
this.scene.add(trail);
this.packetTrail.push(trail);
}
+ if (this.hideVerticalOrbs) {
+ this.packet.visible = false;
+ this.packetGlow.visible = false;
+ this.packetTrail.forEach((trail) => {
+ if (trail) trail.visible = false;
+ });
+ }
}
createOverlayUI() {
@@ -723,6 +1102,72 @@ class NetworkStackVisualization {
this.container.appendChild(puzzleModeBtn);
this.overlayNodes.push(puzzleModeBtn);
this.puzzleModeButton = puzzleModeBtn;
+
+ const noiseModeBtn = document.createElement('button');
+ noiseModeBtn.textContent = 'NOISE: DENSE';
+ noiseModeBtn.style.cssText = `
+ position: absolute;
+ top: 130px;
+ right: 20px;
+ padding: 7px 10px;
+ background: rgba(12, 18, 28, 0.88);
+ border: 1px solid rgba(230, 193, 90, 0.58);
+ color: #f0dca2;
+ font-family: 'Share Tech Mono', monospace;
+ font-size: 10px;
+ letter-spacing: 0.45px;
+ cursor: pointer;
+ z-index: 1002;
+ transition: all 0.2s ease;
+ `;
+ noiseModeBtn.onmouseenter = () => {
+ noiseModeBtn.style.background = 'rgba(19, 28, 40, 0.95)';
+ noiseModeBtn.style.color = '#edf2f8';
+ };
+ noiseModeBtn.onmouseleave = () => {
+ noiseModeBtn.style.background = 'rgba(12, 18, 28, 0.88)';
+ noiseModeBtn.style.color = this.noiseDetailMode === 'dense' ? '#f0dca2' : '#c6d0db';
+ };
+ noiseModeBtn.onclick = () => {
+ this.toggleNoiseDetailMode();
+ };
+ this.container.appendChild(noiseModeBtn);
+ this.overlayNodes.push(noiseModeBtn);
+ this.noiseModeButton = noiseModeBtn;
+
+ const readModeBtn = document.createElement('button');
+ readModeBtn.textContent = 'READ: FORENSICS';
+ readModeBtn.style.cssText = `
+ position: absolute;
+ top: 166px;
+ 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;
+ `;
+ readModeBtn.onmouseenter = () => {
+ readModeBtn.style.background = 'rgba(19, 28, 40, 0.95)';
+ readModeBtn.style.color = '#edf2f8';
+ };
+ readModeBtn.onmouseleave = () => {
+ readModeBtn.style.background = 'rgba(12, 18, 28, 0.88)';
+ if (this.readMode === 'scene') readModeBtn.style.color = '#9bd4f2';
+ else if (this.readMode === 'ops') readModeBtn.style.color = '#f0dca2';
+ else readModeBtn.style.color = '#c6d0db';
+ };
+ readModeBtn.onclick = () => {
+ this.toggleReadMode();
+ };
+ this.container.appendChild(readModeBtn);
+ this.overlayNodes.push(readModeBtn);
+ this.readModeButton = readModeBtn;
this.updateBottomPanelsLayout();
this.updatePacketLifecycleUI();
this.applyViewDensityMode();
@@ -789,6 +1234,13 @@ class NetworkStackVisualization {
if (this.puzzleModeButton) {
this.puzzleModeButton.style.display = minimal ? 'none' : 'block';
}
+ if (this.noiseModeButton) {
+ this.noiseModeButton.style.display = minimal ? 'none' : 'block';
+ }
+ if (this.readModeButton) {
+ this.readModeButton.style.display = minimal ? 'none' : 'block';
+ }
+ this.applyReadModeVisibility();
}
toggleViewDensityMode() {
@@ -809,6 +1261,64 @@ class NetworkStackVisualization {
this.updatePacketLifecycleUI();
}
+ toggleNoiseDetailMode() {
+ this.noiseDetailMode = this.noiseDetailMode === 'dense' ? 'normal' : 'dense';
+ if (this.noiseModeButton) {
+ const dense = this.noiseDetailMode === 'dense';
+ this.noiseModeButton.textContent = dense ? 'NOISE: DENSE' : 'NOISE: NORMAL';
+ this.noiseModeButton.style.borderColor = dense
+ ? 'rgba(230, 193, 90, 0.58)'
+ : 'rgba(125, 138, 156, 0.34)';
+ this.noiseModeButton.style.color = dense ? '#f0dca2' : '#c6d0db';
+ }
+ Object.keys(this.layerBranchMetricDelta || {}).forEach((layerId) => {
+ const deltas = this.layerBranchMetricDelta[layerId];
+ if (Array.isArray(deltas)) {
+ this.layerBranchMetricDelta[layerId] = deltas.map((v) => Number(v) || 0);
+ }
+ });
+ }
+
+ applyReadModeVisibility() {
+ if (this.viewDensityMode === 'minimal') return;
+ const mode = this.readMode;
+ const showLayers = mode !== 'scene';
+ const showChips = mode !== 'scene';
+ const showGalaxy = mode !== 'scene';
+ const showLifecycle = mode === 'forensics';
+
+ if (this.layersPanelNode) this.layersPanelNode.style.display = showLayers ? 'block' : 'none';
+ if (this.chipLayerNode) this.chipLayerNode.style.display = showChips ? 'block' : 'none';
+ if (this.galaxyPanelNode) this.galaxyPanelNode.style.display = showGalaxy ? 'block' : 'none';
+ if (this.lifecyclePanelNode) this.lifecyclePanelNode.style.display = showLifecycle ? 'block' : 'none';
+
+ if (this.noiseModeButton) this.noiseModeButton.style.display = mode === 'forensics' ? 'block' : 'none';
+ if (this.puzzleModeButton) this.puzzleModeButton.style.display = mode === 'forensics' ? 'block' : 'none';
+ }
+
+ toggleReadMode() {
+ const order = ['scene', 'ops', 'forensics'];
+ const idx = order.indexOf(this.readMode);
+ this.readMode = order[(idx + 1) % order.length];
+ if (this.readModeButton) {
+ if (this.readMode === 'scene') {
+ this.readModeButton.textContent = 'READ: SCENE';
+ this.readModeButton.style.borderColor = 'rgba(130, 204, 240, 0.58)';
+ this.readModeButton.style.color = '#9bd4f2';
+ } else if (this.readMode === 'ops') {
+ this.readModeButton.textContent = 'READ: OPS';
+ this.readModeButton.style.borderColor = 'rgba(230, 193, 90, 0.58)';
+ this.readModeButton.style.color = '#f0dca2';
+ } else {
+ this.readModeButton.textContent = 'READ: FORENSICS';
+ this.readModeButton.style.borderColor = 'rgba(125, 138, 156, 0.34)';
+ this.readModeButton.style.color = '#c6d0db';
+ }
+ }
+ this.applyReadModeVisibility();
+ this.updatePacketLifecycleUI();
+ }
+
selectGalaxy(id) {
if (!id || !this.galaxyNodes[id]) return;
this.selectedGalaxy = id;
@@ -1311,6 +1821,130 @@ class NetworkStackVisualization {
driver: Number(a.driver ?? this.layerActivityTarget.driver),
nic: Number(a.nic ?? this.layerActivityTarget.nic)
};
+
+ const clamp01 = (value) => Math.max(0, Math.min(1, Number(value) || 0));
+ const userspaceProcs = safeNum(m.userspace?.active_processes ?? 0);
+ const socketEst = safeNum(m.socket_api?.established ?? 0);
+ const ipIn = safeNum(m.ip?.in_packets_per_sec ?? 0);
+ const ipOut = safeNum(m.ip?.out_packets_per_sec ?? 0);
+ const driverDrops = safeNum(m.driver?.drops_per_sec ?? 0);
+ const flowIntensity = safeNum(this.packetSpeed || 0) / 5.2;
+ const ipAsym = Math.abs(ipIn - ipOut) / Math.max(1, ipIn + ipOut);
+
+ this.layerSemanticNoiseTarget = {
+ userspace: {
+ stress: clamp01(userspaceProcs / 320),
+ jitter: clamp01(flowIntensity * 0.55 + userspaceProcs / 540),
+ branch: clamp01(0.2 + userspaceProcs / 500)
+ },
+ socket: {
+ stress: clamp01(socketEst / 180 + retransPerSec / 44),
+ jitter: clamp01(retransPerSec / 34 + flowIntensity * 0.25),
+ branch: clamp01(0.24 + socketEst / 260)
+ },
+ tcp: {
+ stress: clamp01(rttMs / 260 + retransPerSec / 34),
+ jitter: clamp01(retransPerSec / 24 + rttMs / 420),
+ branch: clamp01(0.25 + retransPerSec / 30)
+ },
+ ip: {
+ stress: clamp01((ipIn + ipOut) / 9000 + ipAsym * 0.7),
+ jitter: clamp01(ipAsym + flowIntensity * 0.2),
+ branch: clamp01(0.2 + (ipIn + ipOut) / 13000)
+ },
+ netfilter: {
+ stress: clamp01(dropRatio * 2.6 + dropPerSec / 24),
+ jitter: clamp01(dropPerSec / 16 + dropRatio * 1.6),
+ branch: clamp01(0.22 + dropRatio * 2.1)
+ },
+ driver: {
+ stress: clamp01(driverTxQ / 360 + driverDrops / 22),
+ jitter: clamp01(driverTxQ / 470 + driverDrops / 15),
+ branch: clamp01(0.22 + driverTxQ / 500)
+ },
+ nic: {
+ stress: clamp01(nicErrTotal / 30 + driverTxQ / 650),
+ jitter: clamp01(nicErrTotal / 18 + flowIntensity * 0.2),
+ branch: clamp01(0.2 + nicErrTotal / 28)
+ }
+ };
+
+ this.layerBranchSignalTarget = {
+ userspace: [
+ clamp01(userspaceProcs / 320),
+ clamp01(flowIntensity),
+ clamp01((userspaceProcs * 0.08 + socketEst * 0.25) / 120)
+ ],
+ socket: [
+ clamp01(socketEst / 180),
+ clamp01((socketEst * 0.18 + retransPerSec * 4) / 60),
+ clamp01(retransPerSec / 26)
+ ],
+ tcp: [
+ clamp01(safeNum(m.tcp_udp?.cwnd ?? 0) / 240),
+ clamp01(rttMs / 260),
+ clamp01(retransPerSec / 20)
+ ],
+ ip: [
+ clamp01(ipIn / 5500),
+ clamp01(ipOut / 5500),
+ clamp01(ipAsym)
+ ],
+ netfilter: [
+ clamp01(dropRatio * 2.4),
+ clamp01(dropPerSec / 20),
+ clamp01((dropRatio * 120 + dropPerSec) / 30)
+ ],
+ driver: [
+ clamp01(driverTxQ / 360),
+ clamp01(driverDrops / 18),
+ clamp01((driverTxQ + driverDrops * 8) / 520)
+ ],
+ nic: [
+ clamp01(nicErrRx / 14),
+ clamp01(nicErrTx / 14),
+ clamp01(nicErrTotal / 22 + flowIntensity * 0.25)
+ ]
+ };
+
+ this.layerBranchMetricTarget = {
+ userspace: [
+ userspaceProcs,
+ flowIntensity * 100,
+ userspaceProcs * 0.08 + socketEst * 0.25
+ ],
+ socket: [
+ socketEst,
+ socketEst * 0.18 + retransPerSec * 4,
+ retransPerSec
+ ],
+ tcp: [
+ safeNum(m.tcp_udp?.cwnd ?? 0),
+ rttMs,
+ retransPerSec
+ ],
+ ip: [
+ ipIn,
+ ipOut,
+ ipAsym
+ ],
+ netfilter: [
+ dropRatio,
+ dropPerSec,
+ dropRatio * 100 + dropPerSec
+ ],
+ driver: [
+ driverTxQ,
+ driverDrops,
+ driverTxQ * 0.6 + driverDrops * 3.5
+ ],
+ nic: [
+ nicErrRx,
+ nicErrTx,
+ nicErrTotal * 4 + flowIntensity * 40
+ ]
+ };
+ this.updateBranchMetricHistory(Date.now());
}
fetchTelemetry() {
@@ -1401,41 +2035,197 @@ class NetworkStackVisualization {
this.layerActivity[id] = blended;
const actRaw = blended;
const act = Math.max(0, Math.min(1, actRaw));
+ const currentSemantic = this.layerSemanticNoise[id] || { stress: 0.2, jitter: 0.2, branch: 0.2 };
+ const targetSemantic = this.layerSemanticNoiseTarget[id] || currentSemantic;
+ const semanticBlend = Math.min(1, dt * 3.1);
+ currentSemantic.stress += (targetSemantic.stress - currentSemantic.stress) * semanticBlend;
+ currentSemantic.jitter += (targetSemantic.jitter - currentSemantic.jitter) * semanticBlend;
+ currentSemantic.branch += (targetSemantic.branch - currentSemantic.branch) * semanticBlend;
+ this.layerSemanticNoise[id] = currentSemantic;
+ const semanticStress = Math.max(0, Math.min(1, currentSemantic.stress));
+ const semanticJitter = Math.max(0, Math.min(1, currentSemantic.jitter));
+ const semanticBranch = Math.max(0, Math.min(1, currentSemantic.branch));
+ const targetSignals = this.layerBranchSignalTarget[id] || [];
+ if (!this.layerBranchSignalCurrent[id]) {
+ this.layerBranchSignalCurrent[id] = targetSignals.slice();
+ } else {
+ const currentSignals = this.layerBranchSignalCurrent[id];
+ const signalCount = Math.max(currentSignals.length, targetSignals.length);
+ for (let i = 0; i < signalCount; i++) {
+ const cur = Number(currentSignals[i] ?? 0.2);
+ const tgt = Number(targetSignals[i] ?? targetSignals[targetSignals.length - 1] ?? 0.2);
+ currentSignals[i] = cur + (tgt - cur) * Math.min(1, dt * 3.6);
+ }
+ }
+ const targetMetrics = this.layerBranchMetricTarget[id] || [];
+ if (!this.layerBranchMetricCurrent[id]) {
+ this.layerBranchMetricCurrent[id] = targetMetrics.slice();
+ } else {
+ const currentMetrics = this.layerBranchMetricCurrent[id];
+ const metricCount = Math.max(currentMetrics.length, targetMetrics.length);
+ for (let i = 0; i < metricCount; i++) {
+ const cur = Number(currentMetrics[i] ?? 0);
+ const tgt = Number(targetMetrics[i] ?? targetMetrics[targetMetrics.length - 1] ?? 0);
+ currentMetrics[i] = cur + (tgt - cur) * Math.min(1, dt * 3.4);
+ }
+ }
+
if (layer.mesh?.material) {
- layer.mesh.material.opacity = 0.2 + act * 0.28;
+ layer.mesh.material.opacity = 0.03 + act * 0.09;
layer.mesh.material.emissiveIntensity = 0.02 + act * 0.18;
}
if (layer.strip?.material) {
- layer.strip.scale.x = 0.14 + act * 0.86;
- layer.strip.material.opacity = 0.12 + act * 0.78;
+ layer.strip.scale.x = 0.14 + act * 0.86 + semanticStress * 0.14;
+ layer.strip.material.opacity = 0.12 + act * 0.78 + semanticBranch * 0.06;
+ }
+ if (this.hideOsiTiles) {
+ if (layer.mesh) layer.mesh.visible = false;
+ if (layer.edge) layer.edge.visible = false;
+ if (layer.strip) layer.strip.visible = false;
+ }
+ if (layer.noiseRig) {
+ const rig = layer.noiseRig;
+ const profile = rig.profile || {};
+ const ringSpeed = Number(profile.ringRotateSpeed || 0.14);
+ const pulseAmp = Number(profile.pulseAmp || 0.08);
+ if (rig.hub?.material) {
+ rig.hub.material.opacity = 0.2 + act * 0.42 + semanticStress * 0.22;
+ }
+ if (rig.hub?.scale) {
+ const hubPulse = 1 + (0.02 + semanticJitter * 0.08) * Math.sin((performance.now() * 0.0032) + (layer.id.length * 0.7));
+ rig.hub.scale.setScalar(hubPulse);
+ }
+ if (rig.ring?.material) {
+ rig.ring.material.opacity = 0.12 + act * 0.26 + semanticBranch * 0.24;
+ rig.ring.rotation.z += (ringSpeed + act * 0.15 + semanticJitter * 0.38) * dt;
+ }
+ if (Array.isArray(rig.supportRods)) {
+ rig.supportRods.forEach((rod, idx) => {
+ if (rod?.material) {
+ const pulse = 0.5 + 0.5 * Math.sin((performance.now() * 0.0018) + idx * 0.37);
+ rod.material.opacity = 0.06 + act * 0.12 + semanticBranch * 0.1 + pulse * 0.05;
+ }
+ });
+ }
+ if (Array.isArray(rig.arms)) {
+ const signals = this.layerBranchSignalCurrent[id] || [];
+ const metricValues = this.layerBranchMetricCurrent[id] || [];
+ const metricDeltas = this.layerBranchMetricDelta[id] || [];
+ rig.arms.forEach((armEntry, idx) => {
+ const arm = armEntry?.rod;
+ const signal = Number(signals[armEntry.signalIdx % Math.max(1, signals.length)] ?? semanticBranch);
+ armEntry.signal += (signal - armEntry.signal) * Math.min(1, dt * 4.5);
+ const armSignal = Math.max(0, Math.min(1, armEntry.signal));
+ if (arm?.material) {
+ const idxPulse = 0.5 + 0.5 * Math.sin((performance.now() * 0.0015) + idx * 0.42);
+ arm.material.opacity = 0.08
+ + act * (0.12 + (idx % 3) * 0.025)
+ + semanticBranch * 0.16
+ + armSignal * 0.22
+ + semanticJitter * idxPulse * 0.09;
+ const hue = 0.56 - armSignal * 0.34;
+ arm.material.color.setHSL(hue, 0.55, 0.54);
+ }
+ if (armEntry.signalTag?.sprite) {
+ const posPulse = Math.sin((performance.now() * 0.0021) + idx * 0.7) * (0.02 + semanticJitter * 0.04);
+ const tagPos = armEntry.start
+ .clone()
+ .lerp(armEntry.end, 0.56 + posPulse)
+ .add(armEntry.signalTagOffset);
+ armEntry.signalTag.sprite.position.copy(tagPos);
+ const metricValue = Number(metricValues[armEntry.signalIdx % Math.max(1, metricValues.length)] ?? armSignal * 100);
+ const metricDelta = Number(metricDeltas[armEntry.signalIdx % Math.max(1, metricDeltas.length)] ?? 0);
+ const denseTags = this.noiseDetailMode === 'dense' && this.readMode === 'forensics';
+ const signalText = this.formatBranchMetric(
+ id,
+ armEntry.signalIdx,
+ metricValue,
+ metricDelta,
+ denseTags
+ );
+ this.updateSignalTagSprite(armEntry.signalTag, signalText, armSignal, semanticJitter);
+ const modeOpacity = this.readMode === 'scene' ? 0.35 : (this.readMode === 'ops' ? 0.65 : 1);
+ armEntry.signalTag.sprite.material.opacity = (0.25 + armSignal * 0.7) * modeOpacity;
+ armEntry.signalTag.sprite.scale.set(
+ (1.2 + armSignal * 0.2) * (this.readMode === 'scene' ? 0.78 : 1),
+ (0.29 + armSignal * 0.05) * (this.readMode === 'scene' ? 0.78 : 1),
+ 1
+ );
+ }
+ if (Array.isArray(armEntry?.beads)) {
+ armEntry.phase += dt * (0.22 + armSignal * 1.45 + semanticJitter * 0.55);
+ armEntry.beads.forEach((bead, bIdx) => {
+ if (!bead) return;
+ const t = (armEntry.phase + bIdx * 0.27) % 1;
+ const pos = armEntry.start.clone().lerp(armEntry.end, t);
+ bead.position.copy(pos);
+ if (bead.material) {
+ bead.material.opacity = 0.14 + armSignal * 0.45 + semanticBranch * 0.16;
+ const beadHue = 0.58 - armSignal * 0.4;
+ bead.material.color.setHSL(beadHue, 0.62, 0.6);
+ }
+ const scale = 0.85 + armSignal * 0.9;
+ bead.scale.setScalar(scale);
+ });
+ }
+ });
+ }
+ if (Array.isArray(rig.satellites)) {
+ rig.satellites.forEach((dot, idx) => {
+ if (dot?.material) {
+ const pulse = 0.5 + 0.5 * Math.sin((performance.now() * 0.0025) + idx * 0.65);
+ dot.material.opacity = 0.1 + act * 0.2 + semanticBranch * 0.2 + pulse * (pulseAmp + semanticJitter * 0.12);
+ }
+ });
+ }
}
});
}
getLayerTooltipContent(layerId) {
const m = this.telemetryData?.layer_metrics || {};
+ const safeNum = (value) => {
+ const n = Number(value);
+ return Number.isFinite(n) ? n : 0;
+ };
+ const labels = this.layerSignalLabels[layerId] || [];
+ const metrics = this.layerBranchMetricCurrent[layerId] || [];
+ const deltas = this.layerBranchMetricDelta[layerId] || [];
+ const signalLine = labels.length
+ ? labels.map((_, idx) => this.formatBranchMetric(
+ layerId,
+ idx,
+ safeNum(metrics[idx] ?? 0),
+ safeNum(deltas[idx] ?? 0),
+ this.noiseDetailMode === 'dense' && this.readMode === 'forensics'
+ ).replace('\n', ' ')).join(' | ')
+ : '';
+ const appendSignals = (body) => (
+ `${body}${signalLine ? `
branches: ${signalLine}` : ''}`
+ );
+
if (layerId === 'userspace') {
- return `Userspace
active processes: ${m.userspace?.active_processes ?? 0}`;
+ return appendSignals(`Userspace
active processes: ${m.userspace?.active_processes ?? 0}`);
}
if (layerId === 'socket') {
- return `Socket API
established: ${m.socket_api?.established ?? 0}
retransmits/s: ${m.socket_api?.retransmits_per_sec ?? 0}`;
+ return appendSignals(`Socket API
established: ${m.socket_api?.established ?? 0}
retransmits/s: ${m.socket_api?.retransmits_per_sec ?? 0}`);
}
if (layerId === 'tcp') {
- return `TCP/UDP
cwnd: ${m.tcp_udp?.cwnd ?? 0}
rtt: ${m.tcp_udp?.rtt_ms ?? 0} ms
retrans/s: ${m.tcp_udp?.retrans_per_sec ?? 0}`;
+ return appendSignals(`TCP/UDP
cwnd: ${m.tcp_udp?.cwnd ?? 0}
rtt: ${m.tcp_udp?.rtt_ms ?? 0} ms
retrans/s: ${m.tcp_udp?.retrans_per_sec ?? 0}`);
}
if (layerId === 'ip') {
- return `IP
packets in: ${m.ip?.in_packets_per_sec ?? 0}/s
packets out: ${m.ip?.out_packets_per_sec ?? 0}/s`;
+ return appendSignals(`IP
packets in: ${m.ip?.in_packets_per_sec ?? 0}/s
packets out: ${m.ip?.out_packets_per_sec ?? 0}/s`);
}
if (layerId === 'netfilter') {
- return `Netfilter
drop/s: ${m.netfilter?.drop_per_sec ?? 0}
drop ratio: ${((m.netfilter?.drop_ratio ?? 0) * 100).toFixed(2)}%`;
+ return appendSignals(`Netfilter
drop/s: ${m.netfilter?.drop_per_sec ?? 0}
drop ratio: ${((m.netfilter?.drop_ratio ?? 0) * 100).toFixed(2)}%`);
}
if (layerId === 'driver') {
- return `Driver
tx queue: ${m.driver?.tx_queue ?? 0}
drops/s: ${m.driver?.drops_per_sec ?? 0}`;
+ return appendSignals(`Driver
tx queue: ${m.driver?.tx_queue ?? 0}
drops/s: ${m.driver?.drops_per_sec ?? 0}`);
}
if (layerId === 'nic') {
- return `NIC
iface: ${m.nic?.iface ?? 'n/a'}
errors rx/tx: ${m.nic?.rx_errors ?? 0}/${m.nic?.tx_errors ?? 0}`;
+ return appendSignals(`NIC
iface: ${m.nic?.iface ?? 'n/a'}
errors rx/tx: ${m.nic?.rx_errors ?? 0}/${m.nic?.tx_errors ?? 0}`);
}
- return `${layerId}`;
+ return appendSignals(`${layerId}`);
}
onMouseMove(event) {
@@ -1483,9 +2273,13 @@ class NetworkStackVisualization {
const dt = Math.min(0.04, (now - this.lastFrameTime) / 1000);
this.lastFrameTime = now;
- this.updatePacket(dt);
+ if (!this.hideVerticalOrbs) {
+ this.updatePacket(dt);
+ }
this.updateEffects(dt);
- this.updateFlowParticles(dt);
+ if (!this.hideVerticalOrbs) {
+ this.updateFlowParticles(dt);
+ }
this.updateLayerStrips(dt);
this.updatePacketLifecycleUI();
diff --git a/static/js/security-belt.js b/static/js/security-belt.js
index 6e42215..a1cee3e 100644
--- a/static/js/security-belt.js
+++ b/static/js/security-belt.js
@@ -23,8 +23,28 @@ class SecuritySubsystemVisualization {
this.hoveredProcessPid = null;
this.trustNodeHitAreas = [];
this.capabilityRowHitAreas = [];
+ this.ringHitAreas = [];
+ this.focusedRingKey = null;
+ this.hoveredRingKey = null;
+ this.ringFocusPinned = false;
+ this.panelRects = {};
+ this.panelSpotlight = null;
this.canvasClickHandler = null;
this.canvasMouseMoveHandler = null;
+ this.canvasDoubleClickHandler = null;
+ this.showSecurityCorePanel = false;
+ this.corePanelToggleButton = null;
+ this.ringDerivedVerdictFilter = null;
+ this.focusStatusNode = null;
+ this.unpinButton = null;
+ this.trustGraphMode = 'topology';
+ this.trustModeButton = null;
+ this.trustForensicsLayout = new Map();
+ this.trustForensicsFreezeUntil = 0;
+ this.trustForensicsFreezeWindowMs = 8000;
+ this.selectedIncidentTrail = [];
+ this.incidentTrailWindowMs = 22000;
+ this.incidentTrailMaxPoints = 36;
}
init(containerId = 'security-belt-container') {
@@ -60,8 +80,10 @@ class SecuritySubsystemVisualization {
this.ctx = this.canvas.getContext('2d');
this.canvasClickHandler = (event) => this.onCanvasClick(event);
this.canvasMouseMoveHandler = (event) => this.onCanvasMouseMove(event);
+ this.canvasDoubleClickHandler = (event) => this.onCanvasDoubleClick(event);
this.canvas.addEventListener('click', this.canvasClickHandler);
this.canvas.addEventListener('mousemove', this.canvasMouseMoveHandler);
+ this.canvas.addEventListener('dblclick', this.canvasDoubleClickHandler);
this.createOverlayUI();
this.addExitButton();
@@ -101,10 +123,53 @@ class SecuritySubsystemVisualization {
font-size: 11px;
z-index: 1001;
`;
- legend.textContent = 'pipeline + trust + attack surface + lsm/capabilities/seccomp tools';
+ legend.textContent = 'ring-first security model: policy / sandbox / threat pressure';
this.container.appendChild(legend);
this.overlayNodes.push(legend);
+ const focusStatus = document.createElement('div');
+ focusStatus.style.cssText = `
+ position: absolute;
+ top: 92px;
+ left: 24px;
+ color: rgba(170, 188, 214, 0.88);
+ font-family: 'Share Tech Mono', monospace;
+ font-size: 10px;
+ letter-spacing: 0.3px;
+ z-index: 1001;
+ `;
+ focusStatus.textContent = 'FOCUS: NONE';
+ this.container.appendChild(focusStatus);
+ this.overlayNodes.push(focusStatus);
+ this.focusStatusNode = focusStatus;
+
+ const unpinBtn = document.createElement('button');
+ unpinBtn.textContent = 'UNPIN';
+ unpinBtn.style.cssText = `
+ position: absolute;
+ top: 88px;
+ left: 218px;
+ padding: 3px 8px;
+ background: rgba(8, 12, 18, 0.56);
+ border: 1px solid rgba(150, 164, 188, 0.22);
+ color: rgba(188, 200, 219, 0.55);
+ font-family: 'Share Tech Mono', monospace;
+ font-size: 9px;
+ letter-spacing: 0.25px;
+ cursor: not-allowed;
+ z-index: 1001;
+ `;
+ unpinBtn.onclick = () => {
+ if (!this.ringFocusPinned) return;
+ this.ringFocusPinned = false;
+ this.focusedRingKey = null;
+ this.applyRingFocusVerdictFilter();
+ this.updateFocusStatusUi();
+ };
+ this.container.appendChild(unpinBtn);
+ this.overlayNodes.push(unpinBtn);
+ this.unpinButton = unpinBtn;
+
const filterPanel = document.createElement('div');
filterPanel.style.cssText = `
position: absolute;
@@ -156,13 +221,239 @@ class SecuritySubsystemVisualization {
clearBtn.onclick = () => {
if (!this.selectedProcessFilter) return;
this.selectedProcessFilter = null;
+ this.selectedIncidentTrail = [];
this.updateClearProcessButtonState();
};
filterPanel.appendChild(clearBtn);
this.clearProcessButton = clearBtn;
this.overlayNodes.push(clearBtn);
+
+ const coreToggleBtn = document.createElement('button');
+ coreToggleBtn.textContent = 'CORE PANEL: OFF';
+ coreToggleBtn.style.cssText = `
+ padding: 4px 8px;
+ background: rgba(8, 12, 18, 0.56);
+ border: 1px solid rgba(150, 164, 188, 0.22);
+ color: rgba(188, 200, 219, 0.8);
+ font-family: 'Share Tech Mono', monospace;
+ font-size: 9px;
+ letter-spacing: 0.3px;
+ cursor: pointer;
+ `;
+ coreToggleBtn.onclick = () => {
+ this.showSecurityCorePanel = !this.showSecurityCorePanel;
+ this.updateCorePanelToggleButtonState();
+ };
+ filterPanel.appendChild(coreToggleBtn);
+ this.corePanelToggleButton = coreToggleBtn;
+ this.overlayNodes.push(coreToggleBtn);
+
+ const trustModeBtn = document.createElement('button');
+ trustModeBtn.textContent = 'TRUST MODE: TOPOLOGY';
+ trustModeBtn.style.cssText = `
+ padding: 4px 8px;
+ background: rgba(8, 12, 18, 0.56);
+ border: 1px solid rgba(150, 164, 188, 0.22);
+ color: rgba(188, 200, 219, 0.8);
+ font-family: 'Share Tech Mono', monospace;
+ font-size: 9px;
+ letter-spacing: 0.3px;
+ cursor: pointer;
+ `;
+ trustModeBtn.onclick = () => this.toggleTrustGraphMode();
+ filterPanel.appendChild(trustModeBtn);
+ this.trustModeButton = trustModeBtn;
+ this.overlayNodes.push(trustModeBtn);
+
this.setVerdictFilter(this.verdictFilter);
this.updateClearProcessButtonState();
+ this.updateCorePanelToggleButtonState();
+ this.updateTrustGraphModeButtonState();
+ this.updateFocusStatusUi();
+ }
+
+ updateCorePanelToggleButtonState() {
+ if (!this.corePanelToggleButton) return;
+ const on = this.showSecurityCorePanel;
+ this.corePanelToggleButton.textContent = on ? 'CORE PANEL: ON' : 'CORE PANEL: OFF';
+ this.corePanelToggleButton.style.background = on ? 'rgba(32, 52, 81, 0.92)' : 'rgba(8, 12, 18, 0.56)';
+ this.corePanelToggleButton.style.borderColor = on ? 'rgba(124, 178, 255, 0.9)' : 'rgba(150, 164, 188, 0.22)';
+ this.corePanelToggleButton.style.color = on ? '#d9ecff' : 'rgba(188, 200, 219, 0.8)';
+ }
+
+ toggleTrustGraphMode() {
+ const order = ['topology', 'risk', 'forensics'];
+ const currentIdx = order.indexOf(this.trustGraphMode);
+ const nextIdx = currentIdx >= 0 ? ((currentIdx + 1) % order.length) : 0;
+ this.trustGraphMode = order[nextIdx];
+ this.updateTrustGraphModeButtonState();
+ }
+
+ updateTrustGraphModeButtonState() {
+ if (!this.trustModeButton) return;
+ const labels = {
+ topology: 'TOPOLOGY',
+ risk: 'RISK HEAT',
+ forensics: 'FORENSICS'
+ };
+ const modeLabel = labels[this.trustGraphMode] || 'TOPOLOGY';
+ const isRisk = this.trustGraphMode === 'risk';
+ const isForensics = this.trustGraphMode === 'forensics';
+ this.trustModeButton.textContent = `TRUST MODE: ${modeLabel}`;
+ this.trustModeButton.style.background = isRisk
+ ? 'rgba(70, 35, 32, 0.92)'
+ : (isForensics ? 'rgba(28, 45, 64, 0.92)' : 'rgba(8, 12, 18, 0.56)');
+ this.trustModeButton.style.borderColor = isRisk
+ ? 'rgba(255, 156, 126, 0.9)'
+ : (isForensics ? 'rgba(132, 190, 255, 0.88)' : 'rgba(150, 164, 188, 0.22)');
+ this.trustModeButton.style.color = isRisk
+ ? '#ffd8cb'
+ : (isForensics ? '#e3f2ff' : 'rgba(188, 200, 219, 0.8)');
+ }
+
+ buildForensicsLayout(rows, cx, cy, innerRadius, maxRadius) {
+ const layout = new Map();
+ const ordered = rows.slice().sort((a, b) => Number(a?.pid || 0) - Number(b?.pid || 0));
+ ordered.forEach((row, idx) => {
+ const pid = Number(row?.pid || 0);
+ const risk = Number(row?.risk_score || 0);
+ const riskNorm = Math.max(0, Math.min(1, risk / 100));
+ const seed = ((pid % 37) + 37) % 37;
+ const offset = (seed / 37 - 0.5) * 0.34;
+ const angle = ((Math.PI * 2) / Math.max(ordered.length, 1)) * idx - Math.PI / 2 + offset;
+ const orbit = innerRadius + (maxRadius - innerRadius) * (0.24 + riskNorm * 0.76);
+ layout.set(pid, {
+ angle,
+ orbit,
+ nx: cx + Math.cos(angle) * orbit,
+ ny: cy + Math.sin(angle) * orbit
+ });
+ });
+ return layout;
+ }
+
+ findSelectedTrustRow(rows) {
+ if (!this.selectedProcessFilter || !Array.isArray(rows) || !rows.length) return null;
+ const selectedPid = Number(this.selectedProcessFilter.pid || 0);
+ const selectedName = this.normalizeName(this.selectedProcessFilter.name || '');
+ return rows.find((row) => {
+ const rowPid = Number(row?.pid || 0);
+ const rowName = this.normalizeName(row?.name || '');
+ return rowPid === selectedPid || (selectedName && rowName === selectedName);
+ }) || null;
+ }
+
+ updateSelectedIncidentTrail(rows) {
+ if (!this.selectedProcessFilter) {
+ this.selectedIncidentTrail = [];
+ return;
+ }
+ const selectedRow = this.findSelectedTrustRow(rows);
+ if (!selectedRow) return;
+ const now = Date.now();
+ const risk = Number(selectedRow.risk_score || 0);
+ const trust = String(selectedRow.trust || 'trusted');
+ const prev = this.selectedIncidentTrail[this.selectedIncidentTrail.length - 1];
+ if (!prev || Math.abs(Number(prev.risk || 0) - risk) >= 0.5 || prev.trust !== trust || (now - Number(prev.ts || 0)) >= 1300) {
+ this.selectedIncidentTrail.push({ ts: now, risk, trust });
+ } else {
+ prev.ts = now;
+ prev.risk = risk;
+ prev.trust = trust;
+ }
+ const cutoff = now - this.incidentTrailWindowMs;
+ this.selectedIncidentTrail = this.selectedIncidentTrail
+ .filter((item) => Number(item.ts || 0) >= cutoff)
+ .slice(-this.incidentTrailMaxPoints);
+ }
+
+ drawIncidentTrail(x, y, w, h, selectedNode) {
+ if (!this.selectedProcessFilter || this.selectedIncidentTrail.length < 2) return;
+ const trail = this.selectedIncidentTrail;
+ const now = Date.now();
+ const chartX = x + 14;
+ const chartY = y + h - 58;
+ const chartW = Math.max(110, w - 28);
+ const chartH = 42;
+
+ this.drawRoundedRect(chartX, chartY, chartW, chartH, 5);
+ this.ctx.fillStyle = 'rgba(9, 13, 19, 0.56)';
+ this.ctx.fill();
+ this.ctx.strokeStyle = 'rgba(122, 141, 166, 0.28)';
+ this.ctx.lineWidth = 0.9;
+ this.ctx.stroke();
+
+ this.ctx.beginPath();
+ trail.forEach((entry, idx) => {
+ const age = Math.max(0, now - Number(entry.ts || now));
+ const progress = 1 - Math.min(1, age / this.incidentTrailWindowMs);
+ const px = chartX + 8 + progress * (chartW - 16);
+ const py = chartY + chartH - 8 - (Math.max(0, Math.min(100, Number(entry.risk || 0))) / 100) * (chartH - 14);
+ if (idx === 0) this.ctx.moveTo(px, py);
+ else this.ctx.lineTo(px, py);
+ });
+ this.ctx.strokeStyle = 'rgba(151, 210, 255, 0.88)';
+ this.ctx.lineWidth = 1.2;
+ this.ctx.stroke();
+
+ const latest = trail[trail.length - 1];
+ const latestAge = Math.max(0, now - Number(latest.ts || now));
+ const latestProgress = 1 - Math.min(1, latestAge / this.incidentTrailWindowMs);
+ const latestX = chartX + 8 + latestProgress * (chartW - 16);
+ const latestY = chartY + chartH - 8 - (Math.max(0, Math.min(100, Number(latest.risk || 0))) / 100) * (chartH - 14);
+ this.ctx.beginPath();
+ this.ctx.arc(latestX, latestY, 2.6, 0, Math.PI * 2);
+ this.ctx.fillStyle = 'rgba(175, 228, 255, 0.95)';
+ this.ctx.fill();
+
+ this.ctx.fillStyle = 'rgba(154, 176, 206, 0.85)';
+ this.ctx.font = '8px "Share Tech Mono", monospace';
+ this.ctx.fillText(`incident trail ${Math.min(this.incidentTrailWindowMs / 1000, (now - Number(trail[0].ts || now)) / 1000).toFixed(1)}s`, chartX + 8, chartY + 12);
+ this.ctx.fillText(`risk ${Number(latest.risk || 0).toFixed(1)} ${String(latest.trust || '').toUpperCase()}`, chartX + chartW - 154, chartY + 12);
+
+ // Thin halo markers from historical risk samples around the selected node.
+ if (selectedNode) {
+ const historyToDraw = trail.slice(-8);
+ const angle = Number(selectedNode.angle || -Math.PI / 2);
+ historyToDraw.forEach((entry, idx) => {
+ const riskNorm = Math.max(0, Math.min(1, Number(entry.risk || 0) / 100));
+ const rr = selectedNode.innerRadius + (selectedNode.maxRadius - selectedNode.innerRadius) * (0.2 + riskNorm * 0.8);
+ const hx = selectedNode.cx + Math.cos(angle) * rr;
+ const hy = selectedNode.cy + Math.sin(angle) * rr;
+ const alpha = 0.16 + (idx / Math.max(1, historyToDraw.length)) * 0.45;
+ this.ctx.beginPath();
+ this.ctx.arc(hx, hy, 1.1 + idx * 0.1, 0, Math.PI * 2);
+ this.ctx.fillStyle = `rgba(158, 219, 255, ${alpha.toFixed(3)})`;
+ this.ctx.fill();
+ });
+ }
+ }
+
+ updateFocusStatusUi() {
+ const ringTone = {
+ policy: '#8bd0ff',
+ sandbox: '#8ff0bf',
+ threat: '#ff9aa6'
+ };
+ if (this.focusStatusNode) {
+ if (!this.focusedRingKey) {
+ this.focusStatusNode.textContent = 'FOCUS: NONE';
+ this.focusStatusNode.style.color = 'rgba(170, 188, 214, 0.88)';
+ } else {
+ const ringLabel = String(this.focusedRingKey).toUpperCase();
+ this.focusStatusNode.textContent = this.ringFocusPinned
+ ? `FOCUS: ${ringLabel} (PINNED)`
+ : `FOCUS: ${ringLabel}`;
+ this.focusStatusNode.style.color = ringTone[this.focusedRingKey] || 'rgba(170, 188, 214, 0.88)';
+ }
+ }
+ if (this.unpinButton) {
+ const active = Boolean(this.ringFocusPinned && this.focusedRingKey);
+ this.unpinButton.style.background = active ? 'rgba(32, 52, 81, 0.92)' : 'rgba(8, 12, 18, 0.56)';
+ this.unpinButton.style.borderColor = active ? 'rgba(124, 178, 255, 0.9)' : 'rgba(150, 164, 188, 0.22)';
+ this.unpinButton.style.color = active ? '#d9ecff' : 'rgba(188, 200, 219, 0.55)';
+ this.unpinButton.style.cursor = active ? 'pointer' : 'not-allowed';
+ }
}
addExitButton() {
@@ -239,6 +530,8 @@ class SecuritySubsystemVisualization {
throw new Error(data?.error || 'No security data');
}
this.telemetry = data;
+ const trustRows = Array.isArray(data?.trust_graph) ? data.trust_graph : [];
+ this.updateSelectedIncidentTrail(trustRows);
})
.catch(() => {
this.telemetry = {
@@ -259,6 +552,14 @@ class SecuritySubsystemVisualization {
const rect = this.canvas.getBoundingClientRect();
const x = event.clientX - rect.left;
const y = event.clientY - rect.top;
+ const ringHit = this.getRingHitAt(x, y);
+ if (ringHit) {
+ this.focusedRingKey = this.focusedRingKey === ringHit.key ? null : ringHit.key;
+ this.ringFocusPinned = false;
+ this.applyRingFocusVerdictFilter();
+ this.updateFocusStatusUi();
+ return;
+ }
let matched = null;
for (const node of this.trustNodeHitAreas) {
const dx = x - node.x;
@@ -281,6 +582,7 @@ class SecuritySubsystemVisualization {
const pid = Number(matched.pid || 0);
if (this.selectedProcessFilter && Number(this.selectedProcessFilter.pid || 0) === pid) {
this.selectedProcessFilter = null;
+ this.selectedIncidentTrail = [];
this.updateClearProcessButtonState();
return;
}
@@ -288,14 +590,73 @@ class SecuritySubsystemVisualization {
pid,
name: this.normalizeName(matched.name || 'unknown')
};
+ this.selectedIncidentTrail = [];
this.updateClearProcessButtonState();
}
+ onCanvasDoubleClick(event) {
+ if (!this.canvas) return;
+ const rect = this.canvas.getBoundingClientRect();
+ const x = event.clientX - rect.left;
+ const y = event.clientY - rect.top;
+ const ringHit = this.getRingHitAt(x, y);
+ if (!ringHit) return;
+ this.focusedRingKey = ringHit.key;
+ this.ringFocusPinned = true;
+ this.applyRingFocusVerdictFilter();
+ this.updateFocusStatusUi();
+ this.triggerPanelFocusFromRing(ringHit.key);
+ }
+
+ applyRingFocusVerdictFilter() {
+ if (!this.focusedRingKey) {
+ this.ringDerivedVerdictFilter = null;
+ this.ringFocusPinned = false;
+ this.updateFocusStatusUi();
+ return;
+ }
+ const map = {
+ policy: 'audit',
+ sandbox: 'allow',
+ threat: 'deny'
+ };
+ this.ringDerivedVerdictFilter = map[this.focusedRingKey] || null;
+ this.updateFocusStatusUi();
+ }
+
+ triggerPanelFocusFromRing(ringKey) {
+ const map = {
+ policy: 'lsm',
+ sandbox: 'seccomp',
+ threat: 'attack'
+ };
+ const panelKey = map[ringKey] || 'pipeline';
+ this.panelSpotlight = {
+ panelKey,
+ startedAt: performance.now(),
+ durationMs: 2100
+ };
+ }
+
+ getRingHitAt(x, y) {
+ for (const ring of this.ringHitAreas) {
+ const dx = x - ring.cx;
+ const dy = y - ring.cy;
+ const radiusNorm = Math.sqrt((dx * dx) / (ring.rx * ring.rx) + (dy * dy) / (ring.ry * ring.ry));
+ if (radiusNorm >= ring.hitMin && radiusNorm <= ring.hitMax) {
+ return ring;
+ }
+ }
+ return null;
+ }
+
onCanvasMouseMove(event) {
if (!this.canvas) return;
const rect = this.canvas.getBoundingClientRect();
const x = event.clientX - rect.left;
const y = event.clientY - rect.top;
+ const ringHit = this.getRingHitAt(x, y);
+ this.hoveredRingKey = ringHit ? ringHit.key : null;
let hovered = null;
for (const node of this.trustNodeHitAreas) {
const dx = x - node.x;
@@ -315,7 +676,7 @@ class SecuritySubsystemVisualization {
}
}
this.hoveredProcessPid = hovered;
- this.canvas.style.cursor = hovered ? 'pointer' : 'default';
+ this.canvas.style.cursor = (hovered || ringHit) ? 'pointer' : 'default';
}
drawRoundedRect(x, y, w, h, r) {
@@ -334,15 +695,28 @@ class SecuritySubsystemVisualization {
ctx.closePath();
}
- drawPanel(x, y, w, h, title) {
+ isPanelInFocusedRing(panelKey) {
+ if (!this.focusedRingKey) return true;
+ const map = {
+ policy: new Set(['lsm', 'core', 'pipeline']),
+ sandbox: new Set(['seccomp', 'capabilities', 'core']),
+ threat: new Set(['attack', 'trust', 'pipeline', 'core'])
+ };
+ const set = map[this.focusedRingKey];
+ if (!set) return true;
+ return set.has(panelKey);
+ }
+
+ drawPanel(x, y, w, h, title, panelKey = 'generic') {
this.drawRoundedRect(x, y, w, h, 8);
- this.ctx.fillStyle = 'rgba(8, 11, 16, 0.88)';
+ const active = this.isPanelInFocusedRing(panelKey);
+ this.ctx.fillStyle = active ? 'rgba(8, 11, 16, 0.9)' : 'rgba(8, 11, 16, 0.5)';
this.ctx.fill();
- this.ctx.strokeStyle = 'rgba(165, 178, 200, 0.35)';
+ this.ctx.strokeStyle = active ? 'rgba(165, 178, 200, 0.4)' : 'rgba(110, 122, 140, 0.2)';
this.ctx.lineWidth = 0.95;
this.ctx.stroke();
- this.ctx.fillStyle = '#d8e5f7';
+ this.ctx.fillStyle = active ? '#d8e5f7' : 'rgba(180, 196, 218, 0.52)';
this.ctx.font = '13px "Share Tech Mono", monospace';
this.ctx.fillText(title, x + 14, y + 20);
}
@@ -362,18 +736,42 @@ class SecuritySubsystemVisualization {
}
drawDecisionPipeline(x, y, w, h, telemetry) {
- this.drawPanel(x, y, w, h, 'THREAT DECISION PIPELINE');
+ this.drawPanel(x, y, w, h, 'THREAT DECISION PIPELINE', 'pipeline');
const lanes = Array.isArray(telemetry?.pipeline?.lanes) ? telemetry.pipeline.lanes : [];
+ const effectiveVerdictFilter = this.ringDerivedVerdictFilter || this.verdictFilter;
const selected = this.selectedProcessFilter;
const filtered = lanes.filter((lane) => {
- const verdictMatch = this.verdictFilter === 'all' || lane.verdict === this.verdictFilter;
+ const verdictMatch = effectiveVerdictFilter === 'all' || lane.verdict === effectiveVerdictFilter;
if (!verdictMatch) return false;
if (!selected) return true;
const lanePid = Number(lane.pid || 0);
const laneName = this.normalizeName(lane.process);
return lanePid === Number(selected.pid || 0) || laneName === this.normalizeName(selected.name);
});
- const laneRows = filtered.slice(0, 7);
+ const priorityScore = (lane) => {
+ const verdict = String(lane.verdict || '').toLowerCase();
+ const hook = String(lane.hook || '').toLowerCase();
+ if (this.focusedRingKey === 'threat') {
+ let score = verdict === 'deny' ? 100 : (verdict === 'audit' ? 75 : 40);
+ if (hook.includes('netfilter') || hook.includes('lsm')) score += 10;
+ return score;
+ }
+ if (this.focusedRingKey === 'sandbox') {
+ let score = hook.includes('seccomp') ? 95 : (hook.includes('cap') ? 82 : 55);
+ if (verdict === 'allow') score += 8;
+ return score;
+ }
+ if (this.focusedRingKey === 'policy') {
+ let score = hook.includes('lsm') ? 95 : (hook.includes('policy') ? 80 : 56);
+ if (verdict === 'audit') score += 10;
+ return score;
+ }
+ return 0;
+ };
+ const laneRows = filtered
+ .slice()
+ .sort((a, b) => priorityScore(b) - priorityScore(a))
+ .slice(0, 7);
const stageXs = [x + 18, x + Math.floor(w * 0.44), x + Math.floor(w * 0.78)];
const stageW = 130;
@@ -421,60 +819,174 @@ class SecuritySubsystemVisualization {
if (selected) {
this.ctx.fillText(`NO EVENTS FOR ${selected.name || 'PROCESS'} + FILTER`, x + 18, y + h - 16);
} else {
- this.ctx.fillText('NO EVENTS FOR CURRENT FILTER', x + 18, y + h - 16);
+ this.ctx.fillText(`NO EVENTS FOR CURRENT FILTER (${String(effectiveVerdictFilter).toUpperCase()})`, x + 18, y + h - 16);
}
+ } else if (this.focusedRingKey) {
+ this.ctx.fillStyle = 'rgba(149, 189, 234, 0.82)';
+ this.ctx.font = '9px "Share Tech Mono", monospace';
+ this.ctx.fillText(`ring focus: ${this.focusedRingKey} / verdict ${String(effectiveVerdictFilter).toUpperCase()}`, x + 18, y + h - 16);
}
}
drawTrustGraph(x, y, w, h, telemetry) {
- this.drawPanel(x, y, w, h, 'PROCESS TRUST GRAPH');
+ this.drawPanel(x, y, w, h, 'PROCESS TRUST GRAPH', 'trust');
this.trustNodeHitAreas = [];
- const rows = Array.isArray(telemetry?.trust_graph) ? telemetry.trust_graph.slice(0, 10) : [];
+ const mode = this.trustGraphMode || 'topology';
+ const nodeLimit = mode === 'forensics' ? 12 : 10;
+ const rows = Array.isArray(telemetry?.trust_graph) ? telemetry.trust_graph.slice(0, nodeLimit) : [];
const cx = x + Math.floor(w * 0.5);
- const cy = y + Math.floor(h * 0.56);
- const radius = Math.min(w, h) * 0.28;
+ const cy = y + Math.floor(h * 0.58);
+ const maxRadius = Math.min(w, h) * 0.31;
+ const innerRadius = Math.max(30, Math.floor(maxRadius * 0.24));
+ const now = performance.now();
+
+ const highRiskCount = rows.filter((row) => Number(row?.risk_score || 0) >= 70).length;
+ this.ctx.fillStyle = 'rgba(168, 186, 210, 0.82)';
+ this.ctx.font = '9px "Share Tech Mono", monospace';
+ this.ctx.fillText(`nodes ${rows.length}`, x + 14, y + 34);
+ this.ctx.fillStyle = highRiskCount > 0 ? 'rgba(255, 164, 170, 0.92)' : 'rgba(146, 226, 181, 0.86)';
+ this.ctx.fillText(`high risk ${highRiskCount}`, x + 92, y + 34);
+ if (this.selectedProcessFilter) {
+ this.ctx.fillStyle = 'rgba(141, 197, 255, 0.92)';
+ this.ctx.fillText(`selected pid ${Number(this.selectedProcessFilter.pid || 0)}`, x + 210, y + 34);
+ }
+ this.ctx.fillStyle = mode === 'risk'
+ ? 'rgba(255, 176, 155, 0.95)'
+ : (mode === 'forensics' ? 'rgba(168, 219, 255, 0.95)' : 'rgba(173, 194, 219, 0.86)');
+ this.ctx.fillText(`mode ${String(mode).toUpperCase()}`, x + w - 122, y + 34);
+ if (mode === 'forensics') {
+ const freezeLeftSec = Math.max(0, (this.trustForensicsFreezeUntil - now) / 1000);
+ this.ctx.fillStyle = 'rgba(148, 198, 255, 0.88)';
+ this.ctx.fillText(`freeze ${freezeLeftSec.toFixed(1)}s`, x + w - 208, y + 34);
+ }
+
+ // Risk zones: inner = stable, middle = caution, outer = critical.
+ const zones = [
+ {
+ r: maxRadius * 0.48,
+ color: mode === 'risk' ? 'rgba(142, 236, 181, 0.14)' : 'rgba(113, 222, 163, 0.12)'
+ },
+ {
+ r: maxRadius * 0.76,
+ color: mode === 'risk' ? 'rgba(255, 213, 128, 0.16)' : 'rgba(244, 201, 119, 0.11)'
+ },
+ {
+ r: maxRadius,
+ color: mode === 'risk' ? 'rgba(255, 136, 148, 0.15)' : 'rgba(255, 143, 152, 0.1)'
+ }
+ ];
+ zones.forEach((zone) => {
+ this.ctx.beginPath();
+ this.ctx.arc(cx, cy, zone.r, 0, Math.PI * 2);
+ this.ctx.fillStyle = zone.color;
+ this.ctx.fill();
+ this.ctx.strokeStyle = 'rgba(162, 176, 198, 0.18)';
+ this.ctx.lineWidth = 0.8;
+ this.ctx.stroke();
+ });
this.ctx.beginPath();
- this.ctx.arc(cx, cy, radius + 24, 0, Math.PI * 2);
- this.ctx.strokeStyle = 'rgba(162, 176, 198, 0.18)';
- this.ctx.stroke();
- this.ctx.beginPath();
- this.ctx.arc(cx, cy, radius, 0, Math.PI * 2);
- this.ctx.strokeStyle = 'rgba(162, 176, 198, 0.32)';
- this.ctx.stroke();
- this.ctx.beginPath();
- this.ctx.arc(cx, cy, 26, 0, Math.PI * 2);
- this.ctx.fillStyle = 'rgba(7, 10, 15, 0.9)';
+ this.ctx.arc(cx, cy, innerRadius, 0, Math.PI * 2);
+ this.ctx.fillStyle = 'rgba(7, 10, 15, 0.92)';
this.ctx.fill();
- this.ctx.strokeStyle = 'rgba(162, 176, 198, 0.32)';
+ this.ctx.strokeStyle = 'rgba(162, 176, 198, 0.34)';
+ this.ctx.lineWidth = 1.1;
this.ctx.stroke();
this.ctx.fillStyle = '#cde6ff';
this.ctx.font = '10px "Share Tech Mono", monospace';
this.ctx.fillText('LSM', cx - 10, cy + 3);
+ if (!rows.length) {
+ this.ctx.fillStyle = 'rgba(178, 195, 219, 0.75)';
+ this.ctx.font = '10px "Share Tech Mono", monospace';
+ this.ctx.fillText('no trust graph data', cx - 54, cy + maxRadius + 16);
+ this.trustForensicsLayout.clear();
+ return;
+ }
+
+ if (mode === 'forensics') {
+ const pidSet = new Set(rows.map((row) => Number(row?.pid || 0)));
+ let layoutMismatch = this.trustForensicsLayout.size !== pidSet.size;
+ if (!layoutMismatch) {
+ for (const pid of pidSet) {
+ if (!this.trustForensicsLayout.has(pid)) {
+ layoutMismatch = true;
+ break;
+ }
+ }
+ }
+ const shouldRebuild = now >= this.trustForensicsFreezeUntil || this.trustForensicsLayout.size === 0 || layoutMismatch;
+ if (shouldRebuild) {
+ this.trustForensicsLayout = this.buildForensicsLayout(rows, cx, cy, innerRadius, maxRadius);
+ this.trustForensicsFreezeUntil = now + this.trustForensicsFreezeWindowMs;
+ }
+ }
+
+ const riskBandColor = (risk) => {
+ if (risk >= 70) return '#ff8f97';
+ if (risk >= 45) return '#f4c977';
+ return '#71dfa8';
+ };
+ let selectedNode = null;
+
rows.forEach((row, idx) => {
- const a = ((Math.PI * 2) / Math.max(rows.length, 1)) * idx - Math.PI / 2;
- const nx = cx + Math.cos(a) * radius;
- const ny = cy + Math.sin(a) * radius;
+ const risk = Number(row.risk_score || 0);
+ const riskNorm = Math.max(0, Math.min(1, risk / 100));
+ const pid = Number(row.pid || 0);
+ const forensicsEntry = mode === 'forensics' ? this.trustForensicsLayout.get(pid) : null;
+ const a = forensicsEntry
+ ? Number(forensicsEntry.angle || 0)
+ : (((Math.PI * 2) / Math.max(rows.length, 1)) * idx - Math.PI / 2);
+ const baseOrbit = mode === 'risk'
+ ? innerRadius + (maxRadius - innerRadius) * (0.2 + riskNorm * 0.8)
+ : innerRadius + (maxRadius - innerRadius) * (0.35 + riskNorm * 0.65);
+ const orbit = forensicsEntry
+ ? Number(forensicsEntry.orbit || baseOrbit)
+ : Math.max(innerRadius + 8, baseOrbit);
+ const nx = forensicsEntry
+ ? Number(forensicsEntry.nx || (cx + Math.cos(a) * orbit))
+ : (cx + Math.cos(a) * orbit);
+ const ny = forensicsEntry
+ ? Number(forensicsEntry.ny || (cy + Math.sin(a) * orbit))
+ : (cy + Math.sin(a) * orbit);
const trust = String(row.trust || 'trusted');
- const color = this.trustColor(trust);
+ const color = mode === 'risk' ? riskBandColor(risk) : this.trustColor(trust);
const pulse = 0.78 + 0.22 * Math.sin(this.tick * 0.04 + idx * 0.5);
- const pid = Number(row.pid || 0);
- const isSelected = this.selectedProcessFilter && Number(this.selectedProcessFilter.pid || 0) === pid;
+ const isSelected = Boolean(this.selectedProcessFilter) && (
+ Number(this.selectedProcessFilter.pid || 0) === pid
+ || this.normalizeName(this.selectedProcessFilter.name || '') === this.normalizeName(row.name || '')
+ );
const isHovered = Number(this.hoveredProcessPid || 0) === pid;
- const nodeR = isSelected ? 12 : (isHovered ? 11 : 10);
- const glowR = isSelected ? 18 : (isHovered ? 15 : 0);
+ const isCritical = risk >= 70 || trust === 'blocked';
+ const nodeR = isSelected ? 12 : (isHovered ? 11 : (isCritical ? 10 : (mode === 'forensics' ? 9.5 : 9)));
+ const glowR = isSelected ? 20 : (isHovered ? 16 : (isCritical ? 11 : 0));
+ if (isSelected) {
+ selectedNode = {
+ cx,
+ cy,
+ angle: a,
+ innerRadius,
+ maxRadius
+ };
+ }
this.ctx.beginPath();
this.ctx.moveTo(cx, cy);
this.ctx.lineTo(nx, ny);
- this.ctx.strokeStyle = `rgba(155, 168, 190, ${0.20 + 0.15 * pulse})`;
- this.ctx.lineWidth = isSelected ? 1.15 : 0.9;
+ this.ctx.strokeStyle = isCritical
+ ? `rgba(255, 147, 156, ${0.24 + 0.22 * pulse})`
+ : `rgba(155, 168, 190, ${0.18 + 0.14 * pulse})`;
+ this.ctx.lineWidth = isSelected ? 1.2 : 0.9;
this.ctx.stroke();
if (glowR > 0) {
const glow = this.ctx.createRadialGradient(nx, ny, 0, nx, ny, glowR);
- glow.addColorStop(0, isSelected ? 'rgba(124, 178, 255, 0.38)' : 'rgba(138, 156, 234, 0.24)');
+ glow.addColorStop(
+ 0,
+ isSelected
+ ? 'rgba(124, 178, 255, 0.4)'
+ : (isCritical ? 'rgba(255, 137, 149, 0.26)' : 'rgba(138, 156, 234, 0.22)')
+ );
glow.addColorStop(1, 'rgba(124, 178, 255, 0)');
this.ctx.beginPath();
this.ctx.arc(nx, ny, glowR, 0, Math.PI * 2);
@@ -493,10 +1005,22 @@ class SecuritySubsystemVisualization {
this.ctx.stroke();
this.ctx.lineWidth = 1;
- this.ctx.fillStyle = '#cfdced';
+ const rightSide = Math.cos(a) >= 0;
+ const labelX = rightSide ? (nx + 14) : (nx - (mode === 'forensics' ? 116 : 98));
+ const labelY = ny + 18;
+ this.ctx.fillStyle = isSelected ? '#dff0ff' : 'rgba(207, 220, 237, 0.9)';
this.ctx.font = '9px "Share Tech Mono", monospace';
- const label = `${String(row.name || 'proc').slice(0, 9)} (${row.risk_score || 0})`;
- this.ctx.fillText(label, nx - 32, ny + 21);
+ const label = `${String(row.name || 'proc').slice(0, 10)} #${pid}`;
+ const shouldShowLabel = mode === 'forensics' || isSelected || isHovered || isCritical;
+ if (shouldShowLabel) {
+ this.ctx.fillText(label, labelX, labelY);
+ this.ctx.fillStyle = isCritical ? 'rgba(255, 166, 173, 0.95)' : 'rgba(164, 186, 212, 0.86)';
+ this.ctx.fillText(`risk ${risk.toFixed(0)} ${String(trust).toUpperCase()}`, labelX, labelY + 10);
+ if (mode === 'forensics') {
+ this.ctx.fillStyle = 'rgba(151, 173, 202, 0.8)';
+ this.ctx.fillText(`orbit ${Math.round(orbit)} freeze ${(Math.max(0, this.trustForensicsFreezeUntil - now) / 1000).toFixed(1)}s`, labelX, labelY + 20);
+ }
+ }
this.trustNodeHitAreas.push({
x: nx,
@@ -506,10 +1030,12 @@ class SecuritySubsystemVisualization {
name: row.name || 'unknown'
});
});
+
+ this.drawIncidentTrail(x, y, w, h, selectedNode);
}
drawAttackSurface(x, y, w, h, telemetry) {
- this.drawPanel(x, y, w, h, 'ATTACK SURFACE MAP');
+ this.drawPanel(x, y, w, h, 'ATTACK SURFACE MAP', 'attack');
const rows = Array.isArray(telemetry?.attack_surface) ? telemetry.attack_surface.slice(0, 8) : [];
rows.forEach((row, idx) => {
@@ -534,7 +1060,7 @@ class SecuritySubsystemVisualization {
}
drawLsmStatusCard(x, y, w, h, telemetry) {
- this.drawPanel(x, y, w, h, 'LSM STATUS MATRIX');
+ this.drawPanel(x, y, w, h, 'LSM STATUS MATRIX', 'lsm');
const rows = Array.isArray(telemetry?.security_tools?.lsm_status)
? telemetry.security_tools.lsm_status.slice(0, 6)
: [];
@@ -558,7 +1084,7 @@ class SecuritySubsystemVisualization {
}
drawCapabilitiesCard(x, y, w, h, telemetry) {
- this.drawPanel(x, y, w, h, 'CAPABILITIES DRIFT');
+ this.drawPanel(x, y, w, h, 'CAPABILITIES DRIFT', 'capabilities');
this.capabilityRowHitAreas = [];
const rows = Array.isArray(telemetry?.security_tools?.capabilities_drift)
? telemetry.security_tools.capabilities_drift.slice(0, 6)
@@ -601,7 +1127,7 @@ class SecuritySubsystemVisualization {
}
drawSeccompCoverageCard(x, y, w, h, telemetry) {
- this.drawPanel(x, y, w, h, 'SECCOMP COVERAGE');
+ this.drawPanel(x, y, w, h, 'SECCOMP COVERAGE', 'seccomp');
const cov = telemetry?.security_tools?.seccomp_coverage || {};
const none = Number(cov.none || 0);
const strict = Number(cov.strict || 0);
@@ -637,8 +1163,186 @@ class SecuritySubsystemVisualization {
});
}
+ drawGhostShellSecurityRings(x, y, w, h, telemetry) {
+ const ctx = this.ctx;
+ const clamp01 = (n) => Math.max(0, Math.min(1, Number(n) || 0));
+ const cx = x + Math.floor(w * 0.5);
+ const cy = y + Math.floor(h * 0.49);
+ const baseRx = Math.max(120, Math.floor(w * 0.2));
+ const baseRy = Math.max(42, Math.floor(h * 0.15));
+ const t = this.tick * 0.013;
+
+ const meta = telemetry?.meta || {};
+ const core = telemetry?.security_core || {};
+ const seccompCov = telemetry?.security_tools?.seccomp_coverage || {};
+ const attackRows = Array.isArray(telemetry?.attack_surface) ? telemetry.attack_surface : [];
+ const capRows = Array.isArray(telemetry?.security_tools?.capabilities_drift) ? telemetry.security_tools.capabilities_drift : [];
+ const lsmEngines = Array.isArray(core.lsm_engines) ? core.lsm_engines : [];
+
+ const decisionsPerSec = Number(meta.decisions_per_sec || 0);
+ const events = Math.max(1, Number(meta.events || 0));
+ const blocked = Number(meta.blocked || 0);
+ const suspicious = Number(meta.suspicious || 0);
+ const enforcingCount = lsmEngines.filter((item) => String(item.status || '').toLowerCase() === 'enforcing').length;
+ const lsmCount = Math.max(1, lsmEngines.length);
+ const seccompCoverage = Number(seccompCov.coverage_percent || 0);
+ const unsandboxedCount = Array.isArray(seccompCov.high_risk_unsandboxed) ? seccompCov.high_risk_unsandboxed.length : 0;
+ const dangerousCapsCount = capRows.reduce((acc, row) => acc + Number(row?.dangerous_count || 0), 0);
+ const avgAttackRisk = attackRows.length > 0
+ ? attackRows.reduce((acc, row) => acc + Number(row?.risk_score || 0), 0) / attackRows.length
+ : 0;
+
+ const policyScore = clamp01((decisionsPerSec / 2400) * 0.55 + (enforcingCount / lsmCount) * 0.45);
+ const sandboxStrength = clamp01((seccompCoverage / 100) * 0.72 + (1 - clamp01((unsandboxedCount + dangerousCapsCount * 0.3) / 18)) * 0.28);
+ const threatPressure = clamp01(((blocked + suspicious * 0.55) / events) * 1.9 + (avgAttackRisk / 100) * 0.65 + (unsandboxedCount / 12) * 0.4);
+
+ const verdict = threatPressure >= 0.72
+ ? 'UNDER ATTACK'
+ : (sandboxStrength < 0.42 || threatPressure >= 0.46 ? 'DEGRADED' : 'SAFE');
+ const verdictColor = verdict === 'UNDER ATTACK' ? '#ff8f98' : (verdict === 'DEGRADED' ? '#f4c977' : '#71dfa8');
+
+ ctx.save();
+ ctx.globalCompositeOperation = 'lighter';
+
+ const rings = [
+ {
+ key: 'policy',
+ label: 'POLICY',
+ score: policyScore,
+ speed: 0.32,
+ color: '126,196,255',
+ rx: baseRx * 0.88,
+ ry: baseRy * 0.9,
+ metric: `lsm ${enforcingCount}/${lsmCount} dec ${decisionsPerSec.toFixed(1)}/s`
+ },
+ {
+ key: 'sandbox',
+ label: 'SANDBOX',
+ score: sandboxStrength,
+ speed: -0.26,
+ color: '120,230,174',
+ rx: baseRx * 1.16,
+ ry: baseRy * 1.3,
+ metric: `seccomp ${seccompCoverage.toFixed(1)}% unsbx ${unsandboxedCount}`
+ },
+ {
+ key: 'threat',
+ label: 'THREAT',
+ score: threatPressure,
+ speed: 0.18,
+ color: '255,148,156',
+ rx: baseRx * 1.42,
+ ry: baseRy * 1.62,
+ metric: `risk ${avgAttackRisk.toFixed(1)} block ${blocked}`
+ }
+ ];
+
+ this.ringHitAreas = [];
+ rings.forEach((ring, idx) => {
+ const phase = t * ring.speed + idx * 1.27;
+ const isFocused = this.focusedRingKey === ring.key;
+ const isHovered = this.hoveredRingKey === ring.key;
+ const dimmed = Boolean(this.focusedRingKey) && !isFocused;
+ const alphaBase = 0.12 + ring.score * 0.24;
+ const arcSpan = 0.52 + ring.score * 2.2;
+ const emphasis = isFocused ? 1.25 : (isHovered ? 1.12 : 1);
+ const alphaMul = dimmed ? 0.32 : (isFocused ? 1.2 : 1);
+
+ // Base orbit.
+ ctx.strokeStyle = `rgba(${ring.color}, ${Math.min(1, alphaBase * alphaMul).toFixed(3)})`;
+ ctx.lineWidth = (1 + ring.score * 1.6) * emphasis;
+ ctx.beginPath();
+ ctx.ellipse(cx, cy, ring.rx, ring.ry, -0.08, 0, Math.PI * 2);
+ ctx.stroke();
+
+ // Informative arc indicating score magnitude.
+ ctx.strokeStyle = `rgba(${ring.color}, ${Math.min(1, (0.42 + ring.score * 0.5) * alphaMul).toFixed(3)})`;
+ ctx.lineWidth = (2 + ring.score * 2.4) * emphasis;
+ ctx.beginPath();
+ ctx.ellipse(cx, cy, ring.rx, ring.ry, -0.08, phase, phase + arcSpan);
+ ctx.stroke();
+
+ // Orbiting probes.
+ for (let i = 0; i < 3; i++) {
+ const a = phase + i * 2.09;
+ const px = cx + Math.cos(a) * ring.rx;
+ const py = cy + Math.sin(a) * ring.ry;
+ ctx.fillStyle = `rgba(${ring.color}, ${Math.min(1, (0.3 + i * 0.16) * alphaMul).toFixed(3)})`;
+ ctx.beginPath();
+ ctx.arc(px, py, (1.3 + i * 0.55) * emphasis, 0, Math.PI * 2);
+ ctx.fill();
+ }
+
+ // Ring label + core metric near arc.
+ const lx = cx + Math.cos(phase + arcSpan + 0.16) * (ring.rx + 12);
+ const ly = cy + Math.sin(phase + arcSpan + 0.16) * (ring.ry + 12);
+ ctx.fillStyle = `rgba(${ring.color}, ${Math.min(1, 0.86 * alphaMul + (isFocused ? 0.12 : 0)).toFixed(3)})`;
+ ctx.font = '9px "Share Tech Mono", monospace';
+ ctx.fillText(`${ring.label} ${(ring.score * 100).toFixed(0)}%`, lx, ly);
+ ctx.fillStyle = `rgba(${ring.color}, ${Math.min(1, 0.54 * alphaMul).toFixed(3)})`;
+ ctx.font = '8px "Share Tech Mono", monospace';
+ ctx.fillText(ring.metric, lx, ly + 10);
+
+ this.ringHitAreas.push({
+ key: ring.key,
+ cx,
+ cy,
+ rx: ring.rx,
+ ry: ring.ry,
+ hitMin: 0.84,
+ hitMax: 1.18
+ });
+ });
+
+ // Center verdict core.
+ this.drawRoundedRect(cx - 118, cy - 40, 236, 74, 10);
+ ctx.fillStyle = 'rgba(8, 13, 20, 0.64)';
+ ctx.fill();
+ ctx.strokeStyle = 'rgba(150, 178, 214, 0.42)';
+ ctx.lineWidth = 1.2;
+ ctx.stroke();
+ ctx.fillStyle = 'rgba(164, 190, 223, 0.92)';
+ ctx.font = '9px "Share Tech Mono", monospace';
+ ctx.fillText('RING-FIRST SECURITY VERDICT', cx - 96, cy - 20);
+ ctx.fillStyle = verdictColor;
+ ctx.font = '15px "Share Tech Mono", monospace';
+ ctx.fillText(verdict, cx - 66, cy + 2);
+ ctx.fillStyle = 'rgba(176, 198, 228, 0.8)';
+ ctx.font = '8px "Share Tech Mono", monospace';
+ ctx.fillText(`threat ${(threatPressure * 100).toFixed(0)}% sandbox ${(sandboxStrength * 100).toFixed(0)}% policy ${(policyScore * 100).toFixed(0)}%`, cx - 108, cy + 18);
+
+ // Ambient data noise strings around outer ring.
+ const tokenPool = [
+ `deny:${blocked}`,
+ `sus:${suspicious}`,
+ `risk:${avgAttackRisk.toFixed(0)}`,
+ `sec:${seccompCoverage.toFixed(0)}%`,
+ `dec:${decisionsPerSec.toFixed(1)}/s`,
+ `unsbx:${unsandboxedCount}`,
+ 'lsm',
+ 'seccomp',
+ 'cap',
+ 'audit'
+ ];
+ ctx.font = '8px "Share Tech Mono", monospace';
+ ctx.fillStyle = 'rgba(150, 216, 255, 0.5)';
+ for (let i = 0; i < 12; i++) {
+ const token = tokenPool[(i + Math.floor(this.tick / 8)) % tokenPool.length];
+ const a = t * 0.29 + i * (Math.PI * 2 / 12);
+ const rx = baseRx * 1.62;
+ const ry = baseRy * 1.92;
+ const px = cx + Math.cos(a) * rx;
+ const py = cy + Math.sin(a) * ry;
+ ctx.fillText(token, px, py);
+ }
+ ctx.fillStyle = 'rgba(170, 194, 224, 0.68)';
+ ctx.fillText('click ring to focus related panels', cx - 90, cy + baseRy * 1.98);
+
+ ctx.restore();
+ }
+
drawSecurityCore(x, y, w, h, telemetry) {
- this.drawPanel(x, y, w, h, 'KERNEL SECURITY CORE');
+ this.drawPanel(x, y, w, h, 'KERNEL SECURITY CORE', 'core');
const core = telemetry?.security_core || {};
const lsmEngines = Array.isArray(core.lsm_engines) ? core.lsm_engines : [];
const seccompProcs = Array.isArray(core.seccomp_processes) ? core.seccomp_processes : [];
@@ -793,6 +1497,35 @@ class SecuritySubsystemVisualization {
}
}
+ drawPanelSpotlight() {
+ if (!this.panelSpotlight || !this.ctx) return;
+ const rect = this.panelRects[this.panelSpotlight.panelKey];
+ if (!rect) return;
+ const now = performance.now();
+ const elapsed = now - Number(this.panelSpotlight.startedAt || now);
+ const duration = Math.max(800, Number(this.panelSpotlight.durationMs || 1800));
+ if (elapsed >= duration) {
+ this.panelSpotlight = null;
+ return;
+ }
+
+ const t = elapsed / duration;
+ const pulse = 0.5 + 0.5 * Math.sin(elapsed * 0.012);
+ const alpha = (1 - t) * (0.34 + pulse * 0.24);
+ const expand = 4 + pulse * 7;
+ const x = rect.x - expand;
+ const y = rect.y - expand;
+ const w = rect.w + expand * 2;
+ const h = rect.h + expand * 2;
+
+ this.ctx.save();
+ this.drawRoundedRect(x, y, w, h, 10);
+ this.ctx.strokeStyle = `rgba(154, 220, 255, ${alpha.toFixed(3)})`;
+ this.ctx.lineWidth = 2.1 + pulse * 1.3;
+ this.ctx.stroke();
+ this.ctx.restore();
+ }
+
drawScene() {
if (!this.ctx || !this.canvas) return;
const w = window.innerWidth;
@@ -851,14 +1584,27 @@ class SecuritySubsystemVisualization {
const tools2X = leftX + toolsW + gap;
const tools3X = tools2X + toolsW + gap;
const coreY = toolsY + toolsH + gap;
+ this.panelRects = {
+ pipeline: { x: leftX, y: panelTop, w: leftW, h: panelH },
+ trust: { x: centerX, y: panelTop, w: centerW, h: panelH },
+ attack: { x: rightX, y: panelTop, w: rightW, h: panelH },
+ lsm: { x: leftX, y: toolsY, w: toolsW, h: toolsH },
+ capabilities: { x: tools2X, y: toolsY, w: toolsW, h: toolsH },
+ seccomp: { x: tools3X, y: toolsY, w: toolsW, h: toolsH },
+ core: { x: leftX, y: coreY, w: w - gap * 2, h: coreH }
+ };
+ this.drawGhostShellSecurityRings(leftX, coreY, w - gap * 2, coreH, this.telemetry);
this.drawDecisionPipeline(leftX, panelTop, leftW, panelH, this.telemetry);
this.drawTrustGraph(centerX, panelTop, centerW, panelH, this.telemetry);
this.drawAttackSurface(rightX, panelTop, rightW, panelH, this.telemetry);
this.drawLsmStatusCard(leftX, toolsY, toolsW, toolsH, this.telemetry);
this.drawCapabilitiesCard(tools2X, toolsY, toolsW, toolsH, this.telemetry);
this.drawSeccompCoverageCard(tools3X, toolsY, toolsW, toolsH, this.telemetry);
- this.drawSecurityCore(leftX, coreY, w - gap * 2, coreH, this.telemetry);
+ if (this.showSecurityCorePanel) {
+ this.drawSecurityCore(leftX, coreY, w - gap * 2, coreH, this.telemetry);
+ }
+ this.drawPanelSpotlight();
}
animate() {