diff --git a/kernel_ai/services/system_view.py b/kernel_ai/services/system_view.py
index e4b1025..51896c3 100644
--- a/kernel_ai/services/system_view.py
+++ b/kernel_ai/services/system_view.py
@@ -285,6 +285,8 @@ def get_isolation_context():
namespace_keys = ["mnt", "pid", "net", "ipc", "uts", "user"]
namespace_labels = {"mnt": "MNT", "pid": "PID", "net": "NET", "ipc": "IPC", "uts": "UTS", "user": "USER"}
namespace_counts = {k: {} for k in namespace_keys}
+ # Per-namespace, per-inode sample process names (each inode = one isolated "world").
+ namespace_samples = {k: {} for k in namespace_keys}
cgroup_aggregates = {}
total_scanned = 0
@@ -305,11 +307,15 @@ def get_isolation_context():
process_name = proc.info.get("name") or "unknown"
agg["sample_processes"].append(process_name)
+ proc_name = proc.info.get("name") or "unknown"
for ns_name in namespace_keys:
inode = read_namespace_inode(pid, ns_name)
if inode:
ns_map = namespace_counts[ns_name]
ns_map[inode] = ns_map.get(inode, 0) + 1
+ samples = namespace_samples[ns_name].setdefault(inode, [])
+ if len(samples) < 5 and proc_name not in samples:
+ samples.append(proc_name)
except (psutil.NoSuchProcess, psutil.AccessDenied, KeyError):
continue
@@ -322,6 +328,15 @@ def get_isolation_context():
if entries:
dominant_inode, dominant_count = max(entries.items(), key=lambda kv: kv[1])
activity = round((dominant_count / total_scanned), 3) if total_scanned > 0 else 0
+ # Top isolated "worlds" for this namespace (one per inode), richest first.
+ worlds = [
+ {
+ "inode": inode,
+ "count": count,
+ "sample": namespace_samples[ns_name].get(inode, []),
+ }
+ for inode, count in sorted(entries.items(), key=lambda kv: kv[1], reverse=True)[:6]
+ ]
namespaces.append(
{
"id": ns_name,
@@ -330,6 +345,8 @@ def get_isolation_context():
"dominant_inode": dominant_inode,
"dominant_count": dominant_count,
"activity": activity,
+ "isolated": unique_count > 1,
+ "worlds": worlds,
}
)
diff --git a/static/css/main.css b/static/css/main.css
index 0e16b62..1ea760e 100755
--- a/static/css/main.css
+++ b/static/css/main.css
@@ -13,6 +13,11 @@ svg {
height: 100vh;
}
+/* Модальный скрим открытого меню процесса (клик по нему закрывает меню) */
+.process-modal-scrim {
+ cursor: pointer;
+}
+
/* Central circle */
.central-circle {
fill: rgba(0, 0, 0, 0.09);
@@ -169,6 +174,121 @@ svg {
max-width: 300px;
}
+/* Namespace HUD tooltip (ink-on-paper FUI style) */
+.ns-hud-tooltip {
+ background: rgba(20, 22, 26, 0.94);
+ color: #e8eaef;
+ border: 1px solid rgba(120, 126, 138, 0.5);
+ border-radius: 3px;
+ padding: 9px 11px;
+ min-width: 172px;
+ max-width: 240px;
+ box-shadow: 0 6px 22px rgba(0, 0, 0, 0.35);
+}
+.ns-hud-head {
+ font-size: 10px;
+ letter-spacing: 1.6px;
+ color: #ffffff;
+ padding-bottom: 5px;
+ margin-bottom: 6px;
+ border-bottom: 1px solid rgba(120, 126, 138, 0.35);
+}
+.ns-hud-desc {
+ font-size: 9.5px;
+ line-height: 1.35;
+ color: #aab0bc;
+ margin-bottom: 7px;
+}
+.ns-hud-row {
+ display: flex;
+ justify-content: space-between;
+ font-size: 9.5px;
+ letter-spacing: 0.4px;
+ padding: 1px 0;
+}
+.ns-hud-row span { color: #8b909c; }
+.ns-hud-row b { color: #e8eaef; font-weight: 600; }
+.ns-hud-bar {
+ margin-top: 7px;
+ height: 4px;
+ border-radius: 2px;
+ background: rgba(120, 126, 138, 0.25);
+ overflow: hidden;
+}
+.ns-hud-bar i {
+ display: block;
+ height: 100%;
+ background: rgba(88, 182, 216, 0.85);
+}
+.ns-hud-foot {
+ margin-top: 4px;
+ font-size: 8px;
+ letter-spacing: 1.2px;
+ color: #7f8794;
+ text-align: right;
+}
+
+/* Marker for namespace cells that contain real isolation (unique_count > 1) */
+.ns-isolated-marker {
+ animation: nsIsolatedPulse 1.8s ease-in-out infinite;
+ transform-box: fill-box;
+ transform-origin: center;
+}
+@keyframes nsIsolatedPulse {
+ 0%, 100% { opacity: 0.35; }
+ 50% { opacity: 1; }
+}
+
+/* Namespace system-info tree (unfolds from a namespace cell) */
+.ns-tree-conn { stroke: rgba(90, 94, 102, 0.75); stroke-width: 1.3; fill: none; }
+.ns-tree-anchor { fill: rgba(60, 63, 70, 0.9); }
+.ns-tree-frame {
+ fill: rgba(20, 22, 26, 0.95);
+ stroke: rgba(120, 126, 138, 0.5);
+ stroke-width: 1;
+}
+.ns-tree-title {
+ fill: #ffffff;
+ font-family: 'Share Tech Mono', monospace;
+ font-size: 10px;
+ letter-spacing: 1.5px;
+}
+.ns-tree-link { stroke: rgba(150, 156, 168, 0.5); stroke-width: 1; fill: none; }
+.ns-tree-root rect {
+ fill: rgba(88, 182, 216, 0.9);
+ stroke: rgba(88, 182, 216, 1);
+ stroke-width: 1;
+}
+.ns-tree-root text {
+ fill: #06131b;
+ font-family: 'Share Tech Mono', monospace;
+ font-size: 9.5px;
+ font-weight: 600;
+ letter-spacing: 0.6px;
+}
+.ns-tree-branch rect {
+ fill: rgba(255, 255, 255, 0.1);
+ stroke: rgba(255, 255, 255, 0.28);
+ stroke-width: 1;
+}
+.ns-tree-branch text {
+ fill: #eef1f5;
+ font-family: 'Share Tech Mono', monospace;
+ font-size: 9px;
+ letter-spacing: 1px;
+}
+.ns-tree-leaf rect {
+ fill: rgba(255, 255, 255, 0.045);
+ stroke: rgba(120, 126, 138, 0.42);
+ stroke-width: 1;
+}
+.ns-tree-leaf text {
+ fill: #c4c9d3;
+ font-family: 'Share Tech Mono', monospace;
+ font-size: 8.5px;
+ letter-spacing: 0.3px;
+}
+
/* Bezier curve styles - matching connection lines */
.bezier-curve {
fill: none;
diff --git a/static/js/isolation-ui.js b/static/js/isolation-ui.js
index 579d633..080a86a 100644
--- a/static/js/isolation-ui.js
+++ b/static/js/isolation-ui.js
@@ -30,6 +30,7 @@ function drawIsolationConceptLayer(centerX, centerY, width, height) {
const mobileLayout = isMobileLayout();
// Skip overlay in Matrix/DNA modes to keep views clean.
if (window.kernelContextMenu && ['matrix', 'dna', 'dna-timeline'].includes(window.kernelContextMenu.currentView)) {
+ collapseNamespaceTree(false);
return;
}
@@ -48,6 +49,38 @@ function drawIsolationConceptLayer(centerX, centerY, width, height) {
});
}
+// Short human meaning of each Linux namespace, shown in the HUD tooltip.
+const NS_META = {
+ mnt: { name: 'MOUNT', isolates: 'Mount points, filesystem tree' },
+ pid: { name: 'PID', isolates: 'Process tree, PID isolation' },
+ net: { name: 'NET', isolates: 'Interfaces, stack, ports, routes' },
+ ipc: { name: 'IPC', isolates: 'System V IPC, POSIX queues' },
+ uts: { name: 'UTS', isolates: 'Hostname and domain name' },
+ user: { name: 'USER', isolates: 'UID/GID mapping, privileges' },
+ cgroup: { name: 'CGROUP', isolates: 'Cgroup hierarchy root' },
+ time: { name: 'TIME', isolates: 'boottime / monotonic clocks' },
+};
+
+// Kernel facets each namespace isolates (leaves of the unfolding tree).
+const NS_FACETS = {
+ mnt: ['mount table', 'root filesystem', 'bind & propagation'],
+ pid: ['process tree', 'PID 1 (init)', '/proc view'],
+ net: ['interfaces', 'routes & ARP', 'sockets & ports'],
+ ipc: ['SysV shm / sem', 'POSIX mqueues'],
+ uts: ['hostname', 'domain name'],
+ user: ['UID / GID map', 'capabilities'],
+ cgroup: ['cgroup root'],
+ time: ['boottime clock', 'monotonic clock'],
+};
+
+// Where the namespace pointer lives inside the kernel task struct.
+const NS_KIND = {
+ mnt: 'nsproxy', net: 'nsproxy', uts: 'nsproxy', ipc: 'nsproxy',
+ time: 'nsproxy', cgroup: 'nsproxy', pid: 'pid struct', user: 'cred',
+};
+
+let expandedNsId = null;
+
function drawNamespaceShell(centerX, centerY, namespaces) {
const shellGroup = svg.selectAll('.tag-icon').empty()
? svg.append('g').attr('class', 'namespace-shell-layer')
@@ -62,18 +95,38 @@ function drawNamespaceShell(centerX, centerY, namespaces) {
const numSlots = 8;
const angleStep = (2 * Math.PI) / numSlots;
- const gap = 0.04;
- // Keep enlarged scale while restoring "circle slice" geometry.
- // Center namespace slices on Icon1 orbit (r=150).
+ const gap = 0.045;
+ // Center namespace slices on the Icon1 orbit (r=150).
const ringInner = 110;
const ringOuter = 190;
+ // Ink-on-paper palette (page bg is #e6e6e6 → use dark ink for contrast).
+ const INK = '58, 61, 68';
+ const cells = [];
+
+ const restoreFocus = () => {
+ cells.forEach(c => {
+ c.segment.attr('opacity', 1);
+ c.halo.attr('opacity', 0);
+ c.label.attr('opacity', 1).style('fill', `rgb(${INK})`);
+ if (c.count) c.count.attr('opacity', 1);
+ });
+ };
+
+ const setFocus = (idx) => {
+ cells.forEach((c, j) => {
+ const focused = j === idx;
+ c.segment.attr('opacity', focused ? 1 : 0.22);
+ c.halo.attr('opacity', focused ? 1 : 0);
+ c.label
+ .attr('opacity', focused ? 1 : 0.22)
+ .style('fill', focused ? '#1f2228' : `rgb(${INK})`);
+ if (c.count) c.count.attr('opacity', focused ? 1 : 0.22);
+ });
+ };
+
for (let i = 0; i < numSlots; i++) {
const ns = namespaceSlots[i];
- if (!ns) continue; // keep free slots empty
-
- const activity = Math.max(0, Math.min(1, Number(ns.activity || 0)));
- // Center each namespace slice on the corresponding Icon1 angle.
const centerAngle = i * angleStep;
const startAngle = centerAngle - angleStep / 2 + gap;
const endAngle = centerAngle + angleStep / 2 - gap;
@@ -81,55 +134,318 @@ function drawNamespaceShell(centerX, centerY, namespaces) {
.innerRadius(ringInner)
.outerRadius(ringOuter)
.startAngle(startAngle)
- .endAngle(endAngle);
+ .endAngle(endAngle)
+ .cornerRadius(6);
+ const dPath = arcPath();
+
+ // Empty slot → muted "unused" placeholder so the ring reads as complete.
+ if (!ns) {
+ shellGroup.append('path')
+ .attr('d', dPath)
+ .attr('transform', `translate(${centerX}, ${centerY})`)
+ .attr('fill', 'rgba(90, 92, 98, 0.03)')
+ .attr('stroke', 'rgba(120, 122, 128, 0.28)')
+ .attr('stroke-width', 0.8)
+ .attr('stroke-dasharray', '2 4');
+ continue;
+ }
+
+ const activity = Math.max(0, Math.min(1, Number(ns.activity || 0)));
+ const meta = NS_META[ns.id] || {};
+ const nsName = ns.label || meta.name || String(ns.id || 'NS').toUpperCase();
+ // A cell with more than one distinct inode contains real isolation
+ // (containers / sandboxes) — the most security-relevant signal.
+ const isolated = !!ns.isolated || Number(ns.unique_count || 0) > 1;
+ const ACCENT = '88, 182, 216';
+
+ // Soft focus halo behind the segment (hidden until hover).
+ const halo = shellGroup.append('path')
+ .attr('d', dPath)
+ .attr('transform', `translate(${centerX}, ${centerY})`)
+ .attr('fill', 'none')
+ .attr('stroke', `rgba(${INK}, 0.20)`)
+ .attr('stroke-width', 7)
+ .attr('opacity', 0)
+ .style('pointer-events', 'none');
const segment = shellGroup.append('path')
- .attr('d', arcPath())
+ .attr('d', dPath)
.attr('transform', `translate(${centerX}, ${centerY})`)
- .attr('fill', `rgba(60, 60, 60, ${0.07 + activity * 0.16})`)
- .attr('stroke', `rgba(90, 90, 90, ${0.5 + activity * 0.32})`)
- .attr('stroke-width', 1 + activity * 1.4)
- .style('cursor', 'help');
+ .attr('fill', isolated
+ ? `rgba(${ACCENT}, ${0.08 + activity * 0.2})`
+ : `rgba(${INK}, ${0.06 + activity * 0.22})`)
+ .attr('stroke', isolated
+ ? `rgba(${ACCENT}, ${0.7 + activity * 0.3})`
+ : `rgba(${INK}, ${0.42 + activity * 0.4})`)
+ .attr('stroke-width', isolated ? 1.8 + activity * 1.4 : 1.1 + activity * 1.6)
+ .style('cursor', 'pointer');
const mid = (startAngle + endAngle) / 2;
- const labelR = ringOuter - 12;
+ const labelR = ringOuter - 13;
const lx = centerX + Math.cos(mid - Math.PI / 2) * labelR;
const ly = centerY + Math.sin(mid - Math.PI / 2) * labelR;
- shellGroup.append('text')
+ const label = shellGroup.append('text')
.attr('x', lx)
.attr('y', ly)
.attr('text-anchor', 'middle')
.style('font-family', 'Share Tech Mono, monospace')
- .style('font-size', '8px')
- .style('letter-spacing', '0.5px')
- .style('fill', '#d2d6de')
- .text(ns.label || String(ns.id || 'NS').toUpperCase());
+ .style('font-size', '8.5px')
+ .style('letter-spacing', '1.2px')
+ .style('fill', `rgb(${INK})`)
+ .style('pointer-events', 'none')
+ .text(nsName.toUpperCase());
+
+ // Always-visible live count near the inner edge.
+ const countR = ringInner + 12;
+ const cx = centerX + Math.cos(mid - Math.PI / 2) * countR;
+ const cy = centerY + Math.sin(mid - Math.PI / 2) * countR;
+ const count = shellGroup.append('text')
+ .attr('x', cx)
+ .attr('y', cy)
+ .attr('text-anchor', 'middle')
+ .style('font-family', 'Share Tech Mono, monospace')
+ .style('font-size', '9px')
+ .style('font-weight', '600')
+ .style('letter-spacing', '0.3px')
+ .style('fill', isolated ? `rgb(${ACCENT})` : `rgba(${INK}, 0.72)`)
+ .style('pointer-events', 'none')
+ .text(String(ns.unique_count || 0));
+
+ // Pulsing marker flags cells that actually contain isolation.
+ if (isolated) {
+ const markR = ringOuter + 6;
+ shellGroup.append('circle')
+ .attr('class', 'ns-isolated-marker')
+ .attr('cx', centerX + Math.cos(mid - Math.PI / 2) * markR)
+ .attr('cy', centerY + Math.sin(mid - Math.PI / 2) * markR)
+ .attr('r', 2.6)
+ .attr('fill', `rgb(${ACCENT})`)
+ .style('pointer-events', 'none');
+ }
+
+ const idx = cells.length;
+ cells.push({ segment, halo, label, count });
segment
.on('mouseenter', (event) => {
+ setFocus(idx);
d3.selectAll('.ns-tooltip').remove();
d3.select('body')
.append('div')
- .attr('class', 'tooltip ns-tooltip')
- .style('opacity', 0.95)
- .style('left', `${event.pageX + 10}px`)
+ .attr('class', 'tooltip ns-tooltip ns-hud-tooltip')
+ .style('left', `${event.pageX + 14}px`)
.style('top', `${event.pageY - 10}px`)
.html(`
- Namespace ${ns.label || String(ns.id || '').toUpperCase()}
- Unique: ${ns.unique_count || 0}
- Dominant: ${ns.dominant_count || 0} procs
- Inode: ${ns.dominant_inode || 'n/a'}
+