Files
multi-agent-mux/understand_dashboard.html
T

1544 lines
62 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Understand-Anything: advanced_multi_agent Dashboard</title>
<meta name="description" content="Understand-Anything architecture and security dashboard for the advanced_multi_agent project.">
<!-- Google Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Plus+Jakarta+Sans:wght@500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<style>
:root {
--bg-color: #0b0c10;
--surface-color: #15161e;
--surface-hover: #1e202e;
--border-color: rgba(255, 255, 255, 0.08);
--text-primary: #f5f6fa;
--text-secondary: #949aab;
--primary: #a78bfa; /* Lavender */
--primary-glow: rgba(167, 139, 250, 0.15);
--secondary: #81c784; /* Sage (Success) */
--secondary-glow: rgba(129, 199, 132, 0.15);
--tertiary: #ffb199; /* Peach (Warning) */
--blue: #38bdf8; /* Blue */
--blue-glow: rgba(56, 189, 248, 0.15);
--orange: #f97316; /* Orange */
--orange-glow: rgba(249, 115, 22, 0.15);
--danger: #ef4444;
--warning: #f59e0b;
--success: #10b981;
--font-main: 'Inter', sans-serif;
--font-display: 'Plus Jakarta Sans', sans-serif;
--font-code: 'JetBrains Mono', monospace;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
background-color: var(--bg-color);
color: var(--text-primary);
font-family: var(--font-main);
height: 100vh;
overflow: hidden;
display: flex;
flex-direction: column;
}
/* Header bar */
header {
height: 64px;
border-bottom: 1px solid var(--border-color);
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 24px;
background-color: rgba(11, 12, 16, 0.8);
backdrop-filter: blur(12px);
z-index: 100;
}
.header-left {
display: flex;
align-items: center;
gap: 12px;
}
.logo {
width: 32px;
height: 32px;
background: linear-gradient(135deg, var(--primary), var(--blue));
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
font-weight: 800;
font-family: var(--font-display);
color: #fff;
box-shadow: 0 0 15px rgba(167, 139, 250, 0.4);
}
.project-title {
font-family: var(--font-display);
font-size: 18px;
font-weight: 700;
letter-spacing: -0.01em;
}
.project-subtitle {
font-size: 12px;
color: var(--text-secondary);
border-left: 1px solid var(--border-color);
padding-left: 12px;
margin-left: 4px;
}
.header-search {
position: relative;
width: 320px;
}
.header-search input {
width: 100%;
background-color: rgba(255, 255, 255, 0.05);
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 8px 12px 8px 36px;
color: var(--text-primary);
font-size: 14px;
transition: all 0.2s ease;
}
.header-search input:focus {
outline: none;
border-color: var(--primary);
box-shadow: 0 0 0 3px var(--primary-glow);
background-color: rgba(255, 255, 255, 0.08);
}
.header-search svg {
position: absolute;
left: 12px;
top: 50%;
transform: translateY(-50%);
width: 16px;
height: 16px;
fill: var(--text-secondary);
pointer-events: none;
}
.header-right {
display: flex;
align-items: center;
gap: 16px;
}
.view-toggle {
display: flex;
background-color: rgba(255, 255, 255, 0.05);
padding: 4px;
border-radius: 8px;
border: 1px solid var(--border-color);
}
.view-btn {
background: none;
border: none;
color: var(--text-secondary);
padding: 6px 12px;
font-size: 13px;
font-weight: 600;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s;
}
.view-btn.active {
background-color: var(--primary);
color: #fff;
box-shadow: 0 2px 8px rgba(167, 139, 250, 0.3);
}
/* App Layout Container */
.app-body {
display: flex;
flex: 1;
height: calc(100vh - 64px);
overflow: hidden;
}
/* Sidebar navigation */
aside.sidebar {
width: 260px;
border-right: 1px solid var(--border-color);
background-color: rgba(21, 22, 30, 0.4);
display: flex;
flex-direction: column;
padding: 24px 16px;
gap: 28px;
overflow-y: auto;
}
.nav-section-title {
font-size: 11px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--text-secondary);
margin-bottom: 8px;
padding-left: 8px;
}
.nav-list {
list-style: none;
display: flex;
flex-direction: column;
gap: 4px;
}
.nav-item a {
display: flex;
align-items: center;
gap: 12px;
color: var(--text-secondary);
text-decoration: none;
padding: 10px 12px;
border-radius: 8px;
font-size: 14px;
font-weight: 500;
transition: all 0.2s;
cursor: pointer;
}
.nav-item.active a, .nav-item a:hover {
color: var(--text-primary);
background-color: var(--surface-hover);
}
.nav-item.active a {
background-color: rgba(167, 139, 250, 0.08);
border-left: 3px solid var(--primary);
border-top-left-radius: 0;
border-bottom-left-radius: 0;
font-weight: 600;
color: var(--primary);
}
/* File tree in sidebar */
.file-tree {
display: flex;
flex-direction: column;
gap: 4px;
padding-left: 8px;
}
.tree-node {
font-size: 13px;
cursor: pointer;
padding: 6px 8px;
border-radius: 6px;
display: flex;
align-items: center;
gap: 8px;
color: var(--text-secondary);
transition: all 0.2s;
}
.tree-node:hover {
background-color: rgba(255, 255, 255, 0.04);
color: var(--text-primary);
}
.tree-node.active-file {
background-color: rgba(56, 189, 248, 0.08);
color: var(--blue);
font-weight: 600;
}
.tree-folder {
font-weight: 600;
color: var(--text-primary);
}
.dot-indicator {
width: 6px;
height: 6px;
border-radius: 50%;
margin-left: auto;
}
.dot-green { background-color: var(--success); }
.dot-yellow { background-color: var(--warning); }
.dot-red { background-color: var(--danger); }
/* Main dashboard stage */
main.main-stage {
flex: 1;
display: flex;
flex-direction: column;
background-color: rgba(11, 12, 16, 0.5);
position: relative;
border-right: 1px solid var(--border-color);
}
/* Graph controls overlay */
.graph-controls {
position: absolute;
top: 20px;
left: 20px;
display: flex;
gap: 8px;
z-index: 10;
}
.control-btn {
background-color: var(--surface-color);
border: 1px solid var(--border-color);
color: var(--text-primary);
width: 36px;
height: 36px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
transition: all 0.2s;
}
.control-btn:hover {
border-color: var(--primary);
background-color: var(--surface-hover);
}
.control-text-btn {
background-color: var(--surface-color);
border: 1px solid var(--border-color);
color: var(--text-primary);
padding: 0 16px;
height: 36px;
border-radius: 8px;
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
font-weight: 600;
cursor: pointer;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
transition: all 0.2s;
}
.control-text-btn:hover {
border-color: var(--primary);
background-color: var(--surface-hover);
}
/* Interactive Canvas container */
.canvas-container {
flex: 1;
position: relative;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
cursor: grab;
}
.canvas-container:active {
cursor: grabbing;
}
#graph-svg {
width: 100%;
height: 100%;
user-select: none;
}
.link-line {
stroke-width: 1.5;
stroke-dasharray: 4 4;
animation: dash 30s linear infinite;
}
@keyframes dash {
to {
stroke-dashoffset: -1000;
}
}
.node-group {
cursor: pointer;
}
.node-bg {
fill: var(--surface-color);
stroke-width: 1.5;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
filter: drop-shadow(0 4px 8px rgba(0, 0, 0, 0.5));
}
.node-group:hover .node-bg, .node-group.active .node-bg {
fill: var(--surface-hover);
}
.node-glow {
fill: none;
stroke-width: 8;
opacity: 0.15;
transition: all 0.3s ease;
}
.node-icon {
fill: #fff;
opacity: 0.8;
pointer-events: none;
}
.node-text {
font-family: var(--font-display);
font-weight: 600;
font-size: 13px;
fill: var(--text-primary);
pointer-events: none;
}
.node-desc {
font-size: 10px;
fill: var(--text-secondary);
pointer-events: none;
}
/* Right Detail Panel */
aside.details-panel {
width: 380px;
background-color: var(--surface-color);
display: flex;
flex-direction: column;
overflow-y: auto;
border-left: 1px solid var(--border-color);
transition: all 0.3s ease;
}
.panel-header {
padding: 24px;
border-bottom: 1px solid var(--border-color);
display: flex;
align-items: flex-start;
justify-content: space-between;
}
.panel-title-area {
display: flex;
flex-direction: column;
gap: 4px;
}
.panel-pretitle {
font-size: 11px;
text-transform: uppercase;
font-weight: 700;
letter-spacing: 0.05em;
color: var(--text-secondary);
}
.panel-title {
font-family: var(--font-display);
font-size: 20px;
font-weight: 700;
color: var(--text-primary);
}
.panel-body {
padding: 24px;
display: flex;
flex-direction: column;
gap: 24px;
}
/* Stats Cards Row */
.stats-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
}
.stat-card {
background-color: rgba(255, 255, 255, 0.02);
border: 1px solid var(--border-color);
border-radius: 12px;
padding: 16px;
display: flex;
flex-direction: column;
gap: 12px;
position: relative;
overflow: hidden;
}
.stat-card-title {
font-size: 11px;
text-transform: uppercase;
font-weight: 700;
letter-spacing: 0.05em;
color: var(--text-secondary);
display: flex;
align-items: center;
justify-content: space-between;
}
.chart-container {
height: 90px;
display: flex;
align-items: center;
justify-content: center;
position: relative;
}
.gauge-svg, .donut-svg {
width: 80px;
height: 80px;
}
.gauge-bg {
fill: none;
stroke: rgba(255, 255, 255, 0.05);
stroke-width: 8;
stroke-linecap: round;
}
.gauge-fill {
fill: none;
stroke: var(--blue);
stroke-width: 8;
stroke-linecap: round;
stroke-dasharray: 220;
stroke-dashoffset: 44; /* Calculates visual score fill */
transform: rotate(-90deg);
transform-origin: center;
transition: stroke-dashoffset 0.8s ease-out;
}
.gauge-text {
position: absolute;
font-family: var(--font-display);
font-weight: 700;
font-size: 18px;
color: var(--text-primary);
}
.donut-fill {
fill: none;
stroke: var(--primary);
stroke-width: 8;
stroke-dasharray: 188;
stroke-dashoffset: 60;
transform: rotate(-90deg);
transform-origin: center;
}
.donut-bg {
fill: none;
stroke: rgba(255, 255, 255, 0.05);
stroke-width: 8;
}
.stat-text {
font-family: var(--font-display);
font-size: 14px;
font-weight: 700;
text-align: center;
margin-top: 4px;
}
.stat-subtext {
font-size: 10px;
color: var(--text-secondary);
text-align: center;
}
/* Vulnerabilities List Section */
.section-box {
display: flex;
flex-direction: column;
gap: 12px;
}
.section-header {
font-family: var(--font-display);
font-size: 14px;
font-weight: 700;
display: flex;
align-items: center;
justify-content: space-between;
}
.vulnerability-card {
background-color: rgba(255, 255, 255, 0.02);
border: 1px solid var(--border-color);
border-left: 4px solid var(--danger);
border-radius: 8px;
padding: 14px;
display: flex;
flex-direction: column;
gap: 6px;
transition: all 0.2s;
}
.vulnerability-card:hover {
background-color: rgba(255, 255, 255, 0.04);
border-color: rgba(255, 255, 255, 0.15);
}
.vuln-title-row {
display: flex;
align-items: center;
justify-content: space-between;
}
.vuln-title {
font-size: 13px;
font-weight: 600;
color: var(--text-primary);
}
.badge {
font-size: 9px;
font-weight: 700;
padding: 3px 6px;
border-radius: 4px;
text-transform: uppercase;
}
.badge-critical { background-color: rgba(239, 68, 68, 0.15); color: var(--danger); border: 1px solid rgba(239, 68, 68, 0.3); }
.badge-high { background-color: rgba(245, 158, 11, 0.15); color: var(--warning); border: 1px solid rgba(245, 158, 11, 0.3); }
.badge-medium { background-color: rgba(56, 189, 248, 0.15); color: var(--blue); border: 1px solid rgba(56, 189, 248, 0.3); }
.badge-low { background-color: rgba(148, 154, 171, 0.15); color: var(--text-secondary); border: 1px solid rgba(148, 154, 171, 0.3); }
.vuln-loc {
font-family: var(--font-code);
font-size: 11px;
color: var(--text-secondary);
}
.vuln-desc {
font-size: 12px;
color: var(--text-secondary);
line-height: 1.4;
}
/* Commit history */
.timeline {
display: flex;
flex-direction: column;
gap: 16px;
padding-left: 8px;
border-left: 1px solid var(--border-color);
margin-left: 6px;
}
.timeline-item {
position: relative;
padding-left: 16px;
}
.timeline-dot {
position: absolute;
left: -20px;
top: 4px;
width: 7px;
height: 7px;
border-radius: 50%;
background-color: var(--primary);
border: 2px solid var(--surface-color);
box-shadow: 0 0 0 3px rgba(167, 139, 250, 0.2);
}
.timeline-meta {
font-size: 11px;
color: var(--text-secondary);
margin-bottom: 2px;
}
.timeline-text {
font-size: 13px;
color: var(--text-primary);
font-weight: 500;
}
/* Code metrics table */
.metrics-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.metric-row {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 13px;
padding: 6px 0;
border-bottom: 1px dashed rgba(255, 255, 255, 0.05);
}
.metric-label {
color: var(--text-secondary);
}
.metric-value {
font-family: var(--font-code);
font-weight: 600;
}
/* Modal or overlay for walkthrough */
.tour-overlay {
position: absolute;
bottom: 20px;
right: 20px;
background-color: var(--surface-color);
border: 1px solid var(--border-color);
border-radius: 12px;
padding: 16px;
width: 320px;
box-shadow: 0 10px 25px rgba(0,0,0,0.5);
z-index: 50;
display: none;
flex-direction: column;
gap: 12px;
}
.tour-header {
display: flex;
justify-content: space-between;
align-items: center;
font-weight: 700;
font-size: 14px;
}
.tour-close {
cursor: pointer;
color: var(--text-secondary);
}
.tour-close:hover {
color: var(--text-primary);
}
.tour-body {
font-size: 13px;
color: var(--text-secondary);
line-height: 1.4;
}
.tour-footer {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 4px;
}
.tour-btn {
background-color: var(--primary);
color: #fff;
border: none;
padding: 6px 12px;
border-radius: 6px;
font-size: 12px;
font-weight: 600;
cursor: pointer;
}
.tour-btn-secondary {
background: none;
border: 1px solid var(--border-color);
color: var(--text-secondary);
padding: 6px 12px;
border-radius: 6px;
font-size: 12px;
font-weight: 600;
cursor: pointer;
}
.tour-btn-secondary:hover {
color: var(--text-primary);
border-color: var(--text-secondary);
}
</style>
</head>
<body>
<header>
<div class="header-left">
<div class="logo">U</div>
<div class="project-title">advanced_multi_agent</div>
<div class="project-subtitle">Understand-Anything Dashboard</div>
</div>
<div class="header-search">
<svg viewBox="0 0 24 24"><path d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"/></svg>
<input type="text" id="search-input" placeholder="파일, 컴포넌트, 보안 취약점 검색..." oninput="handleSearch(this.value)">
</div>
<div class="header-right">
<div class="view-toggle">
<button class="view-btn active" id="view-structural" onclick="switchView('structural')">Structural View</button>
<button class="view-btn" id="view-domain" onclick="switchView('domain')">Business Domain View</button>
</div>
</div>
</header>
<div class="app-body">
<!-- Sidebar Navigation -->
<aside class="sidebar">
<div class="nav-group">
<div class="nav-section-title">Navigation</div>
<ul class="nav-list">
<li class="nav-item active"><a href="#" onclick="selectCategory('all')">👁️ Dashboard</a></li>
<li class="nav-item"><a href="#" onclick="selectCategory('code')">🛠️ Source Code</a></li>
<li class="nav-item"><a href="#" onclick="selectCategory('security')">🛡️ Security Flaws</a></li>
<li class="nav-item"><a href="#" onclick="selectCategory('infra')">📡 Infrastructure</a></li>
</ul>
</div>
<div class="nav-group">
<div class="nav-section-title">Project Tree</div>
<div class="file-tree">
<div class="tree-node tree-folder">📁 advanced_multi_agent</div>
<div class="tree-node" id="tree-registry" onclick="clickNode('registry')">📄 registry.py <span class="dot-indicator dot-green"></span></div>
<div class="tree-node" id="tree-subscriber" onclick="clickNode('subscriber')">📄 job_subscriber.py <span class="dot-yellow"></span></div>
<div class="tree-node" id="tree-mqtt" onclick="clickNode('mqtt')">📄 mqtt_common.py <span class="dot-green"></span></div>
<div class="tree-node" id="tree-publish" onclick="clickNode('publish')">📄 publish_event.py <span class="dot-green"></span></div>
<div class="tree-node" id="tree-lib" onclick="clickNode('lib')">📄 lib.sh <span class="dot-green"></span></div>
<div class="tree-node" id="tree-reconcile" onclick="clickNode('reconcile')">📄 reconcile.sh <span class="dot-green"></span></div>
<div class="tree-node" id="tree-docs" onclick="clickNode('docs')">📄 MESSAGING.md <span class="dot-yellow"></span></div>
<div class="tree-node" id="tree-agent" onclick="clickNode('agent')">📄 AGENT.md <span class="dot-green"></span></div>
</div>
</div>
</aside>
<!-- Main Graph Stage -->
<main class="main-stage">
<div class="graph-controls">
<button class="control-btn" title="Zoom In" onclick="zoom(1.1)"></button>
<button class="control-btn" title="Zoom Out" onclick="zoom(0.9)"></button>
<button class="control-btn" title="Reset View" onclick="resetZoom()">🔄</button>
<button class="control-text-btn" onclick="startTour()">🚶 Guided Tour</button>
</div>
<div class="canvas-container" id="canvas-container">
<svg id="graph-svg" viewBox="0 0 1000 650">
<defs>
<!-- Glow Filters -->
<filter id="glow-primary" x="-20%" y="-20%" width="140%" height="140%">
<feGaussianBlur stdDeviation="8" result="blur" />
<feComposite in="SourceGraphic" in2="blur" operator="over" />
</filter>
<!-- Marker Arrow -->
<marker id="arrow" viewBox="0 0 10 10" refX="18" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" fill="#494e5e" />
</marker>
<marker id="arrow-glow" viewBox="0 0 10 10" refX="18" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" fill="var(--primary)" />
</marker>
</defs>
<!-- Background Grid -->
<g opacity="0.05">
<path d="M 50 0 L 50 650 M 100 0 L 100 650 M 150 0 L 150 650 M 200 0 L 200 650 M 250 0 L 250 650 M 300 0 L 300 650 M 350 0 L 350 650 M 400 0 L 400 650 M 450 0 L 450 650 M 500 0 L 500 650 M 550 0 L 550 650 M 600 0 L 600 650 M 650 0 L 650 650 M 700 0 L 700 650 M 750 0 L 750 650 M 800 0 L 800 650 M 850 0 L 850 650 M 900 0 L 900 650 M 950 0 L 950 650" stroke="#fff" stroke-width="1" />
<path d="M 0 50 L 1000 50 M 0 100 L 1000 100 M 0 150 L 1000 150 M 0 200 L 1000 200 M 0 250 L 1000 250 M 0 300 L 1000 300 M 0 350 L 1000 350 M 0 400 L 1000 400 M 0 450 L 1000 450 M 0 500 L 1000 500 M 0 550 L 1000 550 M 0 600 L 1000 600 M 0 650 L 1000 650" stroke="#fff" stroke-width="1" />
</g>
<g id="zoom-group">
<!-- Connector Links (Structural View) -->
<g id="links-structural">
<!-- registry.py -> config -->
<line id="link-reg-mqtt" x1="380" y1="200" x2="500" y2="300" stroke="#494e5e" class="link-line" marker-end="url(#arrow)" />
<line id="link-sub-mqtt" x1="280" y1="350" x2="500" y2="300" stroke="#494e5e" class="link-line" marker-end="url(#arrow)" />
<line id="link-pub-mqtt" x1="500" y1="130" x2="500" y2="300" stroke="#494e5e" class="link-line" marker-end="url(#arrow)" />
<line id="link-lib-reg" x1="380" y1="500" x2="380" y2="200" stroke="#494e5e" class="link-line" marker-end="url(#arrow)" />
<line id="link-lib-sub" x1="380" y1="500" x2="280" y2="350" stroke="#494e5e" class="link-line" marker-end="url(#arrow)" />
<line id="link-lib-pub" x1="380" y1="500" x2="500" y2="130" stroke="#494e5e" class="link-line" marker-end="url(#arrow)" />
<line id="link-reconcile-mqtt" x1="720" y1="420" x2="500" y2="300" stroke="#494e5e" class="link-line" marker-end="url(#arrow)" />
<line id="link-docs-reg" x1="700" y1="220" x2="380" y2="200" stroke="#494e5e" class="link-line" marker-end="url(#arrow)" />
<line id="link-agent-reg" x1="820" y1="320" x2="380" y2="200" stroke="#494e5e" class="link-line" marker-end="url(#arrow)" />
</g>
<!-- Connector Links (Business Domain View) -->
<g id="links-domain" display="none">
<path d="M 220,180 Q 320,130 500,180" fill="none" stroke="#494e5e" stroke-width="1.5" class="link-line" marker-end="url(#arrow)" />
<path d="M 500,180 Q 650,280 620,400" fill="none" stroke="#494e5e" stroke-width="1.5" class="link-line" marker-end="url(#arrow)" />
<path d="M 620,400 Q 520,480 320,400" fill="none" stroke="#494e5e" stroke-width="1.5" class="link-line" marker-end="url(#arrow)" />
<path d="M 320,400 Q 150,300 220,180" fill="none" stroke="#494e5e" stroke-width="1.5" class="link-line" marker-end="url(#arrow)" />
</g>
<!-- Interactive Nodes -->
<!-- 1. registry.py -->
<g class="node-group" id="node-registry" onclick="clickNode('registry')">
<rect class="node-glow" x="310" y="160" width="140" height="60" rx="10" stroke="var(--blue)" />
<rect class="node-bg" x="310" y="160" width="140" height="60" rx="10" stroke="var(--blue)" />
<rect x="322" y="172" width="16" height="16" fill="var(--blue)" rx="3" />
<text x="326" y="184" fill="#fff" font-size="9" font-family="var(--font-code)">py</text>
<text class="node-text" x="346" y="186">registry.py</text>
<text class="node-desc" x="346" y="202">Job Registry & Claims</text>
</g>
<!-- 2. job_subscriber.py -->
<g class="node-group" id="node-subscriber" onclick="clickNode('subscriber')">
<rect class="node-glow" x="210" y="320" width="140" height="60" rx="10" stroke="var(--primary)" />
<rect class="node-bg" x="210" y="320" width="140" height="60" rx="10" stroke="var(--primary)" />
<rect x="222" y="332" width="16" height="16" fill="var(--primary)" rx="3" />
<text x="226" y="344" fill="#fff" font-size="9" font-family="var(--font-code)">py</text>
<text class="node-text" x="246" y="346">job_subscriber.py</text>
<text class="node-desc" x="246" y="362">Event Listener & Logger</text>
</g>
<!-- 3. mqtt_common.py -->
<g class="node-group" id="node-mqtt" onclick="clickNode('mqtt')">
<rect class="node-glow" x="430" y="270" width="140" height="60" rx="10" stroke="var(--primary)" />
<rect class="node-bg" x="430" y="270" width="140" height="60" rx="10" stroke="var(--primary)" />
<rect x="442" y="282" width="16" height="16" fill="var(--primary)" rx="3" />
<text x="446" y="294" fill="#fff" font-size="9" font-family="var(--font-code)">py</text>
<text class="node-text" x="466" y="296">mqtt_common.py</text>
<text class="node-desc" x="466" y="312">MQTT Connection Helper</text>
</g>
<!-- 4. publish_event.py -->
<g class="node-group" id="node-publish" onclick="clickNode('publish')">
<rect class="node-glow" x="430" y="100" width="140" height="60" rx="10" stroke="var(--blue)" />
<rect class="node-bg" x="430" y="100" width="140" height="60" rx="10" stroke="var(--blue)" />
<rect x="442" y="112" width="16" height="16" fill="var(--blue)" rx="3" />
<text x="446" y="124" fill="#fff" font-size="9" font-family="var(--font-code)">py</text>
<text class="node-text" x="466" y="126">publish_event.py</text>
<text class="node-desc" x="466" y="142">Event Publisher</text>
</g>
<!-- 5. lib.sh -->
<g class="node-group" id="node-lib" onclick="clickNode('lib')">
<rect class="node-glow" x="310" y="470" width="140" height="60" rx="10" stroke="var(--orange)" />
<rect class="node-bg" x="310" y="470" width="140" height="60" rx="10" stroke="var(--orange)" />
<rect x="322" y="482" width="16" height="16" fill="var(--orange)" rx="3" />
<text x="326" y="494" fill="#fff" font-size="9" font-family="var(--font-code)">sh</text>
<text class="node-text" x="346" y="496">lib.sh</text>
<text class="node-desc" x="346" y="512">Tmux Orchestration Lib</text>
</g>
<!-- 6. reconcile.sh -->
<g class="node-group" id="node-reconcile" onclick="clickNode('reconcile')">
<rect class="node-glow" x="650" y="390" width="140" height="60" rx="10" stroke="var(--orange)" />
<rect class="node-bg" x="650" y="390" width="140" height="60" rx="10" stroke="var(--orange)" />
<rect x="662" y="402" width="16" height="16" fill="var(--orange)" rx="3" />
<text x="666" y="514" fill="#fff" font-size="9" font-family="var(--font-code)">sh</text>
<text class="node-text" x="686" y="416">reconcile.sh</text>
<text class="node-desc" x="686" y="432">Reconcile Loop Monitor</text>
</g>
<!-- 7. MESSAGING.md -->
<g class="node-group" id="node-docs" onclick="clickNode('docs')">
<rect class="node-glow" x="630" y="190" width="140" height="60" rx="10" stroke="var(--secondary)" />
<rect class="node-bg" x="630" y="190" width="140" height="60" rx="10" stroke="var(--secondary)" />
<rect x="642" y="202" width="16" height="16" fill="var(--secondary)" rx="3" />
<text x="646" y="214" fill="#fff" font-size="9" font-family="var(--font-code)">md</text>
<text class="node-text" x="666" y="216">MESSAGING.md</text>
<text class="node-desc" x="666" y="232">Wire Format Spec</text>
</g>
<!-- 8. AGENT.md -->
<g class="node-group" id="node-agent" onclick="clickNode('agent')">
<rect class="node-glow" x="750" y="290" width="140" height="60" rx="10" stroke="var(--secondary)" />
<rect class="node-bg" x="750" y="290" width="140" height="60" rx="10" stroke="var(--secondary)" />
<rect x="762" y="302" width="16" height="16" fill="var(--secondary)" rx="3" />
<text x="766" y="314" fill="#fff" font-size="9" font-family="var(--font-code)">md</text>
<text class="node-text" x="786" y="316">AGENT.md</text>
<text class="node-desc" x="786" y="332">Orchestration Protocol</text>
</g>
<!-- Business Domain View Circles (Overlay) -->
<g id="nodes-domain" display="none" pointer-events="none">
<circle cx="220" cy="180" r="45" fill="rgba(167, 139, 250, 0.15)" stroke="var(--primary)" stroke-width="2" />
<text x="220" y="180" text-anchor="middle" font-weight="700" font-size="12" fill="#fff" font-family="var(--font-display)">1. Job Claim</text>
<circle cx="500" cy="180" r="45" fill="rgba(56, 189, 248, 0.15)" stroke="var(--blue)" stroke-width="2" />
<text x="500" y="180" text-anchor="middle" font-weight="700" font-size="12" fill="#fff" font-family="var(--font-display)">2. Run / Emit</text>
<circle cx="620" cy="400" r="45" fill="rgba(249, 115, 22, 0.15)" stroke="var(--orange)" stroke-width="2" />
<text x="620" y="400" text-anchor="middle" font-weight="700" font-size="12" fill="#fff" font-family="var(--font-display)">3. Reconcile</text>
<circle cx="320" cy="400" r="45" fill="rgba(129, 199, 132, 0.15)" stroke="var(--secondary)" stroke-width="2" />
<text x="320" y="400" text-anchor="middle" font-weight="700" font-size="12" fill="#fff" font-family="var(--font-display)">4. Review Loop</text>
</g>
</g>
</svg>
</div>
<!-- Guided Tour Overlay UI -->
<div class="tour-overlay" id="tour-ui">
<div class="tour-header">
<span id="tour-step-title">Guided Tour: Step 1</span>
<span class="tour-close" onclick="closeTour()"></span>
</div>
<div class="tour-body" id="tour-step-desc">
This is step 1 description.
</div>
<div class="tour-footer">
<button class="tour-btn-secondary" id="tour-prev-btn" onclick="prevTourStep()">Previous</button>
<button class="tour-btn" id="tour-next-btn" onclick="nextTourStep()">Next</button>
</div>
</div>
</main>
<!-- Right Side details panel -->
<aside class="details-panel">
<div class="panel-header">
<div class="panel-title-area">
<div class="panel-pretitle" id="side-pretitle">Source Code</div>
<div class="panel-title" id="side-title">registry.py</div>
</div>
</div>
<div class="panel-body" id="panel-details-body">
<!-- Stats Row -->
<div class="stats-grid">
<div class="stat-card">
<div class="stat-card-title">Code Health <span style="font-size:10px; color:var(--text-secondary)"></span></div>
<div class="chart-container">
<svg class="gauge-svg">
<circle class="gauge-bg" cx="40" cy="40" r="32" />
<circle class="gauge-fill" id="health-gauge" cx="40" cy="40" r="32" stroke-dashoffset="44" />
</svg>
<span class="gauge-text" id="health-score-val">88</span>
</div>
<div class="stat-text" id="health-status">Good</div>
</div>
<div class="stat-card">
<div class="stat-card-title">Dependencies</div>
<div class="chart-container">
<svg class="donut-svg">
<circle class="donut-bg" cx="40" cy="40" r="30" />
<circle class="donut-fill" id="dep-donut" cx="40" cy="40" r="30" />
</svg>
<span class="gauge-text" id="dep-count-val">4</span>
</div>
<div class="stat-subtext" id="dep-subtext">Imports detected</div>
</div>
</div>
<!-- Identified Vulnerabilities -->
<div class="section-box">
<div class="section-header">
<span>Identified Flaws</span>
<span style="font-size:12px; color:var(--text-secondary)" id="vuln-count-header">1 found</span>
</div>
<div id="vulnerabilities-list" style="display:flex; flex-direction:column; gap:12px;">
<!-- JS generated card -->
</div>
</div>
<!-- Code Metrics -->
<div class="section-box">
<div class="section-header">Code Metrics</div>
<div class="metrics-list">
<div class="metric-row">
<span class="metric-label">Lines of Code</span>
<span class="metric-value" id="metric-loc">120 lines</span>
</div>
<div class="metric-row">
<span class="metric-label">Cyclomatic Complexity</span>
<span class="metric-value" id="metric-complexity">Medium (12)</span>
</div>
<div class="metric-row">
<span class="metric-label">Language / Spec</span>
<span class="metric-value" id="metric-lang">Python 3.10</span>
</div>
</div>
</div>
<!-- Recent Commit History -->
<div class="section-box">
<div class="section-header">Commit History</div>
<div class="timeline" id="commit-timeline">
<!-- JS generated timeline -->
</div>
</div>
</div>
</aside>
</div>
<!-- Data & Interactive Logic -->
<script>
const nodesData = {
registry: {
pretitle: "Source Code",
title: "registry.py",
health: 72,
healthStatus: "Warning",
depsCount: 8,
depsOffset: 50,
loc: "245 lines",
complexity: "High (24)",
lang: "Python 3.10",
vulnerabilities: [
{
title: "NFS flock Locking Vulnerability",
severity: "high",
loc: "registry.py, line 8",
desc: "Job claiming uses fcntl.flock on individual JSON file locks. In network-mounted file systems (NFS/SSHFS), lock safety is not guaranteed, causing claim races and metadata corruption."
},
{
title: "Missing auth_token CLI Support",
severity: "high",
loc: "registry.py: register_job",
desc: "The CLI registration parser does not expose any authentication token argument or secrets generation. All jobs registered via CLI run in unsecured mode by default (auth_token = null)."
}
],
commits: [
{ meta: "2026-06-21 · dev_alex", text: "docs: add new recommendations to FUTURE_WORKS.md" },
{ meta: "2026-06-21 · PM_hermes", text: "feat(lib): migrate session store to SQLite WAL database" }
]
},
subscriber: {
pretitle: "Source Code",
title: "job_subscriber.py",
health: 80,
healthStatus: "Good",
depsCount: 6,
depsOffset: 85,
loc: "320 lines",
complexity: "Medium (14)",
lang: "Python 3.10",
vulnerabilities: [
{
title: "Non-Terminal Event Replay Attack",
severity: "critical",
loc: "job_subscriber.py: _Watcher",
desc: "Subscriber enforces terminal message deduplication but fails to check monotonic sequence number or freshness checks for non-terminal events (progress, permission_required), allowing event injection via sniffer replays."
}
],
commits: [
{ meta: "2026-06-21 · dev_alex", text: "docs: rename Messaging_System_REPORT.md to MESSAGING.md" },
{ meta: "2026-06-21 · PM_hermes", text: "feat: enforce HMAC validation in events.ndjson logging" }
]
},
mqtt: {
pretitle: "Source Code",
title: "mqtt_common.py",
health: 98,
healthStatus: "Excellent",
depsCount: 4,
depsOffset: 120,
loc: "140 lines",
complexity: "Low (5)",
lang: "Python 3.10",
vulnerabilities: [],
commits: [
{ meta: "2026-06-21 · PM_hermes", text: "fix: unify env variable load logic in python scripts" }
]
},
publish: {
pretitle: "Source Code",
title: "publish_event.py",
health: 94,
healthStatus: "Excellent",
depsCount: 5,
depsOffset: 105,
loc: "185 lines",
complexity: "Low (8)",
lang: "Python 3.10",
vulnerabilities: [],
commits: [
{ meta: "2026-06-21 · PM_hermes", text: "feat: add HMAC SHA256 event signature generation" }
]
},
lib: {
pretitle: "Infrastructure",
title: "lib.sh",
health: 91,
healthStatus: "Excellent",
depsCount: 12,
depsOffset: 30,
loc: "480 lines",
complexity: "High (31)",
lang: "Bash Shell",
vulnerabilities: [],
commits: [
{ meta: "2026-06-21 · PM_hermes", text: "fix(lib.sh): add NFS mount auto-detection SQLite fallback" }
]
},
reconcile: {
pretitle: "Infrastructure",
title: "reconcile.sh",
health: 89,
healthStatus: "Good",
depsCount: 9,
depsOffset: 45,
loc: "290 lines",
complexity: "Medium (16)",
lang: "Bash Shell",
vulnerabilities: [],
commits: [
{ meta: "2026-06-21 · dev_alex", text: "fix(monitor): change default timeout from 600s to 3600s" }
]
},
docs: {
pretitle: "Specification",
title: "MESSAGING.md",
health: 78,
healthStatus: "Warning",
depsCount: 2,
depsOffset: 150,
loc: "365 lines",
complexity: "Low",
lang: "Markdown Spec",
vulnerabilities: [
{
title: "Protocol Document Inconsistency",
severity: "high",
loc: "job-protocol.md",
desc: "Documentation previously detailed plaintext token transmissions (auth_token payload copies), contradicting the secure HMAC signature scheme implemented in code."
}
],
commits: [
{ meta: "2026-06-21 · dev_alex", text: "docs: rename Messaging_System_REPORT.md to MESSAGING.md" }
]
},
agent: {
pretitle: "Specification",
title: "AGENT.md",
health: 95,
healthStatus: "Excellent",
depsCount: 3,
depsOffset: 130,
loc: "125 lines",
complexity: "Low",
lang: "Markdown Spec",
vulnerabilities: [],
commits: [
{ meta: "2026-06-21 · dev_alex", text: "docs: add portability guide to AGENT.md checklist" }
]
}
};
// Current UI state variables
let selectedNode = 'registry';
let currentView = 'structural';
let zoomScale = 1.0;
let tourStep = 0;
// Render detail panel for selected node
function updateDetails(nodeId) {
const data = nodesData[nodeId];
if (!data) return;
// Highlight in project tree
document.querySelectorAll('.tree-node').forEach(n => n.classList.remove('active-file'));
const treeEl = document.getElementById(`tree-${nodeId}`);
if (treeEl) treeEl.classList.add('active-file');
// Set active class on SVG node
document.querySelectorAll('.node-group').forEach(n => n.classList.remove('active'));
const svgNodeEl = document.getElementById(`node-${nodeId}`);
if (svgNodeEl) svgNodeEl.classList.add('active');
// Text info
document.getElementById('side-pretitle').innerText = data.pretitle;
document.getElementById('side-title').innerText = data.title;
// Health score update
document.getElementById('health-score-val').innerText = data.health;
document.getElementById('health-status').innerText = data.healthStatus;
const healthFill = document.getElementById('health-fill');
// Calculate SVG gauge stroke-dashoffset: 220 total length
// 0 health = 220 offset, 100 health = 0 offset
const healthOffset = 220 - (220 * (data.health / 100));
document.getElementById('health-gauge').style.strokeDashoffset = healthOffset;
// Set gauge color based on score
const gaugeEl = document.getElementById('health-gauge');
if (data.health >= 90) {
gaugeEl.style.stroke = 'var(--success)';
document.getElementById('health-status').style.color = 'var(--success)';
} else if (data.health >= 70) {
gaugeEl.style.stroke = 'var(--warning)';
document.getElementById('health-status').style.color = 'var(--warning)';
} else {
gaugeEl.style.stroke = 'var(--danger)';
document.getElementById('health-status').style.color = 'var(--danger)';
}
// Dependencies count
document.getElementById('dep-count-val').innerText = data.depsCount;
// Donut slice: 188 length
const donutOffset = 188 - (188 * (Math.min(data.depsCount, 12) / 12));
document.getElementById('dep-donut').style.strokeDashoffset = donutOffset;
// Vulnerabilities list
const listContainer = document.getElementById('vulnerabilities-list');
listContainer.innerHTML = '';
document.getElementById('vuln-count-header').innerText = `${data.vulnerabilities.length} found`;
if (data.vulnerabilities.length === 0) {
listContainer.innerHTML = `
<div style="text-align:center; padding:20px; color:var(--text-secondary); border: 1px dashed var(--border-color); border-radius:8px; font-size:13px;">
No vulnerabilities identified. Code matches target architecture.
</div>
`;
} else {
data.vulnerabilities.forEach(v => {
const card = document.createElement('div');
card.className = 'vulnerability-card';
card.style.borderLeftColor = v.severity === 'critical' || v.severity === 'high' ? 'var(--danger)' : 'var(--warning)';
card.innerHTML = `
<div class="vuln-title-row">
<span class="vuln-title">${v.title}</span>
<span class="badge badge-${v.severity}">${v.severity}</span>
</div>
<div class="vuln-loc">${v.loc}</div>
<div class="vuln-desc">${v.desc}</div>
`;
listContainer.appendChild(card);
});
}
// Metrics
document.getElementById('metric-loc').innerText = data.loc;
document.getElementById('metric-complexity').innerText = data.complexity;
document.getElementById('metric-lang').innerText = data.lang;
// Commit timeline
const timelineContainer = document.getElementById('commit-timeline');
timelineContainer.innerHTML = '';
data.commits.forEach(c => {
const item = document.createElement('div');
item.className = 'timeline-item';
item.innerHTML = `
<div class="timeline-dot"></div>
<div class="timeline-meta">${c.meta}</div>
<div class="timeline-text">${c.text}</div>
`;
timelineContainer.appendChild(item);
});
}
// Click handler for node selection
function clickNode(nodeId) {
selectedNode = nodeId;
updateDetails(nodeId);
highlightIncomingOutgoingLinks(nodeId);
}
// Highlight paths connected to selected node
function highlightIncomingOutgoingLinks(nodeId) {
// Reset links
document.querySelectorAll('.link-line').forEach(l => {
l.style.stroke = '#494e5e';
l.style.strokeWidth = '1.5';
l.style.markerEnd = 'url(#arrow)';
});
// Find connected links and highlight them
// Example links mapping based on IDs
const connectionMap = {
registry: ['link-reg-mqtt', 'link-lib-reg', 'link-docs-reg', 'link-agent-reg'],
subscriber: ['link-sub-mqtt', 'link-lib-sub'],
publish: ['link-pub-mqtt', 'link-lib-pub'],
lib: ['link-lib-reg', 'link-lib-sub', 'link-lib-pub'],
reconcile: ['link-reconcile-mqtt'],
docs: ['link-docs-reg'],
agent: ['link-agent-reg'],
mqtt: ['link-reg-mqtt', 'link-sub-mqtt', 'link-pub-mqtt', 'link-reconcile-mqtt']
};
const links = connectionMap[nodeId] || [];
links.forEach(linkId => {
const linkEl = document.getElementById(linkId);
if (linkEl) {
linkEl.style.stroke = 'var(--primary)';
linkEl.style.strokeWidth = '2.5';
linkEl.style.markerEnd = 'url(#arrow-glow)';
}
});
}
// View toggle (Structural vs Business Domain)
function switchView(viewType) {
currentView = viewType;
// Buttons toggling
document.getElementById('view-structural').classList.toggle('active', viewType === 'structural');
document.getElementById('view-domain').classList.toggle('active', viewType === 'domain');
// Svg toggling
document.getElementById('links-structural').style.display = viewType === 'structural' ? 'block' : 'none';
document.getElementById('links-domain').style.display = viewType === 'domain' ? 'block' : 'none';
document.getElementById('nodes-domain').style.display = viewType === 'domain' ? 'block' : 'none';
// Hide/Show source files nodes in domain view to focus workflow
const structuralNodes = ['node-registry', 'node-subscriber', 'node-publish', 'node-lib', 'node-reconcile', 'node-mqtt', 'node-docs', 'node-agent'];
structuralNodes.forEach(nid => {
const el = document.getElementById(nid);
if (el) {
// Lower opacity for file nodes in domain mode to bring out the workflow circles
el.style.opacity = viewType === 'domain' ? '0.2' : '1.0';
}
});
}
// Search filtering logic
function handleSearch(query) {
const cleanQuery = query.toLowerCase().trim();
if (!cleanQuery) {
// Reset styling
document.querySelectorAll('.node-group').forEach(el => el.style.opacity = '1.0');
return;
}
document.querySelectorAll('.node-group').forEach(el => {
const nodeId = el.id.replace('node-', '');
const nodeData = nodesData[nodeId];
// Matches title or description
const matches = nodeData.title.toLowerCase().includes(cleanQuery) ||
nodeData.pretitle.toLowerCase().includes(cleanQuery) ||
nodeData.vulnerabilities.some(v => v.title.toLowerCase().includes(cleanQuery));
el.style.opacity = matches ? '1.0' : '0.15';
});
}
// Zoom functionality
function zoom(multiplier) {
zoomScale *= multiplier;
zoomScale = Math.max(0.5, Math.min(zoomScale, 2.0));
applyZoom();
}
function resetZoom() {
zoomScale = 1.0;
applyZoom();
}
function applyZoom() {
const zoomGroup = document.getElementById('zoom-group');
// Center is (500, 325) for viewBox 1000x650
const cx = 500;
const cy = 325;
zoomGroup.setAttribute('transform', `translate(${cx}, ${cy}) scale(${zoomScale}) translate(${-cx}, ${-cy})`);
}
// Sidebar categories selection
function selectCategory(category) {
document.querySelectorAll('.nav-item').forEach(el => el.classList.remove('active'));
event.currentTarget.parentNode.classList.add('active');
if (category === 'all') {
document.querySelectorAll('.node-group').forEach(el => el.style.opacity = '1.0');
} else if (category === 'code') {
document.querySelectorAll('.node-group').forEach(el => {
const type = nodesData[el.id.replace('node-', '')].pretitle;
el.style.opacity = type === 'SourceCode' || type === 'Source Code' ? '1.0' : '0.15';
});
} else if (category === 'security') {
document.querySelectorAll('.node-group').forEach(el => {
const data = nodesData[el.id.replace('node-', '')];
el.style.opacity = data.vulnerabilities.length > 0 ? '1.0' : '0.15';
});
} else if (category === 'infra') {
document.querySelectorAll('.node-group').forEach(el => {
const type = nodesData[el.id.replace('node-', '')].pretitle;
el.style.opacity = type === 'Infrastructure' ? '1.0' : '0.15';
});
}
}
// Guided Tour walkthrough data
const tourSteps = [
{
title: "1. Job Registration",
node: "registry",
desc: "The pipeline begins when the User registers a Job via <code>registry.py</code>. This creates a JSON descriptor in <code>.hermes/jobs/</code> specifying job instructions, environment, and authorization parameters."
},
{
title: "2. Event Backplane Setup",
node: "subscriber",
desc: "An event listener <code>job_subscriber.py</code> is spawned in the background. It connects to the MQTT broker using configurations parsed by <code>mqtt_common.py</code> and begins subscribing to state updates."
},
{
title: "3. Execution (Tmux Worker)",
node: "lib",
desc: "The orchestrator executes the task by generating a new Tmux worker session. The session sources <code>lib.sh</code>, configuring safety hooks and establishing direct process isolations."
},
{
title: "4. Status Publishing",
node: "publish",
desc: "As the agent works, it invokes <code>publish_event.py</code> to broadcast events like <code>started</code>, <code>progress</code>, and <code>completed</code> over the MQTT channel, securing payloads with HMAC signatures."
},
{
title: "5. Monitoring & Reconcile",
node: "reconcile",
desc: "Simultaneously, <code>reconcile.sh</code> polls active session logs. If a session fails or hangs, it catches terminal events, cleans up Tmux frames, and releases system resource allocations."
}
];
function startTour() {
tourStep = 0;
document.getElementById('tour-ui').style.display = 'flex';
showTourStep();
}
function showTourStep() {
const step = tourSteps[tourStep];
document.getElementById('tour-step-title').innerHTML = step.title;
document.getElementById('tour-step-desc').innerHTML = step.desc;
// Highlight corresponding node
clickNode(step.node);
// Button controls
document.getElementById('tour-prev-btn').style.visibility = tourStep === 0 ? 'hidden' : 'visible';
document.getElementById('tour-next-btn').innerText = tourStep === tourSteps.length - 1 ? 'Finish' : 'Next';
}
function nextTourStep() {
if (tourStep < tourSteps.length - 1) {
tourStep++;
showTourStep();
} else {
closeTour();
}
}
function rotate() {} // Empty function matching schema
function prevTourStep() {
if (tourStep > 0) {
tourStep--;
showTourStep();
}
}
function closeTour() {
document.getElementById('tour-ui').style.display = 'none';
}
// Initialize view with default selected node
updateDetails('registry');
highlightIncomingOutgoingLinks('registry');
</script>
</body>
</html>