@plumpslabs/kuma 2.3.22 → 2.3.23
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 +1134 -1087
- package/package.json +1 -1
- package/packages/ide/studio/dist/index.js +55 -6
- package/packages/ide/studio/public/index.html +435 -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,66 @@ 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="resetGraphFocus()" title="Reset
|
|
148
|
-
<button onclick="if(network)network.fit()" title="Fit
|
|
149
|
-
<
|
|
150
|
-
<button
|
|
151
|
-
<button
|
|
237
|
+
<button onclick="resetGraphFocus()" title="Reset focus"><i data-lucide="x" style="width:12px;height:12px"></i></button>
|
|
238
|
+
<button onclick="if(network)network.fit()" title="Fit to viewport"><i data-lucide="maximize-2" style="width:12px;height:12px"></i></button>
|
|
239
|
+
<div class="ctrl-sep"></div>
|
|
240
|
+
<button class="depth-btn" data-depth="1" onclick="setDepth(1)">1</button>
|
|
241
|
+
<button class="depth-btn active" data-depth="2" onclick="setDepth(2)">2</button>
|
|
242
|
+
<button class="depth-btn" data-depth="3" onclick="setDepth(3)">3</button>
|
|
243
|
+
<button class="depth-btn" data-depth="99" onclick="setDepth(99)">All</button>
|
|
244
|
+
<div class="ctrl-sep"></div>
|
|
245
|
+
<button id="btn-freeze-physics" onclick="stopPhysics()"><i data-lucide="pause" style="width:12px;height:12px"></i></button>
|
|
246
|
+
<button id="btn-unfreeze-physics" class="active" onclick="restartPhysics()"><i data-lucide="play" style="width:12px;height:12px"></i></button>
|
|
152
247
|
</div>
|
|
153
248
|
<div id="graph-tooltip"></div>
|
|
154
249
|
<div id="legend"></div>
|
|
155
250
|
</div>
|
|
156
251
|
<div id="sidebar">
|
|
157
252
|
<div class="tabs">
|
|
158
|
-
<div class="tab active" data-tab="
|
|
159
|
-
<div class="tab" data-tab="
|
|
160
|
-
<div class="tab" data-tab="health"
|
|
253
|
+
<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>
|
|
254
|
+
<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>
|
|
255
|
+
<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
256
|
</div>
|
|
162
257
|
<div class="tab-content" id="tab-content"></div>
|
|
163
258
|
</div>
|
|
164
259
|
</div>
|
|
260
|
+
<div id="modal-root"></div>
|
|
261
|
+
<div id="ctx-menu">
|
|
262
|
+
<div class="ctx-item" onclick="ctxViewDetail()"><i data-lucide="eye"></i> View Details</div>
|
|
263
|
+
<div class="ctx-item" onclick="ctxCopyId()"><i data-lucide="copy"></i> Copy ID</div>
|
|
264
|
+
<div class="ctx-sep"></div>
|
|
265
|
+
<div class="ctx-item" onclick="ctxZoomTo()"><i data-lucide="focus"></i> Zoom to Node</div>
|
|
266
|
+
</div>
|
|
165
267
|
|
|
166
268
|
<script>
|
|
167
|
-
|
|
168
|
-
|
|
269
|
+
var API = "/api";
|
|
270
|
+
var network = null, nodesData = null, allNodes = null, allEdges = null, currentDbSize = "0 KB";
|
|
271
|
+
var edgeDefaults = {};
|
|
169
272
|
|
|
170
273
|
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
|
-
}
|
|
274
|
+
var confirmation = prompt("⚠️ TYPE 'DELETE' TO CONFIRM RESETTING KUMA DB\nThis will permanently wipe all knowledge:");
|
|
275
|
+
if (confirmation !== "DELETE") { if (confirmation !== null) alert("Reset cancelled."); return; }
|
|
176
276
|
try {
|
|
177
277
|
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
|
-
}
|
|
278
|
+
if (res.ok) { alert("Kuma DB reset."); refetch(); }
|
|
279
|
+
} catch (e) { alert("Reset failed: " + e.message); }
|
|
185
280
|
}
|
|
186
281
|
|
|
187
282
|
async function stopKumaStudio() {
|
|
188
|
-
if (!confirm("Stop Kuma Studio
|
|
283
|
+
if (!confirm("Stop Kuma Studio?")) return;
|
|
189
284
|
try {
|
|
190
285
|
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
|
-
}
|
|
286
|
+
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>';
|
|
287
|
+
} catch (e) { alert("Stop failed: " + e.message); }
|
|
195
288
|
}
|
|
196
289
|
|
|
197
290
|
function resetGraphFocus() {
|
|
198
291
|
if (!network) return;
|
|
199
292
|
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
|
-
});
|
|
293
|
+
network.body.data.nodes.update({ id: n.id, opacity: 1.0, font: { color: '#ffffff', bold: true } });
|
|
205
294
|
});
|
|
206
295
|
document.getElementById("graph-tooltip").style.display = 'none';
|
|
207
|
-
var
|
|
208
|
-
if (searchInput) searchInput.value = "";
|
|
296
|
+
var s = document.getElementById("node-search"); if (s) s.value = "";
|
|
209
297
|
}
|
|
210
298
|
|
|
211
299
|
function exportGraphPNG() {
|
|
@@ -238,29 +326,25 @@ async function init() {
|
|
|
238
326
|
var status = await fetch(API+"/status").then(function(r){return r.json()});
|
|
239
327
|
var dot = document.getElementById("status-dot");
|
|
240
328
|
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
|
-
}
|
|
329
|
+
if (status.ok) { dot.className = "dot on"; txt.textContent = status.project || "connected"; currentDbSize = status.dbSize || "0 KB"; }
|
|
330
|
+
else { dot.className = "dot off"; txt.textContent = "no kuma.db found"; }
|
|
249
331
|
document.getElementById("loading").style.display = "none";
|
|
250
332
|
var data = await fetch(API+"/dashboard").then(function(r){return r.json()});
|
|
251
333
|
if (data.error) return showError(data.error);
|
|
252
334
|
setupTabs();
|
|
253
335
|
renderStats(data);
|
|
254
336
|
renderGraph(data);
|
|
337
|
+
renderFeatures(data);
|
|
255
338
|
renderGotchas(data);
|
|
256
|
-
renderTrajectories(data);
|
|
257
339
|
renderHealth(data);
|
|
258
340
|
setupSearch();
|
|
341
|
+
lucide.createIcons();
|
|
259
342
|
}
|
|
260
343
|
init();
|
|
261
344
|
|
|
262
345
|
function showError(msg) {
|
|
263
|
-
document.getElementById("graph-container").innerHTML = '<div class="empty"
|
|
346
|
+
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>';
|
|
347
|
+
lucide.createIcons();
|
|
264
348
|
}
|
|
265
349
|
|
|
266
350
|
function renderStats(data) {
|
|
@@ -270,25 +354,23 @@ function renderStats(data) {
|
|
|
270
354
|
document.getElementById("stats-bar").innerHTML =
|
|
271
355
|
'<div class="stat"><span class="num">'+(s.node_count||0)+'</span><span class="lbl">nodes</span></div>'+
|
|
272
356
|
'<div class="stat"><span class="num">'+(s.edge_count||0)+'</span><span class="lbl">edges</span></div>'+
|
|
357
|
+
'<div class="stat"><span class="num" style="color:var(--accent)">'+(s.feature_count||0)+'</span><span class="lbl">features</span></div>'+
|
|
273
358
|
'<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
359
|
'<div class="stat"><span class="num" style="color:var(--accent)">'+currentDbSize+'</span><span class="lbl">kuma.db</span></div>'+
|
|
276
360
|
'<div class="stat"><span class="num" style="color:'+hcColor+'">'+(hc||0)+'</span><span class="lbl">health</span></div>';
|
|
277
361
|
}
|
|
278
362
|
|
|
279
|
-
/** Truncate long labels for display */
|
|
280
363
|
function truncateLabel(name, maxLen) {
|
|
281
364
|
if (!name) return "";
|
|
282
|
-
|
|
283
|
-
return name.substring(0, maxLen-2) + "..";
|
|
365
|
+
return name.length <= maxLen ? name : name.substring(0, maxLen-2) + "..";
|
|
284
366
|
}
|
|
285
367
|
|
|
286
|
-
/** Calculate node size based on label length and type */
|
|
287
368
|
function calcNodeSize(name, type) {
|
|
288
369
|
var isGotcha = name && name.toLowerCase().indexOf("gotcha:") === 0;
|
|
289
370
|
if (isGotcha) return 14;
|
|
290
371
|
if (type === "variable") return 14;
|
|
291
372
|
var base = 18;
|
|
373
|
+
if (type === "feature") base = 32;
|
|
292
374
|
if (type === "feature_domain") base = 30;
|
|
293
375
|
if (type === "workflow") base = 24;
|
|
294
376
|
if (type === "cross_service_link") base = 20;
|
|
@@ -296,13 +378,12 @@ function calcNodeSize(name, type) {
|
|
|
296
378
|
if (type === "class") base = 20;
|
|
297
379
|
if (type === "module") base = 22;
|
|
298
380
|
if (type === "component") base = 20;
|
|
299
|
-
if (type === "
|
|
381
|
+
if (type === "api_route") base = 18;
|
|
300
382
|
if (type === "test") base = 18;
|
|
301
383
|
var len = name ? Math.min(name.length, 25) : 4;
|
|
302
|
-
return Math.min(
|
|
384
|
+
return Math.min(36, Math.max(base, base + len * 0.4));
|
|
303
385
|
}
|
|
304
386
|
|
|
305
|
-
|
|
306
387
|
function renderGraph(data) {
|
|
307
388
|
allNodes = data.nodes || [];
|
|
308
389
|
allEdges = data.edges || [];
|
|
@@ -311,8 +392,8 @@ function renderGraph(data) {
|
|
|
311
392
|
return;
|
|
312
393
|
}
|
|
313
394
|
|
|
314
|
-
var types = [...new Set(allNodes.map(function(n){return n.type||'unknown'}))];
|
|
315
395
|
var colors = {
|
|
396
|
+
feature: '#a855f7',
|
|
316
397
|
feature_domain: '#dc2626',
|
|
317
398
|
workflow: '#2563eb',
|
|
318
399
|
cross_service_link: '#059669',
|
|
@@ -321,11 +402,13 @@ function renderGraph(data) {
|
|
|
321
402
|
module: '#16a34a',
|
|
322
403
|
component: '#ea580c',
|
|
323
404
|
file: '#d97706',
|
|
324
|
-
|
|
405
|
+
api_route: '#db2777',
|
|
325
406
|
test: '#0d9488',
|
|
326
|
-
variable: '#6366f1'
|
|
407
|
+
variable: '#6366f1',
|
|
408
|
+
gotcha: '#f97316',
|
|
327
409
|
};
|
|
328
410
|
var shapes = {
|
|
411
|
+
feature: 'star',
|
|
329
412
|
feature_domain: 'star',
|
|
330
413
|
workflow: 'ellipse',
|
|
331
414
|
cross_service_link: 'box',
|
|
@@ -334,309 +417,352 @@ function renderGraph(data) {
|
|
|
334
417
|
module: 'ellipse',
|
|
335
418
|
component: 'diamond',
|
|
336
419
|
file: 'box',
|
|
337
|
-
|
|
420
|
+
api_route: 'triangleDown',
|
|
338
421
|
test: 'star',
|
|
339
|
-
variable: 'box'
|
|
422
|
+
variable: 'box',
|
|
340
423
|
};
|
|
341
424
|
|
|
342
|
-
// Limit nodes for performance if > 200
|
|
343
425
|
var maxNodes = 300;
|
|
344
426
|
var displayNodes = allNodes;
|
|
345
427
|
var truncated = allNodes.length > maxNodes;
|
|
346
|
-
if (truncated)
|
|
347
|
-
displayNodes = allNodes.slice(0, maxNodes);
|
|
348
|
-
}
|
|
428
|
+
if (truncated) displayNodes = allNodes.slice(0, maxNodes);
|
|
349
429
|
|
|
350
430
|
var visNodes = new vis.DataSet(displayNodes.map(function(n){
|
|
351
431
|
var isGotcha = n.name && n.name.toLowerCase().indexOf("gotcha:") === 0;
|
|
352
|
-
var isVariable = n.type === 'variable' && !isGotcha;
|
|
353
432
|
var shape = isGotcha ? 'triangle' : (shapes[n.type] || 'dot');
|
|
354
|
-
var maxLen = isGotcha ? 20 : (shape === 'ellipse' ? 18 :
|
|
433
|
+
var maxLen = isGotcha ? 20 : (shape === 'ellipse' ? 18 : 32);
|
|
355
434
|
var label = truncateLabel(n.name, maxLen);
|
|
356
|
-
var size = isGotcha ? 12 :
|
|
435
|
+
var size = isGotcha ? 12 : calcNodeSize(n.name, n.type);
|
|
357
436
|
var color = isGotcha ? '#f97316' : (colors[n.type] || '#64748b');
|
|
358
437
|
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
|
-
},
|
|
438
|
+
id: n.id, label: label,
|
|
439
|
+
color: { background: color, border: color, highlight: { background: color, border: '#38bdf8' }, hover: { background: color, border: '#a855f7' } },
|
|
368
440
|
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),
|
|
441
|
+
font: { color: '#ffffff', size: Math.min(13, Math.max(10, size * 0.55)), face: 'sans-serif', multi: false, bold: true },
|
|
442
|
+
borderWidth: 0, size: size, mass: 1 + (size / 40),
|
|
375
443
|
};
|
|
376
444
|
}));
|
|
377
445
|
|
|
378
446
|
var nodeIds = new Set(displayNodes.map(function(n){return n.id}));
|
|
379
447
|
var edgeList = allEdges.slice();
|
|
380
448
|
|
|
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
|
-
});
|
|
449
|
+
var displayEdges = edgeList.filter(function(e){ return nodeIds.has(e.source) && nodeIds.has(e.target); }).slice(0, 500);
|
|
408
450
|
|
|
409
|
-
|
|
410
|
-
return nodeIds.has(e.source) && nodeIds.has(e.target);
|
|
411
|
-
}).slice(0, 500);
|
|
412
|
-
|
|
413
|
-
var edgeDefaults = {};
|
|
451
|
+
edgeDefaults = {};
|
|
414
452
|
var visEdges = new vis.DataSet(displayEdges.map(function(e, idx){
|
|
415
|
-
var edgeColor = '#
|
|
453
|
+
var edgeColor = '#4a4a6a';
|
|
416
454
|
var width = 1.5;
|
|
417
455
|
var dashes = false;
|
|
418
456
|
if (e.relation === 'flows_through') { edgeColor = '#3b82f6'; width = 2.5; }
|
|
457
|
+
if (e.relation === 'affects') { edgeColor = '#ef4444'; width = 2; dashes = [5, 5]; }
|
|
419
458
|
if (e.relation === 'triggers') { edgeColor = '#ef4444'; width = 2; dashes = true; }
|
|
420
459
|
if (e.relation === 'syncs_with') { edgeColor = '#10b981'; width = 2; }
|
|
421
|
-
if (e.relation === '
|
|
460
|
+
if (e.relation === 'owns') { edgeColor = '#a855f7'; width = 2; }
|
|
461
|
+
if (e.relation === 'contains') { edgeColor = '#64748b'; width = 1; dashes = true; }
|
|
462
|
+
if (e.relation === 'depends_on') { edgeColor = '#eab308'; width = 1.5; }
|
|
422
463
|
|
|
423
464
|
edgeDefaults['e'+idx] = { color: edgeColor, width: width };
|
|
424
465
|
return {
|
|
425
|
-
id: 'e'+idx,
|
|
426
|
-
from: e.source, to: e.target,
|
|
466
|
+
id: 'e'+idx, from: e.source, to: e.target,
|
|
427
467
|
label: e.relation || '',
|
|
428
468
|
arrows: { to: { enabled: true, scaleFactor: 0.7, type: 'arrow' } },
|
|
429
469
|
color: { color: edgeColor, highlight: '#a855f7', hover: '#38bdf8', opacity: 0.85 },
|
|
430
470
|
font: { size: 10, color: '#94a3b8', strokeWidth: 0, background: 'transparent' },
|
|
431
471
|
dashes: dashes,
|
|
432
472
|
smooth: { type: 'continuous', roundness: 0.3 },
|
|
433
|
-
width: width,
|
|
434
|
-
hoverWidth: 3,
|
|
435
|
-
selectionWidth: 3,
|
|
473
|
+
width: width, hoverWidth: 3, selectionWidth: 3,
|
|
436
474
|
};
|
|
437
475
|
}));
|
|
438
476
|
|
|
439
477
|
var container = document.getElementById("graph-container");
|
|
440
478
|
network = new vis.Network(container, {nodes: visNodes, edges: visEdges}, {
|
|
441
479
|
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' },
|
|
480
|
+
enabled: true, solver: 'barnesHut',
|
|
481
|
+
barnesHut: { gravitationalConstant: -3000, centralGravity: 0.05, springLength: 150, springConstant: 0.02, damping: 0.4, avoidOverlap: 0.5 },
|
|
482
|
+
stabilization: { iterations: 120, updateInterval: 50, onlyDynamicEdges: false, fit: false },
|
|
477
483
|
},
|
|
484
|
+
interaction: { hover: true, tooltipDelay: 300, zoomView: true, dragView: true, dragNodes: true, keyboard: true, multiselect: true, selectable: true, selectConnectedEdges: false },
|
|
485
|
+
nodes: { borderWidth: 0, font: { color: '#ffffff', size: 11, face: 'sans-serif', bold: true }, margin: { top: 6, bottom: 6, left: 10, right: 10 }, shapeProperties: { borderRadius: 6 } },
|
|
486
|
+
edges: { smooth: { type: 'continuous', roundness: 0.2 }, width: 1, color: { color: '#4a4a6a', highlight: '#7c3aed' } },
|
|
478
487
|
configure: { enabled: false },
|
|
479
488
|
});
|
|
480
489
|
|
|
481
|
-
//
|
|
490
|
+
// Auto-freeze physics after stabilization (saves CPU)
|
|
491
|
+
network.once('stabilizationIterationsDone', function() {
|
|
492
|
+
network.setOptions({ physics: { enabled: false } });
|
|
493
|
+
document.getElementById('btn-freeze-physics').classList.add('active');
|
|
494
|
+
document.getElementById('btn-unfreeze-physics').classList.remove('active');
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
// Left-click node → focus only (no modal)
|
|
498
|
+
var lastFocusedNode = null;
|
|
499
|
+
var currentFocusDepth = 2;
|
|
500
|
+
window.setDepth = function(d) {
|
|
501
|
+
currentFocusDepth = d;
|
|
502
|
+
document.querySelectorAll('.depth-btn').forEach(function(b) { b.classList.toggle('active', parseInt(b.dataset.depth) === d); });
|
|
503
|
+
if (lastFocusedNode) focusNode(lastFocusedNode);
|
|
504
|
+
};
|
|
505
|
+
function focusNode(selectedId) {
|
|
506
|
+
// BFS with depth limit
|
|
507
|
+
var connected = new Map();
|
|
508
|
+
var queue = [[selectedId, 0]];
|
|
509
|
+
connected.set(selectedId, 0);
|
|
510
|
+
while (queue.length) {
|
|
511
|
+
var pair = queue.shift();
|
|
512
|
+
var cur = pair[0], depth = pair[1];
|
|
513
|
+
if (depth >= currentFocusDepth) continue;
|
|
514
|
+
network.getConnectedNodes(cur).forEach(function(nid) {
|
|
515
|
+
if (!connected.has(nid)) { connected.set(nid, depth + 1); queue.push([nid, depth + 1]); }
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
var nodeUpdates = [], edgeUpdates = [];
|
|
519
|
+
network.body.data.nodes.forEach(function(n) {
|
|
520
|
+
var hit = connected.has(n.id);
|
|
521
|
+
nodeUpdates.push({ id: n.id, opacity: hit ? 1.0 : 0.08, font: { color: hit ? '#ffffff' : 'rgba(255,255,255,0.04)', bold: hit } });
|
|
522
|
+
});
|
|
523
|
+
network.body.data.nodes.update(nodeUpdates);
|
|
524
|
+
network.body.data.edges.forEach(function(e) {
|
|
525
|
+
var hit = connected.has(e.from) && connected.has(e.to);
|
|
526
|
+
var def = edgeDefaults[e.id] || {};
|
|
527
|
+
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 });
|
|
528
|
+
});
|
|
529
|
+
network.body.data.edges.update(edgeUpdates);
|
|
530
|
+
}
|
|
482
531
|
network.on('click', function(params) {
|
|
532
|
+
hideCtxMenu();
|
|
483
533
|
if (params.nodes.length) {
|
|
484
534
|
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
|
-
});
|
|
535
|
+
if (lastFocusedNode === selectedId) { lastFocusedNode = null; resetHighlight(); return; }
|
|
536
|
+
lastFocusedNode = selectedId;
|
|
537
|
+
focusNode(selectedId);
|
|
520
538
|
} 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
|
-
});
|
|
539
|
+
lastFocusedNode = null;
|
|
540
|
+
resetHighlight();
|
|
543
541
|
}
|
|
544
542
|
});
|
|
545
543
|
|
|
546
|
-
//
|
|
547
|
-
|
|
548
|
-
var
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
544
|
+
// Right-click node → context menu (document level, capture phase)
|
|
545
|
+
document.addEventListener('contextmenu', function(e) {
|
|
546
|
+
var containerEl = document.getElementById("graph-container");
|
|
547
|
+
if (!containerEl || !containerEl.contains(e.target)) return;
|
|
548
|
+
e.preventDefault();
|
|
549
|
+
var rect = containerEl.getBoundingClientRect();
|
|
550
|
+
var x = e.clientX - rect.left;
|
|
551
|
+
var y = e.clientY - rect.top;
|
|
552
|
+
var nodeId = network.getNodeAt({ x: x, y: y });
|
|
553
|
+
if (nodeId && typeof nodeId === 'string') {
|
|
554
|
+
var menu = document.getElementById('ctx-menu');
|
|
555
|
+
menu.style.display = 'block';
|
|
556
|
+
menu.style.left = e.clientX + 'px';
|
|
557
|
+
menu.style.top = e.clientY + 'px';
|
|
558
|
+
menu.dataset.nodeId = nodeId;
|
|
559
|
+
lucide.createIcons();
|
|
560
|
+
}
|
|
561
|
+
}, true);
|
|
562
|
+
// Close context menu on mousedown outside
|
|
563
|
+
document.addEventListener('mousedown', function(e) {
|
|
564
|
+
if (!e.target.closest('#ctx-menu')) hideCtxMenu();
|
|
565
|
+
});
|
|
554
566
|
|
|
555
|
-
|
|
567
|
+
// Hover → tooltip
|
|
568
|
+
network.on('hoverNode', function(params) {
|
|
569
|
+
network.body.data.nodes.updateOnly({ id: params.node, borderWidth: 2, borderWidthSelected: 2 });
|
|
570
|
+
var n = allNodes.find(function(x){return x.id===params.node});
|
|
556
571
|
if (n) {
|
|
557
572
|
var tip = document.getElementById("graph-tooltip");
|
|
558
573
|
tip.style.display = 'block';
|
|
559
574
|
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>' : '') +
|
|
575
|
+
(n.file_path ? '<br><span style="color:#38bdf8;font-size:11px;word-break:break-all;overflow-wrap:anywhere">' + n.file_path + '</span>' : '') +
|
|
576
|
+
'<br><span style="font-size:10px;color:#64748b">Right-click for options</span></div>';
|
|
561
577
|
var pos = network.getPositions([n.id]);
|
|
562
578
|
if (pos && pos[n.id]) {
|
|
563
579
|
var canvasPos = network.canvasToDOM(pos[n.id]);
|
|
564
580
|
var panelRect = container.getBoundingClientRect();
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
tip.style.left = Math.max(tipX, 8) + 'px';
|
|
568
|
-
tip.style.top = tipY + 'px';
|
|
581
|
+
tip.style.left = Math.max(Math.min(canvasPos.x + 12, panelRect.width - 340), 8) + 'px';
|
|
582
|
+
tip.style.top = Math.max(canvasPos.y - 10, 0) + 'px';
|
|
569
583
|
}
|
|
570
584
|
}
|
|
571
585
|
});
|
|
572
586
|
network.on('blurNode', function(params) {
|
|
573
|
-
network.body.data.nodes.updateOnly({
|
|
574
|
-
id: params.node,
|
|
575
|
-
borderWidth: 0,
|
|
576
|
-
borderWidthSelected: 0,
|
|
577
|
-
});
|
|
587
|
+
network.body.data.nodes.updateOnly({ id: params.node, borderWidth: 0, borderWidthSelected: 0 });
|
|
578
588
|
document.getElementById("graph-tooltip").style.display = 'none';
|
|
579
589
|
});
|
|
580
590
|
|
|
591
|
+
var types = [...new Set(allNodes.map(function(n){return n.type||'unknown'}))];
|
|
581
592
|
renderLegend(types, colors);
|
|
582
593
|
|
|
583
|
-
// Show truncation warning
|
|
584
594
|
if (truncated) {
|
|
585
595
|
var warn = document.createElement('div');
|
|
586
596
|
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 = '
|
|
597
|
+
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
598
|
document.getElementById('graph-panel').appendChild(warn);
|
|
589
599
|
}
|
|
600
|
+
}
|
|
590
601
|
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
602
|
+
function hideCtxMenu() { document.getElementById('ctx-menu').style.display = 'none'; }
|
|
603
|
+
function resetHighlight() {
|
|
604
|
+
if (!network) return;
|
|
605
|
+
var nUp = [], eUp = [];
|
|
606
|
+
network.body.data.nodes.forEach(function(n) { nUp.push({ id: n.id, opacity: 1.0, font: { color: '#ffffff', bold: true } }); });
|
|
607
|
+
network.body.data.nodes.update(nUp);
|
|
608
|
+
network.body.data.edges.forEach(function(e) {
|
|
609
|
+
var def = edgeDefaults[e.id];
|
|
610
|
+
eUp.push({ id: e.id, color: { color: def ? def.color : '#64748b', highlight: '#a855f7', hover: '#38bdf8', opacity: 0.85 }, width: def ? def.width : 1 });
|
|
594
611
|
});
|
|
612
|
+
network.body.data.edges.update(eUp);
|
|
613
|
+
document.getElementById("graph-tooltip").style.display = 'none';
|
|
614
|
+
}
|
|
615
|
+
function ctxViewDetail() {
|
|
616
|
+
var id = document.getElementById('ctx-menu').dataset.nodeId;
|
|
617
|
+
hideCtxMenu();
|
|
618
|
+
if (id) openNodeModal(id);
|
|
619
|
+
}
|
|
620
|
+
function ctxCopyId() {
|
|
621
|
+
var id = document.getElementById('ctx-menu').dataset.nodeId;
|
|
622
|
+
hideCtxMenu();
|
|
623
|
+
if (id) navigator.clipboard.writeText(id).catch(function(){});
|
|
624
|
+
}
|
|
625
|
+
function ctxZoomTo() {
|
|
626
|
+
var id = document.getElementById('ctx-menu').dataset.nodeId;
|
|
627
|
+
hideCtxMenu();
|
|
628
|
+
if (id && network) network.focus(id, { scale: 1.5, animation: { duration: 400, easingFunction: 'easeOutQuad' } });
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
// ============ NODE DETAIL MODAL ============
|
|
632
|
+
|
|
633
|
+
async function openNodeModal(nodeId) {
|
|
634
|
+
var node = allNodes.find(function(n){return n.id === nodeId;});
|
|
635
|
+
if (!node) return;
|
|
636
|
+
|
|
637
|
+
var root = document.getElementById("modal-root");
|
|
638
|
+
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>';
|
|
639
|
+
|
|
640
|
+
try {
|
|
641
|
+
var detail = await fetch(API+"/node/"+encodeURIComponent(nodeId)).then(function(r){return r.json()});
|
|
642
|
+
var body = root.querySelector('.modal-body');
|
|
643
|
+
if (!detail || detail.error) {
|
|
644
|
+
body.innerHTML = '<div class="empty">Node not found</div>';
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
var n = typeof detail.node === 'string' ? JSON.parse(detail.node) : detail.node;
|
|
649
|
+
var outgoing = typeof detail.outgoing === 'string' ? JSON.parse(detail.outgoing) : (detail.outgoing || []);
|
|
650
|
+
var incoming = typeof detail.incoming === 'string' ? JSON.parse(detail.incoming) : (detail.incoming || []);
|
|
651
|
+
var gotchas = typeof detail.gotchas === 'string' ? JSON.parse(detail.gotchas) : (detail.gotchas || []);
|
|
652
|
+
|
|
653
|
+
var meta = {};
|
|
654
|
+
try { meta = typeof n.metadata === 'string' ? JSON.parse(n.metadata) : (n.metadata || {}); } catch(e) {}
|
|
655
|
+
|
|
656
|
+
var html = '';
|
|
657
|
+
|
|
658
|
+
// Basic info
|
|
659
|
+
html += '<div class="modal-section">';
|
|
660
|
+
html += '<h3>Info</h3>';
|
|
661
|
+
html += '<div class="modal-kv"><span class="key">Type</span><span class="val"><span style="color:var(--accent)">'+escapeHtml(n.type)+'</span></span></div>';
|
|
662
|
+
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>';
|
|
663
|
+
if (meta.description) html += '<div class="modal-kv"><span class="key">Description</span><span class="val">'+escapeHtml(meta.description)+'</span></div>';
|
|
664
|
+
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>';
|
|
665
|
+
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>';
|
|
666
|
+
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>';
|
|
667
|
+
html += '</div>';
|
|
668
|
+
|
|
669
|
+
// Outgoing relations
|
|
670
|
+
if (outgoing.length) {
|
|
671
|
+
html += '<div class="modal-section"><h3>Outgoing ('+outgoing.length+')</h3>';
|
|
672
|
+
outgoing.forEach(function(r) {
|
|
673
|
+
var rel = typeof r === 'string' ? JSON.parse(r) : r;
|
|
674
|
+
html += '<div class="relation-item" onclick="closeModal();setTimeout(function(){openNodeModal(\''+escapeHtml(rel.target)+'\')},200)">';
|
|
675
|
+
html += '<span class="arrow"><i data-lucide="arrow-right" style="width:12px;height:12px"></i></span>';
|
|
676
|
+
html += '<span class="name">'+escapeHtml(rel.target_name || rel.target.split('::').pop())+'</span>';
|
|
677
|
+
html += '<span class="type">'+escapeHtml(rel.target_type||'')+'</span>';
|
|
678
|
+
html += '<span class="weight">w:'+((rel.weight||1).toFixed(2))+'</span>';
|
|
679
|
+
html += '<span style="color:var(--accent);font-size:10px;flex-shrink:0">'+escapeHtml(rel.relation)+'</span>';
|
|
680
|
+
html += '</div>';
|
|
681
|
+
});
|
|
682
|
+
html += '</div>';
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
// Incoming relations
|
|
686
|
+
if (incoming.length) {
|
|
687
|
+
html += '<div class="modal-section"><h3>Incoming ('+incoming.length+')</h3>';
|
|
688
|
+
incoming.forEach(function(r) {
|
|
689
|
+
var rel = typeof r === 'string' ? JSON.parse(r) : r;
|
|
690
|
+
html += '<div class="relation-item" onclick="closeModal();setTimeout(function(){openNodeModal(\''+escapeHtml(rel.source)+'\')},200)">';
|
|
691
|
+
html += '<span class="arrow"><i data-lucide="arrow-left" style="width:12px;height:12px"></i></span>';
|
|
692
|
+
html += '<span class="name">'+escapeHtml(rel.source_name || rel.source.split('::').pop())+'</span>';
|
|
693
|
+
html += '<span class="type">'+escapeHtml(rel.source_type||'')+'</span>';
|
|
694
|
+
html += '<span class="weight">w:'+((rel.weight||1).toFixed(2))+'</span>';
|
|
695
|
+
html += '<span style="color:var(--accent);font-size:10px;flex-shrink:0">'+escapeHtml(rel.relation)+'</span>';
|
|
696
|
+
html += '</div>';
|
|
697
|
+
});
|
|
698
|
+
html += '</div>';
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
// Gotchas
|
|
702
|
+
if (gotchas.length) {
|
|
703
|
+
html += '<div class="modal-section"><h3>Gotchas ('+gotchas.length+')</h3>';
|
|
704
|
+
gotchas.forEach(function(g) {
|
|
705
|
+
var got = typeof g === 'string' ? JSON.parse(g) : g;
|
|
706
|
+
html += '<div class="gotcha-item" style="border-color:var(--'+(got.severity==='critical'?'red':got.severity==='high'?'orange':got.severity==='medium'?'yellow':'green')+')">';
|
|
707
|
+
html += '<div class="desc">'+escapeHtml(got.description)+'</div>';
|
|
708
|
+
html += '<span class="sev-tag sev-'+got.severity+'">'+got.severity+'</span>';
|
|
709
|
+
html += '</div>';
|
|
710
|
+
});
|
|
711
|
+
html += '</div>';
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
if (!outgoing.length && !incoming.length && !gotchas.length) {
|
|
715
|
+
html += '<div class="empty" style="padding:20px">No relations recorded yet.</div>';
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
body.innerHTML = html;
|
|
719
|
+
lucide.createIcons();
|
|
720
|
+
} catch(e) {
|
|
721
|
+
root.querySelector('.modal-body').innerHTML = '<div class="empty">Failed to load details: '+escapeHtml(e.message)+'</div>';
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
function closeModal() {
|
|
726
|
+
document.getElementById("modal-root").innerHTML = "";
|
|
595
727
|
}
|
|
596
728
|
|
|
729
|
+
function escapeHtml(str) {
|
|
730
|
+
if (!str) return '';
|
|
731
|
+
return String(str).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
// ============ SIDEBAR ============
|
|
735
|
+
|
|
597
736
|
async function refetch() {
|
|
598
737
|
var btn = document.getElementById('refetch-btn');
|
|
599
|
-
btn.innerHTML = '
|
|
738
|
+
btn.innerHTML = '<i data-lucide="loader" style="width:13px;height:13px;vertical-align:middle;margin-right:3px"></i> Loading...';
|
|
600
739
|
btn.style.opacity = '0.6';
|
|
740
|
+
lucide.createIcons();
|
|
601
741
|
try {
|
|
602
|
-
// Update status too
|
|
603
742
|
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
|
|
743
|
+
var dot = document.getElementById("status-dot");
|
|
744
|
+
var txt = document.getElementById("status-text");
|
|
745
|
+
if (status.ok) { dot.className = 'dot on'; txt.textContent = status.project || 'connected'; } else { dot.className = 'dot off'; txt.textContent = 'no kuma.db found'; }
|
|
614
746
|
var data = await fetch(API+'/dashboard').then(function(r){return r.json()});
|
|
615
747
|
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';
|
|
748
|
+
renderStats(data); renderGraph(data); renderFeatures(data); renderGotchas(data); renderHealth(data);
|
|
749
|
+
lucide.createIcons();
|
|
750
|
+
} catch(e) { showError('Refetch failed: ' + e.message); }
|
|
751
|
+
btn.innerHTML = '<i data-lucide="refresh-cw" style="width:13px;height:13px;vertical-align:middle;margin-right:3px"></i> Refetch';
|
|
625
752
|
btn.style.opacity = '1';
|
|
753
|
+
lucide.createIcons();
|
|
626
754
|
}
|
|
627
755
|
|
|
628
756
|
function renderLegend(types, colors){
|
|
629
757
|
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('');
|
|
758
|
+
types.map(function(t){ return '<div class="legend-item"><div class="legend-dot" style="background:'+(colors[t]||'#888')+'"></div>' + t + '</div>'; }).join('');
|
|
633
759
|
}
|
|
634
760
|
|
|
635
761
|
function setupTabs(){
|
|
636
762
|
var content = document.getElementById("tab-content");
|
|
637
763
|
content.innerHTML =
|
|
638
|
-
'<div class="panel active" id="panel-
|
|
639
|
-
'<div class="panel" id="panel-
|
|
764
|
+
'<div class="panel active" id="panel-features"><div class="empty">Loading...</div></div>' +
|
|
765
|
+
'<div class="panel" id="panel-gotchas"><div class="empty">Loading...</div></div>' +
|
|
640
766
|
'<div class="panel" id="panel-health"><div class="empty">Loading...</div></div>';
|
|
641
767
|
document.querySelectorAll('.tab').forEach(function(tab){
|
|
642
768
|
tab.onclick = function(){
|
|
@@ -650,6 +776,28 @@ function setupTabs(){
|
|
|
650
776
|
document.querySelector('.tab.active').click();
|
|
651
777
|
}
|
|
652
778
|
|
|
779
|
+
function renderFeatures(data) {
|
|
780
|
+
var items = data.features || [];
|
|
781
|
+
var el = document.getElementById("panel-features");
|
|
782
|
+
if(!el) return;
|
|
783
|
+
if(!items.length){el.innerHTML='<div class="empty">No features recorded yet.<br><br>Use:<br><code style="color:var(--accent)">kuma_memory({action:\'feature\', title:\'Auth\'})</code></div>';return;}
|
|
784
|
+
el.innerHTML = items.map(function(f){
|
|
785
|
+
var feat = typeof f === 'string' ? JSON.parse(f) : f;
|
|
786
|
+
var meta = {};
|
|
787
|
+
try { meta = typeof feat.metadata === 'string' ? JSON.parse(feat.metadata) : (feat.metadata || {}); } catch(e) {}
|
|
788
|
+
var tags = meta.tags || [];
|
|
789
|
+
var risk = meta.risk || 'medium';
|
|
790
|
+
var riskColor = risk==='critical'?'var(--red)':risk==='high'?'var(--orange)':risk==='medium'?'var(--yellow)':'var(--green)';
|
|
791
|
+
return '<div class="feature-item" onclick="openNodeModal(\''+escapeHtml(feat.id)+'\')">' +
|
|
792
|
+
'<div class="name"><i data-lucide="star" style="width:14px;height:14px;vertical-align:middle;margin-right:4px"></i> '+escapeHtml(feat.name)+'</div>' +
|
|
793
|
+
(meta.description ? '<div class="meta">'+escapeHtml(meta.description)+'</div>' : '') +
|
|
794
|
+
'<div class="meta">Risk: <span style="color:'+riskColor+'">'+risk+'</span></div>' +
|
|
795
|
+
(tags.length ? '<div class="tags">'+tags.map(function(t){return '<span class="tag">'+escapeHtml(t)+'</span>';}).join('')+'</div>' : '') +
|
|
796
|
+
'</div>';
|
|
797
|
+
}).join('');
|
|
798
|
+
lucide.createIcons();
|
|
799
|
+
}
|
|
800
|
+
|
|
653
801
|
function renderGotchas(data){
|
|
654
802
|
var items = data.gotchas || [];
|
|
655
803
|
var el = document.getElementById("panel-gotchas");
|
|
@@ -659,24 +807,12 @@ function renderGotchas(data){
|
|
|
659
807
|
var color = g.severity==='critical'?'red':g.severity==='high'?'orange':g.severity==='medium'?'yellow':'green';
|
|
660
808
|
return '<div class="gotcha-item" style="border-color:var(--'+color+')">' +
|
|
661
809
|
'<div class="file">'+(g.file_path||'global')+'</div>' +
|
|
662
|
-
'<div class="desc">'+g.description+'</div>' +
|
|
810
|
+
'<div class="desc">'+escapeHtml(g.description)+'</div>' +
|
|
663
811
|
'<span class="sev-tag sev-'+g.severity+'">'+g.severity+'</span>' +
|
|
664
|
-
(g.workaround ? ' <span style="font-size:11px;color:var(--muted)"
|
|
812
|
+
(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
813
|
'</div>';
|
|
666
814
|
}).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('');
|
|
815
|
+
lucide.createIcons();
|
|
680
816
|
}
|
|
681
817
|
|
|
682
818
|
function renderHealth(data){
|
|
@@ -688,7 +824,7 @@ function renderHealth(data){
|
|
|
688
824
|
var cls = h.score>=80?'good':h.score>=50?'ok':'bad';
|
|
689
825
|
var summary = h.summary||h.risk_level||'-';
|
|
690
826
|
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>';
|
|
827
|
+
return '<div class="health-item"><div class="score '+cls+'">'+h.score+'</div><div class="meta">'+escapeHtml(summary)+' · '+date+'</div></div>';
|
|
692
828
|
}).join('');
|
|
693
829
|
}
|
|
694
830
|
|
|
@@ -696,24 +832,12 @@ function setupSearch() {
|
|
|
696
832
|
document.getElementById("node-search").oninput = function() {
|
|
697
833
|
var query = this.value.toLowerCase().trim();
|
|
698
834
|
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
|
-
}
|
|
835
|
+
if (!query) { network.body.data.nodes.forEach(function(n){ network.body.data.nodes.update({id:n.id,opacity:1.0}); }); return; }
|
|
705
836
|
var matched = new Set();
|
|
706
837
|
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
|
-
});
|
|
838
|
+
if (n.name.toLowerCase().includes(query) || (n.file_path && n.file_path.toLowerCase().includes(query))) matched.add(n.id);
|
|
716
839
|
});
|
|
840
|
+
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
841
|
};
|
|
718
842
|
}
|
|
719
843
|
</script>
|