@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.
@@ -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,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 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="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="gotchas">&#9888; Gotchas</div>
159
- <div class="tab" data-tab="trajectories">&#8599; Trajectories</div>
160
- <div class="tab" data-tab="health">&#9829; Health</div>
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
- const API = "/api";
168
- let network = null, nodesData = null, allNodes = null, allEdges = null, currentDbSize = "0 KB";
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("⚠️ 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
- }
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
- alert("Kuma DB reset successfully.");
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 background server? You can restart it anytime with 'agy kuma studio'.")) return;
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;">Kuma Studio Server Stopped. You can close this tab.</div>';
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 searchInput = document.getElementById("node-search");
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
- 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
- }
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">&#9888; '+msg+'</div>';
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
- if (name.length <= maxLen) return name;
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 === "route") base = 16;
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(34, Math.max(base, base + len * 0.4));
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
- route: '#db2777',
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
- route: 'triangleDown',
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 : (isVariable ? 16 : 32));
433
+ var maxLen = isGotcha ? 20 : (shape === 'ellipse' ? 18 : 32);
355
434
  var label = truncateLabel(n.name, maxLen);
356
- var size = isGotcha ? 12 : (isVariable ? 10 : calcNodeSize(n.name, n.type));
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
- label: label,
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: 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),
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
- // 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
- });
449
+ var displayEdges = edgeList.filter(function(e){ return nodeIds.has(e.source) && nodeIds.has(e.target); }).slice(0, 500);
408
450
 
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 = {};
451
+ edgeDefaults = {};
414
452
  var visEdges = new vis.DataSet(displayEdges.map(function(e, idx){
415
- var edgeColor = '#64748b';
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 === 'contains') { edgeColor = '#64748b'; width = 1.5; dashes = true; }
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
- 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' },
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
- // Click node highlight connected neighborhood (tooltip via hover only)
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
- 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
- });
535
+ if (lastFocusedNode === selectedId) { lastFocusedNode = null; resetHighlight(); return; }
536
+ lastFocusedNode = selectedId;
537
+ focusNode(selectedId);
520
538
  } 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
- });
539
+ lastFocusedNode = null;
540
+ resetHighlight();
543
541
  }
544
542
  });
545
543
 
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
- });
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
- var n = allNodes.find(function(x){return x.id===nodeId});
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>' : '') + '</div>';
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
- 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';
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 = '&#9888; Showing ' + maxNodes + ' / ' + allNodes.length + ' nodes';
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
- // Fit after stabilization
592
- network.once('stabilizationIterationsDone', function() {
593
- network.fit({ animation: { duration: 500, easingFunction: 'easeOutQuad' } });
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()">&times;</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,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
732
+ }
733
+
734
+ // ============ SIDEBAR ============
735
+
597
736
  async function refetch() {
598
737
  var btn = document.getElementById('refetch-btn');
599
- btn.innerHTML = '&#8635; Refetching...';
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('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
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
- 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';
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-gotchas"><div class="empty">Loading...</div></div>' +
639
- '<div class="panel" id="panel-trajectories"><div class="empty">Loading...</div></div>' +
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)">&#128161; '+g.workaround+'</span>' : '') +
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">&#9201; '+dur+' &middot; &#10004; '+rate+' &middot; '+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+' &middot; '+date+'</div></div>';
827
+ return '<div class="health-item"><div class="score '+cls+'">'+h.score+'</div><div class="meta">'+escapeHtml(summary)+' &middot; '+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>