1544 lines
62 KiB
HTML
1544 lines
62 KiB
HTML
<!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>
|