@plumpslabs/kuma 2.3.22 → 2.3.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +1293 -1134
- package/package.json +1 -1
- package/packages/ide/studio/dist/index.js +55 -6
- package/packages/ide/studio/public/index.html +434 -311
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
<title>Kuma Studio</title>
|
|
7
7
|
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>♢</text></svg>">
|
|
8
8
|
<script src="https://unpkg.com/vis-network@9.1.9/dist/vis-network.min.js"></script>
|
|
9
|
+
<script src="https://unpkg.com/lucide@0.344.0/dist/umd/lucide.min.js"></script>
|
|
9
10
|
<style>
|
|
10
11
|
:root {
|
|
11
12
|
--bg: #1a1a2e; --surface: #16213e; --border: #2a2a4a;
|
|
@@ -13,9 +14,15 @@
|
|
|
13
14
|
--green: #22c55e; --yellow: #eab308; --orange: #f97316; --red: #ef4444;
|
|
14
15
|
}
|
|
15
16
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
16
|
-
body {
|
|
17
|
+
html, body {
|
|
17
18
|
font-family: -apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;
|
|
18
19
|
background: var(--bg); color: var(--text); height: 100vh; overflow: hidden;
|
|
20
|
+
touch-action: none; -webkit-user-select: none; user-select: none;
|
|
21
|
+
}
|
|
22
|
+
header {
|
|
23
|
+
display: flex; align-items: center; gap: 16px; padding: 12px 24px;
|
|
24
|
+
background: var(--surface); border-bottom: 1px solid var(--border);
|
|
25
|
+
-webkit-app-region: drag;
|
|
19
26
|
}
|
|
20
27
|
header {
|
|
21
28
|
display: flex; align-items: center; gap: 16px; padding: 12px 24px;
|
|
@@ -38,7 +45,7 @@ header .status .dot.off { background: var(--red); }
|
|
|
38
45
|
#main { display: flex; height: calc(100vh - 100px); }
|
|
39
46
|
#graph-panel { flex: 1; position: relative; min-width: 0; overflow: hidden; }
|
|
40
47
|
#graph-container { width: 100%; height: 100%; background: radial-gradient(ellipse at center, #1e1e3a 0%, var(--bg) 70%); }
|
|
41
|
-
#sidebar { width:
|
|
48
|
+
#sidebar { width: 360px; min-width: 360px; background: var(--surface); border-left: 1px solid var(--border); display: flex; flex-direction: column; }
|
|
42
49
|
.tabs { display: flex; border-bottom: 1px solid var(--border); }
|
|
43
50
|
.tab {
|
|
44
51
|
flex: 1; padding: 10px; text-align: center; cursor: pointer; font-size: 12px;
|
|
@@ -49,29 +56,47 @@ header .status .dot.off { background: var(--red); }
|
|
|
49
56
|
.tab-content { flex: 1; overflow-y: auto; padding: 12px; }
|
|
50
57
|
.panel { display: none; }
|
|
51
58
|
.panel.active { display: block; }
|
|
59
|
+
|
|
60
|
+
/* Gotcha items */
|
|
52
61
|
.gotcha-item {
|
|
53
62
|
padding: 10px 12px; margin-bottom: 8px; border-radius: 8px;
|
|
54
|
-
background: var(--bg); border-left: 3px solid var(--muted);
|
|
63
|
+
background: var(--bg); border-left: 3px solid var(--muted); cursor: pointer;
|
|
64
|
+
transition: background 0.15s;
|
|
55
65
|
}
|
|
66
|
+
.gotcha-item:hover { background: #1e293b; }
|
|
56
67
|
.gotcha-item .file { font-size: 11px; color: var(--accent2); margin-bottom: 4px; word-break: break-all; }
|
|
57
|
-
.gotcha-item .desc { font-size:
|
|
68
|
+
.gotcha-item .desc { font-size: 12px; margin-bottom: 4px; overflow: hidden; text-overflow: ellipsis; display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; }
|
|
58
69
|
.gotcha-item .sev-tag { display: inline-block; font-size: 10px; padding: 2px 6px; border-radius: 4px; font-weight: 600; }
|
|
59
70
|
.sev-critical { background: #ef444420; color: var(--red); border-color: var(--red); }
|
|
60
71
|
.sev-high { background: #f9731620; color: var(--orange); border-color: var(--orange); }
|
|
61
72
|
.sev-medium { background: #eab30820; color: var(--yellow); border-color: var(--yellow); }
|
|
62
73
|
.sev-low { background: #22c55e20; color: var(--green); border-color: var(--green); }
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
.
|
|
74
|
+
|
|
75
|
+
/* Feature items */
|
|
76
|
+
.feature-item {
|
|
77
|
+
padding: 10px 12px; margin-bottom: 8px; border-radius: 8px;
|
|
78
|
+
background: var(--bg); border-left: 3px solid var(--accent); cursor: pointer;
|
|
79
|
+
transition: background 0.15s;
|
|
80
|
+
}
|
|
81
|
+
.feature-item:hover { background: #1e293b; }
|
|
82
|
+
.feature-item .name { font-size: 14px; font-weight: 600; color: var(--accent); margin-bottom: 4px; }
|
|
83
|
+
.feature-item .meta { font-size: 11px; color: var(--muted); }
|
|
84
|
+
.feature-item .tags { display: flex; gap: 4px; flex-wrap: wrap; margin-top: 4px; }
|
|
85
|
+
.feature-item .tag { font-size: 10px; padding: 1px 6px; border-radius: 4px; background: rgba(124,58,237,0.15); color: var(--accent); }
|
|
86
|
+
|
|
87
|
+
/* Health items */
|
|
66
88
|
.health-item { padding: 8px 12px; margin-bottom: 6px; background: var(--bg); border-radius: 6px; }
|
|
67
89
|
.health-item .score { font-size: 20px; font-weight: 700; }
|
|
68
90
|
.health-item .score.good { color: var(--green); }
|
|
69
91
|
.health-item .score.ok { color: var(--yellow); }
|
|
70
92
|
.health-item .score.bad { color: var(--red); }
|
|
71
93
|
.health-item .meta { font-size: 11px; color: var(--muted); }
|
|
94
|
+
|
|
72
95
|
.empty { text-align: center; color: var(--muted); padding: 40px 20px; font-size: 14px; }
|
|
96
|
+
|
|
97
|
+
/* Legend */
|
|
73
98
|
#legend {
|
|
74
|
-
position:
|
|
99
|
+
position: fixed; bottom: 16px; left: 16px; background: rgba(22,33,62,0.95);
|
|
75
100
|
border: 1px solid var(--border); border-radius: 8px; padding: 10px 14px;
|
|
76
101
|
font-size: 11px; z-index: 10; max-height: 200px; overflow-y: auto;
|
|
77
102
|
backdrop-filter: blur(8px); color: #f8fafc;
|
|
@@ -79,6 +104,8 @@ header .status .dot.off { background: var(--red); }
|
|
|
79
104
|
#legend .title { font-weight: 600; margin-bottom: 6px; color: #94a3b8; }
|
|
80
105
|
.legend-item { display: flex; align-items: center; gap: 6px; margin-bottom: 3px; color: #f1f5f9; }
|
|
81
106
|
.legend-dot { width: 10px; height: 10px; border-radius: 50%; }
|
|
107
|
+
|
|
108
|
+
/* Tooltip */
|
|
82
109
|
#graph-tooltip {
|
|
83
110
|
position: absolute; display: none; background: rgba(15, 23, 42, 0.96);
|
|
84
111
|
border: 1px solid var(--border); border-radius: 8px; padding: 10px 14px;
|
|
@@ -87,9 +114,25 @@ header .status .dot.off { background: var(--red); }
|
|
|
87
114
|
box-shadow: 0 8px 32px rgba(0,0,0,0.6); backdrop-filter: blur(12px);
|
|
88
115
|
pointer-events: none;
|
|
89
116
|
}
|
|
90
|
-
|
|
91
|
-
|
|
117
|
+
|
|
118
|
+
/* Search */
|
|
119
|
+
#search-box { position: absolute; top: 16px; right: 16px; z-index: 10; }
|
|
120
|
+
|
|
121
|
+
/* Context Menu */
|
|
122
|
+
#ctx-menu {
|
|
123
|
+
position: fixed; display: none; z-index: 60; background: var(--surface);
|
|
124
|
+
border: 1px solid var(--border); border-radius: 8px; padding: 4px;
|
|
125
|
+
box-shadow: 0 8px 32px rgba(0,0,0,0.5); min-width: 160px;
|
|
126
|
+
backdrop-filter: blur(12px);
|
|
127
|
+
}
|
|
128
|
+
#ctx-menu .ctx-item {
|
|
129
|
+
display: flex; align-items: center; gap: 8px; padding: 7px 12px;
|
|
130
|
+
border-radius: 6px; cursor: pointer; font-size: 12px; color: var(--text);
|
|
131
|
+
transition: background 0.1s;
|
|
92
132
|
}
|
|
133
|
+
#ctx-menu .ctx-item:hover { background: rgba(124,58,237,0.15); color: var(--accent); }
|
|
134
|
+
#ctx-menu .ctx-item i { width: 14px; height: 14px; flex-shrink: 0; }
|
|
135
|
+
#ctx-menu .ctx-sep { height: 1px; background: var(--border); margin: 4px 8px; }
|
|
93
136
|
#search-box input {
|
|
94
137
|
background: rgba(22,33,62,0.92); border: 1px solid var(--border);
|
|
95
138
|
border-radius: 8px; padding: 8px 12px; color: var(--text); font-size: 12px;
|
|
@@ -98,8 +141,10 @@ header .status .dot.off { background: var(--red); }
|
|
|
98
141
|
}
|
|
99
142
|
#search-box input:focus { border-color: var(--accent); }
|
|
100
143
|
#search-box input::placeholder { color: var(--muted); }
|
|
144
|
+
|
|
145
|
+
/* Graph controls */
|
|
101
146
|
#graph-controls {
|
|
102
|
-
position:
|
|
147
|
+
position: fixed; bottom: 16px; right: 376px; z-index: 10;
|
|
103
148
|
display: flex; gap: 6px;
|
|
104
149
|
}
|
|
105
150
|
#graph-controls button {
|
|
@@ -110,11 +155,14 @@ header .status .dot.off { background: var(--red); }
|
|
|
110
155
|
}
|
|
111
156
|
#graph-controls button:hover { background: var(--accent); border-color: var(--accent); }
|
|
112
157
|
#graph-controls button.active {
|
|
113
|
-
background: var(--accent) !important;
|
|
114
|
-
|
|
115
|
-
color: #ffffff !important;
|
|
116
|
-
box-shadow: 0 0 10px rgba(124, 58, 237, 0.4);
|
|
158
|
+
background: var(--accent) !important; border-color: var(--accent) !important;
|
|
159
|
+
color: #ffffff !important; box-shadow: 0 0 10px rgba(124, 58, 237, 0.4);
|
|
117
160
|
}
|
|
161
|
+
#graph-controls .ctrl-sep { width: 1px; background: var(--border); margin: 0 2px; }
|
|
162
|
+
.depth-btn { min-width: 28px !important; padding: 4px 6px !important; font-size: 11px !important; font-weight: 600; }
|
|
163
|
+
.depth-btn.active { background: rgba(124,58,237,0.3) !important; border-color: var(--accent) !important; color: var(--accent) !important; }
|
|
164
|
+
|
|
165
|
+
/* Loading */
|
|
118
166
|
#loading {
|
|
119
167
|
position: fixed; inset: 0; background: var(--bg); display: flex;
|
|
120
168
|
align-items: center; justify-content: center; z-index: 100;
|
|
@@ -122,6 +170,48 @@ header .status .dot.off { background: var(--red); }
|
|
|
122
170
|
}
|
|
123
171
|
#loading .spinner { width: 32px; height: 32px; border: 3px solid var(--border); border-top-color: var(--accent); border-radius: 50%; animation: spin 0.8s linear infinite; }
|
|
124
172
|
@keyframes spin { to { transform: rotate(360deg); } }
|
|
173
|
+
|
|
174
|
+
/* Node Detail Modal */
|
|
175
|
+
.modal-overlay {
|
|
176
|
+
position: fixed; inset: 0; background: rgba(0,0,0,0.6); z-index: 50;
|
|
177
|
+
display: flex; align-items: center; justify-content: center;
|
|
178
|
+
backdrop-filter: blur(4px); animation: fadeIn 0.15s;
|
|
179
|
+
}
|
|
180
|
+
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
|
|
181
|
+
.modal {
|
|
182
|
+
background: var(--surface); border: 1px solid var(--border); border-radius: 12px;
|
|
183
|
+
width: 560px; max-width: 90vw; max-height: 85vh; overflow: hidden; box-shadow: 0 16px 64px rgba(0,0,0,0.5);
|
|
184
|
+
display: flex; flex-direction: column; animation: slideUp 0.2s;
|
|
185
|
+
}
|
|
186
|
+
@keyframes slideUp { from { transform: translateY(20px); opacity: 0; } to { transform: translateY(0); opacity: 1; } }
|
|
187
|
+
.modal-header {
|
|
188
|
+
display: flex; align-items: center; justify-content: space-between;
|
|
189
|
+
padding: 14px 20px; border-bottom: 1px solid var(--border); flex-shrink: 0;
|
|
190
|
+
}
|
|
191
|
+
.modal-header h2 { font-size: 14px; font-weight: 700; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
|
|
192
|
+
.modal-header .close {
|
|
193
|
+
background: none; border: none; color: var(--muted); font-size: 18px;
|
|
194
|
+
cursor: pointer; width: 28px; height: 28px; display: flex; align-items: center; justify-content: center;
|
|
195
|
+
border-radius: 6px; transition: all 0.15s; flex-shrink: 0;
|
|
196
|
+
}
|
|
197
|
+
.modal-header .close:hover { background: rgba(255,255,255,0.1); color: var(--text); }
|
|
198
|
+
.modal-body { padding: 16px 20px; overflow-y: auto; flex: 1; min-height: 0; }
|
|
199
|
+
.modal-section { margin-bottom: 14px; }
|
|
200
|
+
.modal-section h3 { font-size: 11px; font-weight: 600; color: var(--muted); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 6px; }
|
|
201
|
+
.modal-kv { display: flex; gap: 8px; margin-bottom: 5px; font-size: 12px; line-height: 1.4; }
|
|
202
|
+
.modal-kv .key { color: var(--muted); min-width: 72px; flex-shrink: 0; }
|
|
203
|
+
.modal-kv .val { color: var(--text); word-break: break-word; overflow-wrap: anywhere; min-width: 0; }
|
|
204
|
+
.relation-item {
|
|
205
|
+
display: flex; align-items: center; gap: 6px; padding: 5px 8px;
|
|
206
|
+
margin-bottom: 3px; border-radius: 6px; background: var(--bg);
|
|
207
|
+
font-size: 11px; cursor: pointer; transition: background 0.15s;
|
|
208
|
+
}
|
|
209
|
+
.relation-item:hover { background: #1e293b; }
|
|
210
|
+
.relation-item .arrow { color: var(--accent); font-weight: 700; flex-shrink: 0; }
|
|
211
|
+
.relation-item .name { color: var(--text); flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
|
|
212
|
+
.relation-item .type { color: var(--muted); font-size: 10px; flex-shrink: 0; max-width: 80px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
213
|
+
.relation-item .weight { color: var(--accent2); font-size: 10px; flex-shrink: 0; }
|
|
214
|
+
|
|
125
215
|
::-webkit-scrollbar { width: 6px; }
|
|
126
216
|
::-webkit-scrollbar-track { background: transparent; }
|
|
127
217
|
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
|
|
@@ -130,12 +220,12 @@ header .status .dot.off { background: var(--red); }
|
|
|
130
220
|
<body>
|
|
131
221
|
<div id="loading"><div class="spinner"></div>Connecting to Kuma...</div>
|
|
132
222
|
<header>
|
|
133
|
-
<h1
|
|
223
|
+
<h1><i data-lucide="brain" style="width:20px;height:20px;vertical-align:middle;margin-right:6px"></i> Kuma Studio</h1>
|
|
134
224
|
<div class="status"><span class="dot" id="status-dot"></span><span id="status-text">connecting...</span></div>
|
|
135
225
|
<div style="margin-left:auto;display:flex;gap:8px;-webkit-app-region:no-drag">
|
|
136
|
-
<button onclick="refetch()" id="refetch-btn" style="background:rgba(124,58,237,0.15);border:1px solid rgba(124,58,237,0.3);border-radius:6px;color:var(--accent);padding:5px 12px;font-size:11px;cursor:pointer;transition:all 0.2s"
|
|
137
|
-
<button onclick="resetKumaDb()" style="background:rgba(239,68,68,0.15);border:1px solid rgba(239,68,68,0.3);border-radius:6px;color:var(--red);padding:5px 12px;font-size:11px;cursor:pointer;transition:all 0.2s"
|
|
138
|
-
<button onclick="stopKumaStudio()" style="background:rgba(148,163,184,0.15);border:1px solid rgba(148,163,184,0.3);border-radius:6px;color:#94a3b8;padding:5px 12px;font-size:11px;cursor:pointer;transition:all 0.2s"
|
|
226
|
+
<button onclick="refetch()" id="refetch-btn" style="background:rgba(124,58,237,0.15);border:1px solid rgba(124,58,237,0.3);border-radius:6px;color:var(--accent);padding:5px 12px;font-size:11px;cursor:pointer;transition:all 0.2s"><i data-lucide="refresh-cw" style="width:13px;height:13px;vertical-align:middle;margin-right:3px"></i> Refetch</button>
|
|
227
|
+
<button onclick="resetKumaDb()" style="background:rgba(239,68,68,0.15);border:1px solid rgba(239,68,68,0.3);border-radius:6px;color:var(--red);padding:5px 12px;font-size:11px;cursor:pointer;transition:all 0.2s"><i data-lucide="trash-2" style="width:13px;height:13px;vertical-align:middle;margin-right:3px"></i> Reset DB</button>
|
|
228
|
+
<button onclick="stopKumaStudio()" style="background:rgba(148,163,184,0.15);border:1px solid rgba(148,163,184,0.3);border-radius:6px;color:#94a3b8;padding:5px 12px;font-size:11px;cursor:pointer;transition:all 0.2s"><i data-lucide="square" style="width:13px;height:13px;vertical-align:middle;margin-right:3px"></i> Stop</button>
|
|
139
229
|
</div>
|
|
140
230
|
</header>
|
|
141
231
|
<div class="stats-bar" id="stats-bar"></div>
|
|
@@ -144,68 +234,65 @@ header .status .dot.off { background: var(--red); }
|
|
|
144
234
|
<div id="graph-container"></div>
|
|
145
235
|
<div id="search-box"><input id="node-search" type="text" placeholder="Search nodes..." /></div>
|
|
146
236
|
<div id="graph-controls">
|
|
147
|
-
<button onclick="
|
|
148
|
-
<
|
|
149
|
-
<button
|
|
150
|
-
<button
|
|
151
|
-
<button
|
|
237
|
+
<button onclick="if(network)network.fit()" title="Fit to viewport"><i data-lucide="maximize-2" style="width:12px;height:12px"></i> Fit</button>
|
|
238
|
+
<div class="ctrl-sep"></div>
|
|
239
|
+
<button class="depth-btn" data-depth="1" onclick="setDepth(1)">1</button>
|
|
240
|
+
<button class="depth-btn active" data-depth="2" onclick="setDepth(2)">2</button>
|
|
241
|
+
<button class="depth-btn" data-depth="3" onclick="setDepth(3)">3</button>
|
|
242
|
+
<button class="depth-btn" data-depth="99" onclick="setDepth(99)">All</button>
|
|
243
|
+
<div class="ctrl-sep"></div>
|
|
244
|
+
<button id="btn-freeze-physics" onclick="stopPhysics()"><i data-lucide="pause" style="width:12px;height:12px"></i></button>
|
|
245
|
+
<button id="btn-unfreeze-physics" class="active" onclick="restartPhysics()"><i data-lucide="play" style="width:12px;height:12px"></i></button>
|
|
152
246
|
</div>
|
|
153
247
|
<div id="graph-tooltip"></div>
|
|
154
248
|
<div id="legend"></div>
|
|
155
249
|
</div>
|
|
156
250
|
<div id="sidebar">
|
|
157
251
|
<div class="tabs">
|
|
158
|
-
<div class="tab active" data-tab="
|
|
159
|
-
<div class="tab" data-tab="
|
|
160
|
-
<div class="tab" data-tab="health"
|
|
252
|
+
<div class="tab active" data-tab="features"><i data-lucide="star" style="width:13px;height:13px;vertical-align:middle;margin-right:3px"></i> Features</div>
|
|
253
|
+
<div class="tab" data-tab="gotchas"><i data-lucide="alert-triangle" style="width:13px;height:13px;vertical-align:middle;margin-right:3px"></i> Gotchas</div>
|
|
254
|
+
<div class="tab" data-tab="health"><i data-lucide="heart-pulse" style="width:13px;height:13px;vertical-align:middle;margin-right:3px"></i> Health</div>
|
|
161
255
|
</div>
|
|
162
256
|
<div class="tab-content" id="tab-content"></div>
|
|
163
257
|
</div>
|
|
164
258
|
</div>
|
|
259
|
+
<div id="modal-root"></div>
|
|
260
|
+
<div id="ctx-menu">
|
|
261
|
+
<div class="ctx-item" onclick="ctxViewDetail()"><i data-lucide="eye"></i> View Details</div>
|
|
262
|
+
<div class="ctx-item" onclick="ctxCopyId()"><i data-lucide="copy"></i> Copy ID</div>
|
|
263
|
+
<div class="ctx-sep"></div>
|
|
264
|
+
<div class="ctx-item" onclick="ctxZoomTo()"><i data-lucide="focus"></i> Zoom to Node</div>
|
|
265
|
+
</div>
|
|
165
266
|
|
|
166
267
|
<script>
|
|
167
|
-
|
|
168
|
-
|
|
268
|
+
var API = "/api";
|
|
269
|
+
var network = null, nodesData = null, allNodes = null, allEdges = null, currentDbSize = "0 KB";
|
|
270
|
+
var edgeDefaults = {};
|
|
169
271
|
|
|
170
272
|
async function resetKumaDb() {
|
|
171
|
-
var confirmation = prompt("⚠️
|
|
172
|
-
if (confirmation !== "DELETE") {
|
|
173
|
-
if (confirmation !== null) alert("Reset cancelled. Confirmation text did not match 'DELETE'.");
|
|
174
|
-
return;
|
|
175
|
-
}
|
|
273
|
+
var confirmation = prompt("⚠️ TYPE 'DELETE' TO CONFIRM RESETTING KUMA DB\nThis will permanently wipe all knowledge:");
|
|
274
|
+
if (confirmation !== "DELETE") { if (confirmation !== null) alert("Reset cancelled."); return; }
|
|
176
275
|
try {
|
|
177
276
|
var res = await fetch(API + "/reset-db", { method: "POST" }).then(function(r){return r.json()});
|
|
178
|
-
if (res.ok) {
|
|
179
|
-
|
|
180
|
-
refetch();
|
|
181
|
-
}
|
|
182
|
-
} catch (e) {
|
|
183
|
-
alert("Reset failed: " + e.message);
|
|
184
|
-
}
|
|
277
|
+
if (res.ok) { alert("Kuma DB reset."); refetch(); }
|
|
278
|
+
} catch (e) { alert("Reset failed: " + e.message); }
|
|
185
279
|
}
|
|
186
280
|
|
|
187
281
|
async function stopKumaStudio() {
|
|
188
|
-
if (!confirm("Stop Kuma Studio
|
|
282
|
+
if (!confirm("Stop Kuma Studio?")) return;
|
|
189
283
|
try {
|
|
190
284
|
await fetch(API + "/stop-server", { method: "POST" });
|
|
191
|
-
document.body.innerHTML = '<div style="display:flex;height:100vh;align-items:center;justify-content:center;color:#94a3b8;font-size:16px;">
|
|
192
|
-
} catch (e) {
|
|
193
|
-
alert("Stop failed: " + e.message);
|
|
194
|
-
}
|
|
285
|
+
document.body.innerHTML = '<div style="display:flex;height:100vh;align-items:center;justify-content:center;color:#94a3b8;font-size:16px;">Server stopped. Close this tab.</div>';
|
|
286
|
+
} catch (e) { alert("Stop failed: " + e.message); }
|
|
195
287
|
}
|
|
196
288
|
|
|
197
289
|
function resetGraphFocus() {
|
|
198
290
|
if (!network) return;
|
|
199
291
|
network.body.data.nodes.forEach(function(n) {
|
|
200
|
-
network.body.data.nodes.update({
|
|
201
|
-
id: n.id,
|
|
202
|
-
opacity: 1.0,
|
|
203
|
-
font: { color: '#ffffff', bold: true }
|
|
204
|
-
});
|
|
292
|
+
network.body.data.nodes.update({ id: n.id, opacity: 1.0, font: { color: '#ffffff', bold: true } });
|
|
205
293
|
});
|
|
206
294
|
document.getElementById("graph-tooltip").style.display = 'none';
|
|
207
|
-
var
|
|
208
|
-
if (searchInput) searchInput.value = "";
|
|
295
|
+
var s = document.getElementById("node-search"); if (s) s.value = "";
|
|
209
296
|
}
|
|
210
297
|
|
|
211
298
|
function exportGraphPNG() {
|
|
@@ -238,29 +325,25 @@ async function init() {
|
|
|
238
325
|
var status = await fetch(API+"/status").then(function(r){return r.json()});
|
|
239
326
|
var dot = document.getElementById("status-dot");
|
|
240
327
|
var txt = document.getElementById("status-text");
|
|
241
|
-
if (status.ok) {
|
|
242
|
-
|
|
243
|
-
txt.textContent = status.project || "connected";
|
|
244
|
-
currentDbSize = status.dbSize || "0 KB";
|
|
245
|
-
} else {
|
|
246
|
-
dot.className = "dot off";
|
|
247
|
-
txt.textContent = "no kuma.db found";
|
|
248
|
-
}
|
|
328
|
+
if (status.ok) { dot.className = "dot on"; txt.textContent = status.project || "connected"; currentDbSize = status.dbSize || "0 KB"; }
|
|
329
|
+
else { dot.className = "dot off"; txt.textContent = "no kuma.db found"; }
|
|
249
330
|
document.getElementById("loading").style.display = "none";
|
|
250
331
|
var data = await fetch(API+"/dashboard").then(function(r){return r.json()});
|
|
251
332
|
if (data.error) return showError(data.error);
|
|
252
333
|
setupTabs();
|
|
253
334
|
renderStats(data);
|
|
254
335
|
renderGraph(data);
|
|
336
|
+
renderFeatures(data);
|
|
255
337
|
renderGotchas(data);
|
|
256
|
-
renderTrajectories(data);
|
|
257
338
|
renderHealth(data);
|
|
258
339
|
setupSearch();
|
|
340
|
+
lucide.createIcons();
|
|
259
341
|
}
|
|
260
342
|
init();
|
|
261
343
|
|
|
262
344
|
function showError(msg) {
|
|
263
|
-
document.getElementById("graph-container").innerHTML = '<div class="empty"
|
|
345
|
+
document.getElementById("graph-container").innerHTML = '<div class="empty"><i data-lucide="alert-circle" style="width:18px;height:18px;vertical-align:middle;margin-right:4px"></i> '+msg+'</div>';
|
|
346
|
+
lucide.createIcons();
|
|
264
347
|
}
|
|
265
348
|
|
|
266
349
|
function renderStats(data) {
|
|
@@ -270,25 +353,23 @@ function renderStats(data) {
|
|
|
270
353
|
document.getElementById("stats-bar").innerHTML =
|
|
271
354
|
'<div class="stat"><span class="num">'+(s.node_count||0)+'</span><span class="lbl">nodes</span></div>'+
|
|
272
355
|
'<div class="stat"><span class="num">'+(s.edge_count||0)+'</span><span class="lbl">edges</span></div>'+
|
|
356
|
+
'<div class="stat"><span class="num" style="color:var(--accent)">'+(s.feature_count||0)+'</span><span class="lbl">features</span></div>'+
|
|
273
357
|
'<div class="stat"><span class="num">'+(s.gotcha_count||0)+'</span><span class="lbl">gotchas</span></div>'+
|
|
274
|
-
'<div class="stat"><span class="num">'+(s.trajectory_count||0)+'</span><span class="lbl">trajectories</span></div>'+
|
|
275
358
|
'<div class="stat"><span class="num" style="color:var(--accent)">'+currentDbSize+'</span><span class="lbl">kuma.db</span></div>'+
|
|
276
359
|
'<div class="stat"><span class="num" style="color:'+hcColor+'">'+(hc||0)+'</span><span class="lbl">health</span></div>';
|
|
277
360
|
}
|
|
278
361
|
|
|
279
|
-
/** Truncate long labels for display */
|
|
280
362
|
function truncateLabel(name, maxLen) {
|
|
281
363
|
if (!name) return "";
|
|
282
|
-
|
|
283
|
-
return name.substring(0, maxLen-2) + "..";
|
|
364
|
+
return name.length <= maxLen ? name : name.substring(0, maxLen-2) + "..";
|
|
284
365
|
}
|
|
285
366
|
|
|
286
|
-
/** Calculate node size based on label length and type */
|
|
287
367
|
function calcNodeSize(name, type) {
|
|
288
368
|
var isGotcha = name && name.toLowerCase().indexOf("gotcha:") === 0;
|
|
289
369
|
if (isGotcha) return 14;
|
|
290
370
|
if (type === "variable") return 14;
|
|
291
371
|
var base = 18;
|
|
372
|
+
if (type === "feature") base = 32;
|
|
292
373
|
if (type === "feature_domain") base = 30;
|
|
293
374
|
if (type === "workflow") base = 24;
|
|
294
375
|
if (type === "cross_service_link") base = 20;
|
|
@@ -296,13 +377,12 @@ function calcNodeSize(name, type) {
|
|
|
296
377
|
if (type === "class") base = 20;
|
|
297
378
|
if (type === "module") base = 22;
|
|
298
379
|
if (type === "component") base = 20;
|
|
299
|
-
if (type === "
|
|
380
|
+
if (type === "api_route") base = 18;
|
|
300
381
|
if (type === "test") base = 18;
|
|
301
382
|
var len = name ? Math.min(name.length, 25) : 4;
|
|
302
|
-
return Math.min(
|
|
383
|
+
return Math.min(36, Math.max(base, base + len * 0.4));
|
|
303
384
|
}
|
|
304
385
|
|
|
305
|
-
|
|
306
386
|
function renderGraph(data) {
|
|
307
387
|
allNodes = data.nodes || [];
|
|
308
388
|
allEdges = data.edges || [];
|
|
@@ -311,8 +391,8 @@ function renderGraph(data) {
|
|
|
311
391
|
return;
|
|
312
392
|
}
|
|
313
393
|
|
|
314
|
-
var types = [...new Set(allNodes.map(function(n){return n.type||'unknown'}))];
|
|
315
394
|
var colors = {
|
|
395
|
+
feature: '#a855f7',
|
|
316
396
|
feature_domain: '#dc2626',
|
|
317
397
|
workflow: '#2563eb',
|
|
318
398
|
cross_service_link: '#059669',
|
|
@@ -321,11 +401,13 @@ function renderGraph(data) {
|
|
|
321
401
|
module: '#16a34a',
|
|
322
402
|
component: '#ea580c',
|
|
323
403
|
file: '#d97706',
|
|
324
|
-
|
|
404
|
+
api_route: '#db2777',
|
|
325
405
|
test: '#0d9488',
|
|
326
|
-
variable: '#6366f1'
|
|
406
|
+
variable: '#6366f1',
|
|
407
|
+
gotcha: '#f97316',
|
|
327
408
|
};
|
|
328
409
|
var shapes = {
|
|
410
|
+
feature: 'star',
|
|
329
411
|
feature_domain: 'star',
|
|
330
412
|
workflow: 'ellipse',
|
|
331
413
|
cross_service_link: 'box',
|
|
@@ -334,309 +416,352 @@ function renderGraph(data) {
|
|
|
334
416
|
module: 'ellipse',
|
|
335
417
|
component: 'diamond',
|
|
336
418
|
file: 'box',
|
|
337
|
-
|
|
419
|
+
api_route: 'triangleDown',
|
|
338
420
|
test: 'star',
|
|
339
|
-
variable: 'box'
|
|
421
|
+
variable: 'box',
|
|
340
422
|
};
|
|
341
423
|
|
|
342
|
-
// Limit nodes for performance if > 200
|
|
343
424
|
var maxNodes = 300;
|
|
344
425
|
var displayNodes = allNodes;
|
|
345
426
|
var truncated = allNodes.length > maxNodes;
|
|
346
|
-
if (truncated)
|
|
347
|
-
displayNodes = allNodes.slice(0, maxNodes);
|
|
348
|
-
}
|
|
427
|
+
if (truncated) displayNodes = allNodes.slice(0, maxNodes);
|
|
349
428
|
|
|
350
429
|
var visNodes = new vis.DataSet(displayNodes.map(function(n){
|
|
351
430
|
var isGotcha = n.name && n.name.toLowerCase().indexOf("gotcha:") === 0;
|
|
352
|
-
var isVariable = n.type === 'variable' && !isGotcha;
|
|
353
431
|
var shape = isGotcha ? 'triangle' : (shapes[n.type] || 'dot');
|
|
354
|
-
var maxLen = isGotcha ? 20 : (shape === 'ellipse' ? 18 :
|
|
432
|
+
var maxLen = isGotcha ? 20 : (shape === 'ellipse' ? 18 : 32);
|
|
355
433
|
var label = truncateLabel(n.name, maxLen);
|
|
356
|
-
var size = isGotcha ? 12 :
|
|
434
|
+
var size = isGotcha ? 12 : calcNodeSize(n.name, n.type);
|
|
357
435
|
var color = isGotcha ? '#f97316' : (colors[n.type] || '#64748b');
|
|
358
436
|
return {
|
|
359
|
-
id: n.id,
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
color: {
|
|
363
|
-
background: color,
|
|
364
|
-
border: color,
|
|
365
|
-
highlight: { background: color, border: '#38bdf8' },
|
|
366
|
-
hover: { background: color, border: '#a855f7' }
|
|
367
|
-
},
|
|
437
|
+
id: n.id, label: label,
|
|
438
|
+
color: { background: color, border: color, highlight: { background: color, border: '#38bdf8' }, hover: { background: color, border: '#a855f7' } },
|
|
368
439
|
shape: shape,
|
|
369
|
-
font:
|
|
370
|
-
|
|
371
|
-
: { color: '#ffffff', size: Math.min(13, Math.max(10, size * 0.55)), face: 'sans-serif', multi: false, bold: true },
|
|
372
|
-
borderWidth: 0,
|
|
373
|
-
size: size,
|
|
374
|
-
mass: 1 + (size / 40),
|
|
440
|
+
font: { color: '#ffffff', size: Math.min(13, Math.max(10, size * 0.55)), face: 'sans-serif', multi: false, bold: true },
|
|
441
|
+
borderWidth: 0, size: size, mass: 1 + (size / 40),
|
|
375
442
|
};
|
|
376
443
|
}));
|
|
377
444
|
|
|
378
445
|
var nodeIds = new Set(displayNodes.map(function(n){return n.id}));
|
|
379
446
|
var edgeList = allEdges.slice();
|
|
380
447
|
|
|
381
|
-
|
|
382
|
-
var nameToLinkIds = {};
|
|
383
|
-
displayNodes.forEach(function(n) {
|
|
384
|
-
if (n.type === 'cross_service_link') {
|
|
385
|
-
var cleanName = n.name.toLowerCase().trim();
|
|
386
|
-
if (!nameToLinkIds[cleanName]) nameToLinkIds[cleanName] = [];
|
|
387
|
-
nameToLinkIds[cleanName].push(n.id);
|
|
388
|
-
}
|
|
389
|
-
});
|
|
390
|
-
|
|
391
|
-
displayNodes.forEach(function(n) {
|
|
392
|
-
if (n.type === 'file' || n.type === 'component' || n.type === 'function') {
|
|
393
|
-
var fn = n.name.toLowerCase().trim();
|
|
394
|
-
Object.keys(nameToLinkIds).forEach(function(linkName) {
|
|
395
|
-
if (fn.includes(linkName) || linkName.includes(fn)) {
|
|
396
|
-
var targetLinkIds = nameToLinkIds[linkName];
|
|
397
|
-
targetLinkIds.forEach(function(linkId) {
|
|
398
|
-
edgeList.push({
|
|
399
|
-
source: n.id,
|
|
400
|
-
target: linkId,
|
|
401
|
-
relation: 'contains'
|
|
402
|
-
});
|
|
403
|
-
});
|
|
404
|
-
}
|
|
405
|
-
});
|
|
406
|
-
}
|
|
407
|
-
});
|
|
448
|
+
var displayEdges = edgeList.filter(function(e){ return nodeIds.has(e.source) && nodeIds.has(e.target); }).slice(0, 500);
|
|
408
449
|
|
|
409
|
-
|
|
410
|
-
return nodeIds.has(e.source) && nodeIds.has(e.target);
|
|
411
|
-
}).slice(0, 500);
|
|
412
|
-
|
|
413
|
-
var edgeDefaults = {};
|
|
450
|
+
edgeDefaults = {};
|
|
414
451
|
var visEdges = new vis.DataSet(displayEdges.map(function(e, idx){
|
|
415
|
-
var edgeColor = '#
|
|
452
|
+
var edgeColor = '#4a4a6a';
|
|
416
453
|
var width = 1.5;
|
|
417
454
|
var dashes = false;
|
|
418
455
|
if (e.relation === 'flows_through') { edgeColor = '#3b82f6'; width = 2.5; }
|
|
456
|
+
if (e.relation === 'affects') { edgeColor = '#ef4444'; width = 2; dashes = [5, 5]; }
|
|
419
457
|
if (e.relation === 'triggers') { edgeColor = '#ef4444'; width = 2; dashes = true; }
|
|
420
458
|
if (e.relation === 'syncs_with') { edgeColor = '#10b981'; width = 2; }
|
|
421
|
-
if (e.relation === '
|
|
459
|
+
if (e.relation === 'owns') { edgeColor = '#a855f7'; width = 2; }
|
|
460
|
+
if (e.relation === 'contains') { edgeColor = '#64748b'; width = 1; dashes = true; }
|
|
461
|
+
if (e.relation === 'depends_on') { edgeColor = '#eab308'; width = 1.5; }
|
|
422
462
|
|
|
423
463
|
edgeDefaults['e'+idx] = { color: edgeColor, width: width };
|
|
424
464
|
return {
|
|
425
|
-
id: 'e'+idx,
|
|
426
|
-
from: e.source, to: e.target,
|
|
465
|
+
id: 'e'+idx, from: e.source, to: e.target,
|
|
427
466
|
label: e.relation || '',
|
|
428
467
|
arrows: { to: { enabled: true, scaleFactor: 0.7, type: 'arrow' } },
|
|
429
468
|
color: { color: edgeColor, highlight: '#a855f7', hover: '#38bdf8', opacity: 0.85 },
|
|
430
469
|
font: { size: 10, color: '#94a3b8', strokeWidth: 0, background: 'transparent' },
|
|
431
470
|
dashes: dashes,
|
|
432
471
|
smooth: { type: 'continuous', roundness: 0.3 },
|
|
433
|
-
width: width,
|
|
434
|
-
hoverWidth: 3,
|
|
435
|
-
selectionWidth: 3,
|
|
472
|
+
width: width, hoverWidth: 3, selectionWidth: 3,
|
|
436
473
|
};
|
|
437
474
|
}));
|
|
438
475
|
|
|
439
476
|
var container = document.getElementById("graph-container");
|
|
440
477
|
network = new vis.Network(container, {nodes: visNodes, edges: visEdges}, {
|
|
441
478
|
physics: {
|
|
442
|
-
enabled: true,
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
gravitationalConstant: -2000,
|
|
446
|
-
centralGravity: 0.3,
|
|
447
|
-
springLength: 150,
|
|
448
|
-
springConstant: 0.04,
|
|
449
|
-
damping: 0.6,
|
|
450
|
-
avoidOverlap: 0.5,
|
|
451
|
-
},
|
|
452
|
-
stabilization: { iterations: 400, updateInterval: 25, onlyDynamicEdges: false },
|
|
453
|
-
adaptiveTimestep: true,
|
|
454
|
-
},
|
|
455
|
-
interaction: {
|
|
456
|
-
hover: true,
|
|
457
|
-
tooltipDelay: 300,
|
|
458
|
-
zoomView: true,
|
|
459
|
-
dragView: true,
|
|
460
|
-
dragNodes: true,
|
|
461
|
-
keyboard: true,
|
|
462
|
-
multiselect: true,
|
|
463
|
-
selectable: true,
|
|
464
|
-
selectConnectedEdges: false,
|
|
465
|
-
},
|
|
466
|
-
nodes: {
|
|
467
|
-
borderWidth: 0,
|
|
468
|
-
font: { color: '#ffffff', size: 13, face: 'sans-serif', bold: 'true' },
|
|
469
|
-
margin: { top: 10, bottom: 10, left: 16, right: 16 },
|
|
470
|
-
shapeProperties: { borderRadius: 8 },
|
|
471
|
-
shadow: { enabled: true, color: 'rgba(0,0,0,0.4)', size: 8, x: 0, y: 3 },
|
|
472
|
-
},
|
|
473
|
-
edges: {
|
|
474
|
-
smooth: { type: 'continuous', roundness: 0.3 },
|
|
475
|
-
width: 1.5,
|
|
476
|
-
color: { color: '#4a4a6a', highlight: '#7c3aed' },
|
|
479
|
+
enabled: true, solver: 'barnesHut',
|
|
480
|
+
barnesHut: { gravitationalConstant: -3000, centralGravity: 0.05, springLength: 150, springConstant: 0.02, damping: 0.4, avoidOverlap: 0.5 },
|
|
481
|
+
stabilization: { iterations: 120, updateInterval: 50, onlyDynamicEdges: false, fit: false },
|
|
477
482
|
},
|
|
483
|
+
interaction: { hover: true, tooltipDelay: 300, zoomView: true, dragView: true, dragNodes: true, keyboard: true, multiselect: true, selectable: true, selectConnectedEdges: false },
|
|
484
|
+
nodes: { borderWidth: 0, font: { color: '#ffffff', size: 11, face: 'sans-serif' }, margin: { top: 6, bottom: 6, left: 10, right: 10 }, shapeProperties: { borderRadius: 6 } },
|
|
485
|
+
edges: { smooth: { type: 'continuous', roundness: 0.2 }, width: 1, color: { color: '#4a4a6a', highlight: '#7c3aed' } },
|
|
478
486
|
configure: { enabled: false },
|
|
479
487
|
});
|
|
480
488
|
|
|
481
|
-
//
|
|
489
|
+
// Auto-freeze physics after stabilization (saves CPU)
|
|
490
|
+
network.once('stabilizationIterationsDone', function() {
|
|
491
|
+
network.setOptions({ physics: { enabled: false } });
|
|
492
|
+
document.getElementById('btn-freeze-physics').classList.add('active');
|
|
493
|
+
document.getElementById('btn-unfreeze-physics').classList.remove('active');
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
// Left-click node → focus only (no modal)
|
|
497
|
+
var lastFocusedNode = null;
|
|
498
|
+
var currentFocusDepth = 2;
|
|
499
|
+
window.setDepth = function(d) {
|
|
500
|
+
currentFocusDepth = d;
|
|
501
|
+
document.querySelectorAll('.depth-btn').forEach(function(b) { b.classList.toggle('active', parseInt(b.dataset.depth) === d); });
|
|
502
|
+
if (lastFocusedNode) focusNode(lastFocusedNode);
|
|
503
|
+
};
|
|
504
|
+
function focusNode(selectedId) {
|
|
505
|
+
// BFS with depth limit
|
|
506
|
+
var connected = new Map();
|
|
507
|
+
var queue = [[selectedId, 0]];
|
|
508
|
+
connected.set(selectedId, 0);
|
|
509
|
+
while (queue.length) {
|
|
510
|
+
var pair = queue.shift();
|
|
511
|
+
var cur = pair[0], depth = pair[1];
|
|
512
|
+
if (depth >= currentFocusDepth) continue;
|
|
513
|
+
network.getConnectedNodes(cur).forEach(function(nid) {
|
|
514
|
+
if (!connected.has(nid)) { connected.set(nid, depth + 1); queue.push([nid, depth + 1]); }
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
var nodeUpdates = [], edgeUpdates = [];
|
|
518
|
+
network.body.data.nodes.forEach(function(n) {
|
|
519
|
+
var hit = connected.has(n.id);
|
|
520
|
+
nodeUpdates.push({ id: n.id, opacity: hit ? 1.0 : 0.08, font: { color: hit ? '#ffffff' : 'rgba(255,255,255,0.04)', bold: hit } });
|
|
521
|
+
});
|
|
522
|
+
network.body.data.nodes.update(nodeUpdates);
|
|
523
|
+
network.body.data.edges.forEach(function(e) {
|
|
524
|
+
var hit = connected.has(e.from) && connected.has(e.to);
|
|
525
|
+
var def = edgeDefaults[e.id] || {};
|
|
526
|
+
edgeUpdates.push({ id: e.id, color: { color: hit ? (def.color || '#64748b') : 'rgba(100,116,139,0.03)', highlight: '#a855f7', hover: '#38bdf8', opacity: hit ? 0.9 : 0.03 }, width: hit ? (def.width || 1) : 0.15 });
|
|
527
|
+
});
|
|
528
|
+
network.body.data.edges.update(edgeUpdates);
|
|
529
|
+
}
|
|
482
530
|
network.on('click', function(params) {
|
|
531
|
+
hideCtxMenu();
|
|
483
532
|
if (params.nodes.length) {
|
|
484
533
|
var selectedId = params.nodes[0];
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
network.body.data.nodes.forEach(function(n) {
|
|
489
|
-
var isConnected = connectedNodeIds.has(n.id);
|
|
490
|
-
network.body.data.nodes.update({
|
|
491
|
-
id: n.id,
|
|
492
|
-
opacity: isConnected ? 1.0 : 0.12,
|
|
493
|
-
font: {
|
|
494
|
-
color: isConnected ? '#ffffff' : 'rgba(255,255,255,0.07)',
|
|
495
|
-
bold: isConnected
|
|
496
|
-
}
|
|
497
|
-
});
|
|
498
|
-
});
|
|
499
|
-
|
|
500
|
-
network.body.data.edges.forEach(function(e) {
|
|
501
|
-
var def = edgeDefaults[e.id];
|
|
502
|
-
var isConnected = connectedNodeIds.has(e.from) && connectedNodeIds.has(e.to);
|
|
503
|
-
network.body.data.edges.update({
|
|
504
|
-
id: e.id,
|
|
505
|
-
color: {
|
|
506
|
-
color: isConnected ? (def ? def.color : '#64748b') : 'rgba(100,116,139,0.05)',
|
|
507
|
-
highlight: '#a855f7',
|
|
508
|
-
hover: '#38bdf8',
|
|
509
|
-
opacity: isConnected ? 0.85 : 0.05
|
|
510
|
-
},
|
|
511
|
-
width: isConnected ? (def ? def.width : 1.5) : 0.3,
|
|
512
|
-
font: {
|
|
513
|
-
color: isConnected ? '#94a3b8' : 'rgba(148,163,184,0.1)',
|
|
514
|
-
size: 10,
|
|
515
|
-
strokeWidth: 0,
|
|
516
|
-
background: 'transparent'
|
|
517
|
-
}
|
|
518
|
-
});
|
|
519
|
-
});
|
|
534
|
+
if (lastFocusedNode === selectedId) { lastFocusedNode = null; resetHighlight(); return; }
|
|
535
|
+
lastFocusedNode = selectedId;
|
|
536
|
+
focusNode(selectedId);
|
|
520
537
|
} else {
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
network.body.data.nodes.update({
|
|
524
|
-
id: n.id,
|
|
525
|
-
opacity: 1.0,
|
|
526
|
-
font: { color: '#ffffff', bold: true }
|
|
527
|
-
});
|
|
528
|
-
});
|
|
529
|
-
network.body.data.edges.forEach(function(e) {
|
|
530
|
-
var def = edgeDefaults[e.id];
|
|
531
|
-
network.body.data.edges.update({
|
|
532
|
-
id: e.id,
|
|
533
|
-
color: {
|
|
534
|
-
color: def ? def.color : '#64748b',
|
|
535
|
-
highlight: '#a855f7',
|
|
536
|
-
hover: '#38bdf8',
|
|
537
|
-
opacity: 0.85
|
|
538
|
-
},
|
|
539
|
-
width: def ? def.width : 1.5,
|
|
540
|
-
font: { size: 10, color: '#94a3b8', strokeWidth: 0, background: 'transparent' }
|
|
541
|
-
});
|
|
542
|
-
});
|
|
538
|
+
lastFocusedNode = null;
|
|
539
|
+
resetHighlight();
|
|
543
540
|
}
|
|
544
541
|
});
|
|
545
542
|
|
|
546
|
-
//
|
|
547
|
-
|
|
548
|
-
var
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
543
|
+
// Right-click node → context menu (document level, capture phase)
|
|
544
|
+
document.addEventListener('contextmenu', function(e) {
|
|
545
|
+
var containerEl = document.getElementById("graph-container");
|
|
546
|
+
if (!containerEl || !containerEl.contains(e.target)) return;
|
|
547
|
+
e.preventDefault();
|
|
548
|
+
var rect = containerEl.getBoundingClientRect();
|
|
549
|
+
var x = e.clientX - rect.left;
|
|
550
|
+
var y = e.clientY - rect.top;
|
|
551
|
+
var nodeId = network.getNodeAt({ x: x, y: y });
|
|
552
|
+
if (nodeId && typeof nodeId === 'string') {
|
|
553
|
+
var menu = document.getElementById('ctx-menu');
|
|
554
|
+
menu.style.display = 'block';
|
|
555
|
+
menu.style.left = e.clientX + 'px';
|
|
556
|
+
menu.style.top = e.clientY + 'px';
|
|
557
|
+
menu.dataset.nodeId = nodeId;
|
|
558
|
+
lucide.createIcons();
|
|
559
|
+
}
|
|
560
|
+
}, true);
|
|
561
|
+
// Close context menu on mousedown outside
|
|
562
|
+
document.addEventListener('mousedown', function(e) {
|
|
563
|
+
if (!e.target.closest('#ctx-menu')) hideCtxMenu();
|
|
564
|
+
});
|
|
554
565
|
|
|
555
|
-
|
|
566
|
+
// Hover → tooltip
|
|
567
|
+
network.on('hoverNode', function(params) {
|
|
568
|
+
network.body.data.nodes.updateOnly({ id: params.node, borderWidth: 2, borderWidthSelected: 2 });
|
|
569
|
+
var n = allNodes.find(function(x){return x.id===params.node});
|
|
556
570
|
if (n) {
|
|
557
571
|
var tip = document.getElementById("graph-tooltip");
|
|
558
572
|
tip.style.display = 'block';
|
|
559
573
|
tip.innerHTML = '<div style="word-break:break-all;overflow-wrap:anywhere;max-width:390px;white-space:normal;line-height:1.4"><b style="color:#f8fafc">' + n.name + '</b> <span style="color:#94a3b8">' + n.type + '</span>' +
|
|
560
|
-
(n.file_path ? '<br><span style="color:#38bdf8;font-size:11px;word-break:break-all;overflow-wrap:anywhere">' + n.file_path + '</span>' : '') +
|
|
574
|
+
(n.file_path ? '<br><span style="color:#38bdf8;font-size:11px;word-break:break-all;overflow-wrap:anywhere">' + n.file_path + '</span>' : '') +
|
|
575
|
+
'<br><span style="font-size:10px;color:#64748b">Right-click for options</span></div>';
|
|
561
576
|
var pos = network.getPositions([n.id]);
|
|
562
577
|
if (pos && pos[n.id]) {
|
|
563
578
|
var canvasPos = network.canvasToDOM(pos[n.id]);
|
|
564
579
|
var panelRect = container.getBoundingClientRect();
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
tip.style.left = Math.max(tipX, 8) + 'px';
|
|
568
|
-
tip.style.top = tipY + 'px';
|
|
580
|
+
tip.style.left = Math.max(Math.min(canvasPos.x + 12, panelRect.width - 340), 8) + 'px';
|
|
581
|
+
tip.style.top = Math.max(canvasPos.y - 10, 0) + 'px';
|
|
569
582
|
}
|
|
570
583
|
}
|
|
571
584
|
});
|
|
572
585
|
network.on('blurNode', function(params) {
|
|
573
|
-
network.body.data.nodes.updateOnly({
|
|
574
|
-
id: params.node,
|
|
575
|
-
borderWidth: 0,
|
|
576
|
-
borderWidthSelected: 0,
|
|
577
|
-
});
|
|
586
|
+
network.body.data.nodes.updateOnly({ id: params.node, borderWidth: 0, borderWidthSelected: 0 });
|
|
578
587
|
document.getElementById("graph-tooltip").style.display = 'none';
|
|
579
588
|
});
|
|
580
589
|
|
|
590
|
+
var types = [...new Set(allNodes.map(function(n){return n.type||'unknown'}))];
|
|
581
591
|
renderLegend(types, colors);
|
|
582
592
|
|
|
583
|
-
// Show truncation warning
|
|
584
593
|
if (truncated) {
|
|
585
594
|
var warn = document.createElement('div');
|
|
586
595
|
warn.style.cssText = 'position:absolute;top:16px;left:16px;z-index:10;background:rgba(234,179,8,0.15);color:var(--yellow);border:1px solid rgba(234,179,8,0.3);border-radius:8px;padding:8px 14px;font-size:12px;backdrop-filter:blur(8px);pointer-events:none';
|
|
587
|
-
warn.innerHTML = '
|
|
596
|
+
warn.innerHTML = '<i data-lucide="alert-triangle" style="width:14px;height:14px;vertical-align:middle;margin-right:4px"></i> Showing ' + maxNodes + ' / ' + allNodes.length + ' nodes';
|
|
588
597
|
document.getElementById('graph-panel').appendChild(warn);
|
|
589
598
|
}
|
|
599
|
+
}
|
|
590
600
|
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
601
|
+
function hideCtxMenu() { document.getElementById('ctx-menu').style.display = 'none'; }
|
|
602
|
+
function resetHighlight() {
|
|
603
|
+
if (!network) return;
|
|
604
|
+
var nUp = [], eUp = [];
|
|
605
|
+
network.body.data.nodes.forEach(function(n) { nUp.push({ id: n.id, opacity: 1.0, font: { color: '#ffffff', bold: true } }); });
|
|
606
|
+
network.body.data.nodes.update(nUp);
|
|
607
|
+
network.body.data.edges.forEach(function(e) {
|
|
608
|
+
var def = edgeDefaults[e.id];
|
|
609
|
+
eUp.push({ id: e.id, color: { color: def ? def.color : '#64748b', highlight: '#a855f7', hover: '#38bdf8', opacity: 0.85 }, width: def ? def.width : 1 });
|
|
594
610
|
});
|
|
611
|
+
network.body.data.edges.update(eUp);
|
|
612
|
+
document.getElementById("graph-tooltip").style.display = 'none';
|
|
613
|
+
}
|
|
614
|
+
function ctxViewDetail() {
|
|
615
|
+
var id = document.getElementById('ctx-menu').dataset.nodeId;
|
|
616
|
+
hideCtxMenu();
|
|
617
|
+
if (id) openNodeModal(id);
|
|
618
|
+
}
|
|
619
|
+
function ctxCopyId() {
|
|
620
|
+
var id = document.getElementById('ctx-menu').dataset.nodeId;
|
|
621
|
+
hideCtxMenu();
|
|
622
|
+
if (id) navigator.clipboard.writeText(id).catch(function(){});
|
|
623
|
+
}
|
|
624
|
+
function ctxZoomTo() {
|
|
625
|
+
var id = document.getElementById('ctx-menu').dataset.nodeId;
|
|
626
|
+
hideCtxMenu();
|
|
627
|
+
if (id && network) network.focus(id, { scale: 1.5, animation: { duration: 400, easingFunction: 'easeOutQuad' } });
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
// ============ NODE DETAIL MODAL ============
|
|
631
|
+
|
|
632
|
+
async function openNodeModal(nodeId) {
|
|
633
|
+
var node = allNodes.find(function(n){return n.id === nodeId;});
|
|
634
|
+
if (!node) return;
|
|
635
|
+
|
|
636
|
+
var root = document.getElementById("modal-root");
|
|
637
|
+
root.innerHTML = '<div class="modal-overlay" onclick="if(event.target===this)closeModal()"><div class="modal"><div class="modal-header"><h2>'+escapeHtml(node.name)+'</h2><button class="close" onclick="closeModal()">×</button></div><div class="modal-body"><div style="text-align:center;padding:20px;color:var(--muted)">Loading details...</div></div></div></div>';
|
|
638
|
+
|
|
639
|
+
try {
|
|
640
|
+
var detail = await fetch(API+"/node/"+encodeURIComponent(nodeId)).then(function(r){return r.json()});
|
|
641
|
+
var body = root.querySelector('.modal-body');
|
|
642
|
+
if (!detail || detail.error) {
|
|
643
|
+
body.innerHTML = '<div class="empty">Node not found</div>';
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
var n = typeof detail.node === 'string' ? JSON.parse(detail.node) : detail.node;
|
|
648
|
+
var outgoing = typeof detail.outgoing === 'string' ? JSON.parse(detail.outgoing) : (detail.outgoing || []);
|
|
649
|
+
var incoming = typeof detail.incoming === 'string' ? JSON.parse(detail.incoming) : (detail.incoming || []);
|
|
650
|
+
var gotchas = typeof detail.gotchas === 'string' ? JSON.parse(detail.gotchas) : (detail.gotchas || []);
|
|
651
|
+
|
|
652
|
+
var meta = {};
|
|
653
|
+
try { meta = typeof n.metadata === 'string' ? JSON.parse(n.metadata) : (n.metadata || {}); } catch(e) {}
|
|
654
|
+
|
|
655
|
+
var html = '';
|
|
656
|
+
|
|
657
|
+
// Basic info
|
|
658
|
+
html += '<div class="modal-section">';
|
|
659
|
+
html += '<h3>Info</h3>';
|
|
660
|
+
html += '<div class="modal-kv"><span class="key">Type</span><span class="val"><span style="color:var(--accent)">'+escapeHtml(n.type)+'</span></span></div>';
|
|
661
|
+
if (n.file_path) html += '<div class="modal-kv"><span class="key">Path</span><span class="val" style="color:var(--accent2)">'+escapeHtml(n.file_path)+'</span></div>';
|
|
662
|
+
if (meta.description) html += '<div class="modal-kv"><span class="key">Description</span><span class="val">'+escapeHtml(meta.description)+'</span></div>';
|
|
663
|
+
if (meta.tags && meta.tags.length) html += '<div class="modal-kv"><span class="key">Tags</span><span class="val">'+meta.tags.map(function(t){return '<span class="tag">'+escapeHtml(t)+'</span>';}).join(' ')+'</span></div>';
|
|
664
|
+
if (meta.risk) html += '<div class="modal-kv"><span class="key">Risk</span><span class="val" style="color:'+(meta.risk==='critical'?'var(--red)':meta.risk==='high'?'var(--orange)':meta.risk==='medium'?'var(--yellow)':'var(--green)')+'">'+escapeHtml(meta.risk)+'</span></div>';
|
|
665
|
+
if (n.updated_at) html += '<div class="modal-kv"><span class="key">Updated</span><span class="val">'+new Date(n.updated_at*1000).toLocaleString()+'</span></div>';
|
|
666
|
+
html += '</div>';
|
|
667
|
+
|
|
668
|
+
// Outgoing relations
|
|
669
|
+
if (outgoing.length) {
|
|
670
|
+
html += '<div class="modal-section"><h3>Outgoing ('+outgoing.length+')</h3>';
|
|
671
|
+
outgoing.forEach(function(r) {
|
|
672
|
+
var rel = typeof r === 'string' ? JSON.parse(r) : r;
|
|
673
|
+
html += '<div class="relation-item" onclick="closeModal();setTimeout(function(){openNodeModal(\''+escapeHtml(rel.target)+'\')},200)">';
|
|
674
|
+
html += '<span class="arrow"><i data-lucide="arrow-right" style="width:12px;height:12px"></i></span>';
|
|
675
|
+
html += '<span class="name">'+escapeHtml(rel.target_name || rel.target.split('::').pop())+'</span>';
|
|
676
|
+
html += '<span class="type">'+escapeHtml(rel.target_type||'')+'</span>';
|
|
677
|
+
html += '<span class="weight">w:'+((rel.weight||1).toFixed(2))+'</span>';
|
|
678
|
+
html += '<span style="color:var(--accent);font-size:10px;flex-shrink:0">'+escapeHtml(rel.relation)+'</span>';
|
|
679
|
+
html += '</div>';
|
|
680
|
+
});
|
|
681
|
+
html += '</div>';
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
// Incoming relations
|
|
685
|
+
if (incoming.length) {
|
|
686
|
+
html += '<div class="modal-section"><h3>Incoming ('+incoming.length+')</h3>';
|
|
687
|
+
incoming.forEach(function(r) {
|
|
688
|
+
var rel = typeof r === 'string' ? JSON.parse(r) : r;
|
|
689
|
+
html += '<div class="relation-item" onclick="closeModal();setTimeout(function(){openNodeModal(\''+escapeHtml(rel.source)+'\')},200)">';
|
|
690
|
+
html += '<span class="arrow"><i data-lucide="arrow-left" style="width:12px;height:12px"></i></span>';
|
|
691
|
+
html += '<span class="name">'+escapeHtml(rel.source_name || rel.source.split('::').pop())+'</span>';
|
|
692
|
+
html += '<span class="type">'+escapeHtml(rel.source_type||'')+'</span>';
|
|
693
|
+
html += '<span class="weight">w:'+((rel.weight||1).toFixed(2))+'</span>';
|
|
694
|
+
html += '<span style="color:var(--accent);font-size:10px;flex-shrink:0">'+escapeHtml(rel.relation)+'</span>';
|
|
695
|
+
html += '</div>';
|
|
696
|
+
});
|
|
697
|
+
html += '</div>';
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
// Gotchas
|
|
701
|
+
if (gotchas.length) {
|
|
702
|
+
html += '<div class="modal-section"><h3>Gotchas ('+gotchas.length+')</h3>';
|
|
703
|
+
gotchas.forEach(function(g) {
|
|
704
|
+
var got = typeof g === 'string' ? JSON.parse(g) : g;
|
|
705
|
+
html += '<div class="gotcha-item" style="border-color:var(--'+(got.severity==='critical'?'red':got.severity==='high'?'orange':got.severity==='medium'?'yellow':'green')+')">';
|
|
706
|
+
html += '<div class="desc">'+escapeHtml(got.description)+'</div>';
|
|
707
|
+
html += '<span class="sev-tag sev-'+got.severity+'">'+got.severity+'</span>';
|
|
708
|
+
html += '</div>';
|
|
709
|
+
});
|
|
710
|
+
html += '</div>';
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
if (!outgoing.length && !incoming.length && !gotchas.length) {
|
|
714
|
+
html += '<div class="empty" style="padding:20px">No relations recorded yet.</div>';
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
body.innerHTML = html;
|
|
718
|
+
lucide.createIcons();
|
|
719
|
+
} catch(e) {
|
|
720
|
+
root.querySelector('.modal-body').innerHTML = '<div class="empty">Failed to load details: '+escapeHtml(e.message)+'</div>';
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
function closeModal() {
|
|
725
|
+
document.getElementById("modal-root").innerHTML = "";
|
|
595
726
|
}
|
|
596
727
|
|
|
728
|
+
function escapeHtml(str) {
|
|
729
|
+
if (!str) return '';
|
|
730
|
+
return String(str).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
// ============ SIDEBAR ============
|
|
734
|
+
|
|
597
735
|
async function refetch() {
|
|
598
736
|
var btn = document.getElementById('refetch-btn');
|
|
599
|
-
btn.innerHTML = '
|
|
737
|
+
btn.innerHTML = '<i data-lucide="loader" style="width:13px;height:13px;vertical-align:middle;margin-right:3px"></i> Loading...';
|
|
600
738
|
btn.style.opacity = '0.6';
|
|
739
|
+
lucide.createIcons();
|
|
601
740
|
try {
|
|
602
|
-
// Update status too
|
|
603
741
|
var status = await fetch(API+'/status').then(function(r){return r.json()});
|
|
604
|
-
var dot = document.getElementById(
|
|
605
|
-
var txt = document.getElementById(
|
|
606
|
-
if (status.ok) {
|
|
607
|
-
dot.className = 'dot on';
|
|
608
|
-
txt.textContent = status.project || 'connected';
|
|
609
|
-
} else {
|
|
610
|
-
dot.className = 'dot off';
|
|
611
|
-
txt.textContent = 'no kuma.db found';
|
|
612
|
-
}
|
|
613
|
-
// Refetch dashboard data
|
|
742
|
+
var dot = document.getElementById("status-dot");
|
|
743
|
+
var txt = document.getElementById("status-text");
|
|
744
|
+
if (status.ok) { dot.className = 'dot on'; txt.textContent = status.project || 'connected'; } else { dot.className = 'dot off'; txt.textContent = 'no kuma.db found'; }
|
|
614
745
|
var data = await fetch(API+'/dashboard').then(function(r){return r.json()});
|
|
615
746
|
if (data.error) { showError(data.error); return; }
|
|
616
|
-
renderStats(data);
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
renderHealth(data);
|
|
621
|
-
} catch(e) {
|
|
622
|
-
showError('Refetch failed: ' + e.message);
|
|
623
|
-
}
|
|
624
|
-
btn.innerHTML = '↻ Refetch';
|
|
747
|
+
renderStats(data); renderGraph(data); renderFeatures(data); renderGotchas(data); renderHealth(data);
|
|
748
|
+
lucide.createIcons();
|
|
749
|
+
} catch(e) { showError('Refetch failed: ' + e.message); }
|
|
750
|
+
btn.innerHTML = '<i data-lucide="refresh-cw" style="width:13px;height:13px;vertical-align:middle;margin-right:3px"></i> Refetch';
|
|
625
751
|
btn.style.opacity = '1';
|
|
752
|
+
lucide.createIcons();
|
|
626
753
|
}
|
|
627
754
|
|
|
628
755
|
function renderLegend(types, colors){
|
|
629
756
|
document.getElementById("legend").innerHTML = '<div class="title">Legend</div>' +
|
|
630
|
-
types.map(function(t){
|
|
631
|
-
return '<div class="legend-item"><div class="legend-dot" style="background:'+(colors[t]||'#888')+'"></div>' + t + '</div>';
|
|
632
|
-
}).join('');
|
|
757
|
+
types.map(function(t){ return '<div class="legend-item"><div class="legend-dot" style="background:'+(colors[t]||'#888')+'"></div>' + t + '</div>'; }).join('');
|
|
633
758
|
}
|
|
634
759
|
|
|
635
760
|
function setupTabs(){
|
|
636
761
|
var content = document.getElementById("tab-content");
|
|
637
762
|
content.innerHTML =
|
|
638
|
-
'<div class="panel active" id="panel-
|
|
639
|
-
'<div class="panel" id="panel-
|
|
763
|
+
'<div class="panel active" id="panel-features"><div class="empty">Loading...</div></div>' +
|
|
764
|
+
'<div class="panel" id="panel-gotchas"><div class="empty">Loading...</div></div>' +
|
|
640
765
|
'<div class="panel" id="panel-health"><div class="empty">Loading...</div></div>';
|
|
641
766
|
document.querySelectorAll('.tab').forEach(function(tab){
|
|
642
767
|
tab.onclick = function(){
|
|
@@ -650,6 +775,28 @@ function setupTabs(){
|
|
|
650
775
|
document.querySelector('.tab.active').click();
|
|
651
776
|
}
|
|
652
777
|
|
|
778
|
+
function renderFeatures(data) {
|
|
779
|
+
var items = data.features || [];
|
|
780
|
+
var el = document.getElementById("panel-features");
|
|
781
|
+
if(!el) return;
|
|
782
|
+
if(!items.length){el.innerHTML='<div class="empty">No features recorded yet.<br><br>Use:<br><code style="color:var(--accent)">kuma_memory</code></div>';return;}
|
|
783
|
+
el.innerHTML = items.map(function(f){
|
|
784
|
+
var feat = typeof f === 'string' ? JSON.parse(f) : f;
|
|
785
|
+
var meta = {};
|
|
786
|
+
try { meta = typeof feat.metadata === 'string' ? JSON.parse(feat.metadata) : (feat.metadata || {}); } catch(e) {}
|
|
787
|
+
var tags = meta.tags || [];
|
|
788
|
+
var risk = meta.risk || 'medium';
|
|
789
|
+
var riskColor = risk==='critical'?'var(--red)':risk==='high'?'var(--orange)':risk==='medium'?'var(--yellow)':'var(--green)';
|
|
790
|
+
return '<div class="feature-item" onclick="openNodeModal(\''+escapeHtml(feat.id)+'\')">' +
|
|
791
|
+
'<div class="name"><i data-lucide="star" style="width:14px;height:14px;vertical-align:middle;margin-right:4px"></i> '+escapeHtml(feat.name)+'</div>' +
|
|
792
|
+
(meta.description ? '<div class="meta">'+escapeHtml(meta.description)+'</div>' : '') +
|
|
793
|
+
'<div class="meta">Risk: <span style="color:'+riskColor+'">'+risk+'</span></div>' +
|
|
794
|
+
(tags.length ? '<div class="tags">'+tags.map(function(t){return '<span class="tag">'+escapeHtml(t)+'</span>';}).join('')+'</div>' : '') +
|
|
795
|
+
'</div>';
|
|
796
|
+
}).join('');
|
|
797
|
+
lucide.createIcons();
|
|
798
|
+
}
|
|
799
|
+
|
|
653
800
|
function renderGotchas(data){
|
|
654
801
|
var items = data.gotchas || [];
|
|
655
802
|
var el = document.getElementById("panel-gotchas");
|
|
@@ -659,24 +806,12 @@ function renderGotchas(data){
|
|
|
659
806
|
var color = g.severity==='critical'?'red':g.severity==='high'?'orange':g.severity==='medium'?'yellow':'green';
|
|
660
807
|
return '<div class="gotcha-item" style="border-color:var(--'+color+')">' +
|
|
661
808
|
'<div class="file">'+(g.file_path||'global')+'</div>' +
|
|
662
|
-
'<div class="desc">'+g.description+'</div>' +
|
|
809
|
+
'<div class="desc">'+escapeHtml(g.description)+'</div>' +
|
|
663
810
|
'<span class="sev-tag sev-'+g.severity+'">'+g.severity+'</span>' +
|
|
664
|
-
(g.workaround ? ' <span style="font-size:11px;color:var(--muted)"
|
|
811
|
+
(g.workaround ? ' <span style="font-size:11px;color:var(--muted)"><i data-lucide="lightbulb" style="width:11px;height:11px;vertical-align:middle"></i> '+escapeHtml(g.workaround)+'</span>' : '') +
|
|
665
812
|
'</div>';
|
|
666
813
|
}).join('');
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
function renderTrajectories(data){
|
|
670
|
-
var items = data.trajectories || [];
|
|
671
|
-
var el = document.getElementById("panel-trajectories");
|
|
672
|
-
if(!el) return;
|
|
673
|
-
if(!items.length){el.innerHTML='<div class="empty">No trajectories yet</div>';return;}
|
|
674
|
-
el.innerHTML = items.map(function(t){
|
|
675
|
-
var dur = t.total_duration_ms ? (t.total_duration_ms/1000).toFixed(1)+'s' : '-';
|
|
676
|
-
var rate = t.success_rate != null ? (t.success_rate*100).toFixed(0)+'%' : '-';
|
|
677
|
-
var date = t.created_at ? new Date(t.created_at*1000).toLocaleDateString() : '-';
|
|
678
|
-
return '<div class="traj-item"><div class="goal">'+t.goal+'</div><div class="meta">⏱ '+dur+' · ✔ '+rate+' · '+date+'</div></div>';
|
|
679
|
-
}).join('');
|
|
814
|
+
lucide.createIcons();
|
|
680
815
|
}
|
|
681
816
|
|
|
682
817
|
function renderHealth(data){
|
|
@@ -688,7 +823,7 @@ function renderHealth(data){
|
|
|
688
823
|
var cls = h.score>=80?'good':h.score>=50?'ok':'bad';
|
|
689
824
|
var summary = h.summary||h.risk_level||'-';
|
|
690
825
|
var date = h.created_at ? new Date(h.created_at*1000).toLocaleDateString() : '-';
|
|
691
|
-
return '<div class="health-item"><div class="score '+cls+'">'+h.score+'</div><div class="meta">'+summary+' · '+date+'</div></div>';
|
|
826
|
+
return '<div class="health-item"><div class="score '+cls+'">'+h.score+'</div><div class="meta">'+escapeHtml(summary)+' · '+date+'</div></div>';
|
|
692
827
|
}).join('');
|
|
693
828
|
}
|
|
694
829
|
|
|
@@ -696,24 +831,12 @@ function setupSearch() {
|
|
|
696
831
|
document.getElementById("node-search").oninput = function() {
|
|
697
832
|
var query = this.value.toLowerCase().trim();
|
|
698
833
|
if (!network || !allNodes) return;
|
|
699
|
-
if (!query) {
|
|
700
|
-
network.body.data.nodes.forEach(function(n){
|
|
701
|
-
network.body.data.nodes.update({id:n.id,opacity:1.0});
|
|
702
|
-
});
|
|
703
|
-
return;
|
|
704
|
-
}
|
|
834
|
+
if (!query) { network.body.data.nodes.forEach(function(n){ network.body.data.nodes.update({id:n.id,opacity:1.0}); }); return; }
|
|
705
835
|
var matched = new Set();
|
|
706
836
|
allNodes.forEach(function(n){
|
|
707
|
-
if (n.name.toLowerCase().includes(query) || (n.file_path && n.file_path.toLowerCase().includes(query)))
|
|
708
|
-
matched.add(n.id);
|
|
709
|
-
}
|
|
710
|
-
});
|
|
711
|
-
network.body.data.nodes.forEach(function(n){
|
|
712
|
-
network.body.data.nodes.update({
|
|
713
|
-
id: n.id,
|
|
714
|
-
opacity: matched.has(n.id) ? 1.0 : 0.15,
|
|
715
|
-
});
|
|
837
|
+
if (n.name.toLowerCase().includes(query) || (n.file_path && n.file_path.toLowerCase().includes(query))) matched.add(n.id);
|
|
716
838
|
});
|
|
839
|
+
network.body.data.nodes.forEach(function(n){ network.body.data.nodes.update({ id: n.id, opacity: matched.has(n.id) ? 1.0 : 0.15 }); });
|
|
717
840
|
};
|
|
718
841
|
}
|
|
719
842
|
</script>
|