@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.
@@ -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: 340px; min-width: 340px; background: var(--surface); border-left: 1px solid var(--border); display: flex; flex-direction: column; }
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: 13px; margin-bottom: 4px; }
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
- .traj-item { padding: 8px 12px; margin-bottom: 6px; background: var(--bg); border-radius: 6px; }
64
- .traj-item .goal { font-size: 13px; font-weight: 600; word-break: break-all; }
65
- .traj-item .meta { font-size: 11px; color: var(--muted); margin-top: 2px; }
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: absolute; bottom: 16px; left: 16px; background: rgba(22,33,62,0.95);
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
- #search-box {
91
- position: absolute; top: 16px; right: 16px; z-index: 10;
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: absolute; bottom: 16px; right: 16px; z-index: 10;
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
- border-color: var(--accent) !important;
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>&#9826; Kuma Studio</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">&#8635; Refetch</button>
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">&#128465; Reset DB</button>
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">&#9632; Stop Server</button>
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="resetGraphFocus()" title="Reset node opacity and search">Reset Focus</button>
148
- <button onclick="if(network)network.fit()" title="Fit graph to viewport">Fit</button>
149
- <button onclick="exportGraphPNG()" title="Export graph as PNG image">Export PNG</button>
150
- <button id="btn-unfreeze-physics" class="active" onclick="restartPhysics()" title="Enable physics simulation (nodes will move freely)">Unfreeze Physics</button>
151
- <button id="btn-freeze-physics" onclick="stopPhysics()" title="Freeze physics simulation (lock nodes in place)">Freeze Physics</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="gotchas">&#9888; Gotchas</div>
159
- <div class="tab" data-tab="trajectories">&#8599; Trajectories</div>
160
- <div class="tab" data-tab="health">&#9829; Health</div>
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
- const API = "/api";
168
- let network = null, nodesData = null, allNodes = null, allEdges = null, currentDbSize = "0 KB";
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("⚠️ CRITICAL ACTION: TYPE 'DELETE' TO CONFIRM RESETTING KUMA DB\nThis will permanently wipe all recorded knowledge nodes, edges, gotchas, and trajectories in .kuma/kuma.db:");
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
- alert("Kuma DB reset successfully.");
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 background server? You can restart it anytime with 'agy kuma studio'.")) return;
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;">Kuma Studio Server Stopped. You can close this tab.</div>';
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 searchInput = document.getElementById("node-search");
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
- dot.className = "dot on";
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">&#9888; '+msg+'</div>';
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
- if (name.length <= maxLen) return name;
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 === "route") base = 16;
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(34, Math.max(base, base + len * 0.4));
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
- route: '#db2777',
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
- route: 'triangleDown',
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 : (isVariable ? 16 : 32));
432
+ var maxLen = isGotcha ? 20 : (shape === 'ellipse' ? 18 : 32);
355
433
  var label = truncateLabel(n.name, maxLen);
356
- var size = isGotcha ? 12 : (isVariable ? 10 : calcNodeSize(n.name, n.type));
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
- label: label,
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: isVariable
370
- ? { color: '#ffffff', size: 10, face: 'sans-serif', multi: false, bold: true }
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
- // Auto-connect floating file nodes to matching cross_service_link flow nodes
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
- var displayEdges = edgeList.filter(function(e){
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 = '#64748b';
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 === 'contains') { edgeColor = '#64748b'; width = 1.5; dashes = true; }
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
- solver: 'barnesHut',
444
- barnesHut: {
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
- // Click node highlight connected neighborhood (tooltip via hover only)
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
- var connectedNodeIds = new Set(network.getConnectedNodes(selectedId));
486
- connectedNodeIds.add(selectedId);
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
- document.getElementById("graph-tooltip").style.display = 'none';
522
- network.body.data.nodes.forEach(function(n) {
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
- // Hover node → show tooltip + border highlight
547
- network.on('hoverNode', function(params) {
548
- var nodeId = params.node;
549
- network.body.data.nodes.updateOnly({
550
- id: nodeId,
551
- borderWidth: 2,
552
- borderWidthSelected: 2,
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
- var n = allNodes.find(function(x){return x.id===nodeId});
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>' : '') + '</div>';
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
- var tipX = Math.min(canvasPos.x + 12, panelRect.width - 340);
566
- var tipY = Math.max(canvasPos.y - 10, 0);
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 = '&#9888; Showing ' + maxNodes + ' / ' + allNodes.length + ' nodes';
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
- // Fit after stabilization
592
- network.once('stabilizationIterationsDone', function() {
593
- network.fit({ animation: { duration: 500, easingFunction: 'easeOutQuad' } });
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()">&times;</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,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
731
+ }
732
+
733
+ // ============ SIDEBAR ============
734
+
597
735
  async function refetch() {
598
736
  var btn = document.getElementById('refetch-btn');
599
- btn.innerHTML = '&#8635; Refetching...';
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('status-dot');
605
- var txt = document.getElementById('status-text');
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
- renderGraph(data);
618
- renderGotchas(data);
619
- renderTrajectories(data);
620
- renderHealth(data);
621
- } catch(e) {
622
- showError('Refetch failed: ' + e.message);
623
- }
624
- btn.innerHTML = '&#8635; 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-gotchas"><div class="empty">Loading...</div></div>' +
639
- '<div class="panel" id="panel-trajectories"><div class="empty">Loading...</div></div>' +
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)">&#128161; '+g.workaround+'</span>' : '') +
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">&#9201; '+dur+' &middot; &#10004; '+rate+' &middot; '+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+' &middot; '+date+'</div></div>';
826
+ return '<div class="health-item"><div class="score '+cls+'">'+h.score+'</div><div class="meta">'+escapeHtml(summary)+' &middot; '+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>