@plumpslabs/kuma 2.3.20 → 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.
Files changed (39) hide show
  1. package/README.md +18 -0
  2. package/dist/index.js +10316 -380
  3. package/package.json +2 -2
  4. package/packages/ide/studio/dist/index.js +90 -9
  5. package/packages/ide/studio/public/index.html +528 -233
  6. package/dist/agentDetector-YOWQVJFR.js +0 -186
  7. package/dist/chunk-3BRBJZ7P.js +0 -1055
  8. package/dist/chunk-3OHYYXYN.js +0 -71
  9. package/dist/chunk-ABKE45T4.js +0 -1264
  10. package/dist/chunk-E2KFPEBT.js +0 -183
  11. package/dist/chunk-FKRSI5U5.js +0 -282
  12. package/dist/chunk-GFLSAXAH.js +0 -155
  13. package/dist/chunk-L7F67KUP.js +0 -172
  14. package/dist/chunk-LVKOGXLC.js +0 -658
  15. package/dist/chunk-PRUTTZBS.js +0 -1113
  16. package/dist/contextDigest-QB5XHPXE.js +0 -177
  17. package/dist/domainRules-QLPAQASB.js +0 -20
  18. package/dist/init-PL4XL662.js +0 -15
  19. package/dist/kumaAstValidator-CNM7FHYA.js +0 -150
  20. package/dist/kumaCheckpoint-J2LDQMEO.js +0 -207
  21. package/dist/kumaCodeScanner-J6B2EHGI.js +0 -563
  22. package/dist/kumaContractEngine-KX27T4N7.js +0 -305
  23. package/dist/kumaDb-4XZ5S2LH.js +0 -65
  24. package/dist/kumaDriftDetector-TOORILSZ.js +0 -237
  25. package/dist/kumaGotchas-XRGFFBTA.js +0 -151
  26. package/dist/kumaGraph-UMXZNGYF.js +0 -44
  27. package/dist/kumaMemory-FBJMV77G.js +0 -16
  28. package/dist/kumaMiner-XJETL7TL.js +0 -176
  29. package/dist/kumaPolicyEngine-2QDJDLM7.js +0 -311
  30. package/dist/kumaProgressiveContext-MWEDRXOH.js +0 -231
  31. package/dist/kumaSearch-PV4QTKE7.js +0 -321
  32. package/dist/kumaTrajectory-7NOAVNG2.js +0 -460
  33. package/dist/kumaVerifier-6YEGC77M.js +0 -265
  34. package/dist/kumaVisualize-264OEBGJ.js +0 -264
  35. package/dist/pathValidator-V4DC6U6Z.js +0 -22
  36. package/dist/safetyAudit-O45SPNTS.js +0 -12
  37. package/dist/safetyScore-TMMRD2MV.js +0 -333
  38. package/dist/sessionMemory-YPKVIOMV.js +0 -6
  39. package/dist/skillGenerator-PWEJKZNX.js +0 -304
@@ -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,46 +56,83 @@ 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.92);
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
- backdrop-filter: blur(8px);
102
+ backdrop-filter: blur(8px); color: #f8fafc;
78
103
  }
79
- #legend .title { font-weight: 600; margin-bottom: 6px; color: var(--muted); }
80
- .legend-item { display: flex; align-items: center; gap: 6px; margin-bottom: 3px; }
104
+ #legend .title { font-weight: 600; margin-bottom: 6px; color: #94a3b8; }
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
- position: absolute; display: none; background: rgba(22,33,62,0.95);
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;
85
- font-size: 12px; z-index: 20; max-width: 320px;
86
- box-shadow: 0 8px 32px rgba(0,0,0,0.5); backdrop-filter: blur(8px);
112
+ font-size: 12px; z-index: 20; max-width: 420px; word-break: break-all;
113
+ overflow-wrap: anywhere; white-space: normal; color: #f8fafc;
114
+ box-shadow: 0 8px 32px rgba(0,0,0,0.6); backdrop-filter: blur(12px);
87
115
  pointer-events: none;
88
116
  }
89
- #search-box {
90
- 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);
91
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;
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; }
92
136
  #search-box input {
93
137
  background: rgba(22,33,62,0.92); border: 1px solid var(--border);
94
138
  border-radius: 8px; padding: 8px 12px; color: var(--text); font-size: 12px;
@@ -97,8 +141,10 @@ header .status .dot.off { background: var(--red); }
97
141
  }
98
142
  #search-box input:focus { border-color: var(--accent); }
99
143
  #search-box input::placeholder { color: var(--muted); }
144
+
145
+ /* Graph controls */
100
146
  #graph-controls {
101
- position: absolute; bottom: 16px; right: 16px; z-index: 10;
147
+ position: fixed; bottom: 16px; right: 376px; z-index: 10;
102
148
  display: flex; gap: 6px;
103
149
  }
104
150
  #graph-controls button {
@@ -108,6 +154,15 @@ header .status .dot.off { background: var(--red); }
108
154
  transition: background 0.2s;
109
155
  }
110
156
  #graph-controls button:hover { background: var(--accent); border-color: var(--accent); }
157
+ #graph-controls button.active {
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);
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 */
111
166
  #loading {
112
167
  position: fixed; inset: 0; background: var(--bg); display: flex;
113
168
  align-items: center; justify-content: center; z-index: 100;
@@ -115,6 +170,48 @@ header .status .dot.off { background: var(--red); }
115
170
  }
116
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; }
117
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
+
118
215
  ::-webkit-scrollbar { width: 6px; }
119
216
  ::-webkit-scrollbar-track { background: transparent; }
120
217
  ::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
@@ -123,9 +220,13 @@ header .status .dot.off { background: var(--red); }
123
220
  <body>
124
221
  <div id="loading"><div class="spinner"></div>Connecting to Kuma...</div>
125
222
  <header>
126
- <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>
127
224
  <div class="status"><span class="dot" id="status-dot"></span><span id="status-text">connecting...</span></div>
128
- <button onclick="refetch()" id="refetch-btn" style="margin-left:auto;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;-webkit-app-region:no-drag" onmouseover="this.style.background='rgba(124,58,237,0.3)'" onmouseout="this.style.background='rgba(124,58,237,0.15)'">&#8635; Refetch</button>
225
+ <div style="margin-left:auto;display:flex;gap:8px;-webkit-app-region:no-drag">
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>
229
+ </div>
129
230
  </header>
130
231
  <div class="stats-bar" id="stats-bar"></div>
131
232
  <div id="main">
@@ -133,37 +234,66 @@ header .status .dot.off { background: var(--red); }
133
234
  <div id="graph-container"></div>
134
235
  <div id="search-box"><input id="node-search" type="text" placeholder="Search nodes..." /></div>
135
236
  <div id="graph-controls">
136
- <button onclick="resetGraphFocus()">Reset Focus</button>
137
- <button onclick="if(network)network.fit()">Fit</button>
138
- <button onclick="exportGraphPNG()">Export PNG</button>
139
- <button onclick="if(network)network.startSimulation()">Restart</button>
140
- <button onclick="if(network)network.stopSimulation()">Stop</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>
141
247
  </div>
142
248
  <div id="graph-tooltip"></div>
143
249
  <div id="legend"></div>
144
250
  </div>
145
251
  <div id="sidebar">
146
252
  <div class="tabs">
147
- <div class="tab active" data-tab="gotchas">&#9888; Gotchas</div>
148
- <div class="tab" data-tab="trajectories">&#8599; Trajectories</div>
149
- <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>
150
256
  </div>
151
257
  <div class="tab-content" id="tab-content"></div>
152
258
  </div>
153
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>
154
267
 
155
268
  <script>
156
- const API = "/api";
157
- let network = null, nodesData = null, allNodes = null, allEdges = null;
269
+ var API = "/api";
270
+ var network = null, nodesData = null, allNodes = null, allEdges = null, currentDbSize = "0 KB";
271
+ var edgeDefaults = {};
272
+
273
+ async function resetKumaDb() {
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; }
276
+ try {
277
+ var res = await fetch(API + "/reset-db", { method: "POST" }).then(function(r){return r.json()});
278
+ if (res.ok) { alert("Kuma DB reset."); refetch(); }
279
+ } catch (e) { alert("Reset failed: " + e.message); }
280
+ }
281
+
282
+ async function stopKumaStudio() {
283
+ if (!confirm("Stop Kuma Studio?")) return;
284
+ try {
285
+ await fetch(API + "/stop-server", { method: "POST" });
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); }
288
+ }
158
289
 
159
290
  function resetGraphFocus() {
160
291
  if (!network) return;
161
292
  network.body.data.nodes.forEach(function(n) {
162
- network.body.data.nodes.update({ id: n.id, opacity: 1.0 });
293
+ network.body.data.nodes.update({ id: n.id, opacity: 1.0, font: { color: '#ffffff', bold: true } });
163
294
  });
164
295
  document.getElementById("graph-tooltip").style.display = 'none';
165
- var searchInput = document.getElementById("node-search");
166
- if (searchInput) searchInput.value = "";
296
+ var s = document.getElementById("node-search"); if (s) s.value = "";
167
297
  }
168
298
 
169
299
  function exportGraphPNG() {
@@ -176,32 +306,45 @@ function exportGraphPNG() {
176
306
  link.click();
177
307
  }
178
308
 
309
+ function restartPhysics() {
310
+ if (!network) return;
311
+ network.setOptions({ physics: { enabled: true } });
312
+ network.startSimulation();
313
+ document.getElementById('btn-unfreeze-physics').classList.add('active');
314
+ document.getElementById('btn-freeze-physics').classList.remove('active');
315
+ }
316
+
317
+ function stopPhysics() {
318
+ if (!network) return;
319
+ network.stopSimulation();
320
+ network.setOptions({ physics: { enabled: false } });
321
+ document.getElementById('btn-freeze-physics').classList.add('active');
322
+ document.getElementById('btn-unfreeze-physics').classList.remove('active');
323
+ }
324
+
179
325
  async function init() {
180
326
  var status = await fetch(API+"/status").then(function(r){return r.json()});
181
327
  var dot = document.getElementById("status-dot");
182
328
  var txt = document.getElementById("status-text");
183
- if (status.ok) {
184
- dot.className = "dot on";
185
- txt.textContent = status.project || "connected";
186
- } else {
187
- dot.className = "dot off";
188
- txt.textContent = "no kuma.db found";
189
- }
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"; }
190
331
  document.getElementById("loading").style.display = "none";
191
332
  var data = await fetch(API+"/dashboard").then(function(r){return r.json()});
192
333
  if (data.error) return showError(data.error);
193
334
  setupTabs();
194
335
  renderStats(data);
195
336
  renderGraph(data);
337
+ renderFeatures(data);
196
338
  renderGotchas(data);
197
- renderTrajectories(data);
198
339
  renderHealth(data);
199
340
  setupSearch();
341
+ lucide.createIcons();
200
342
  }
201
343
  init();
202
344
 
203
345
  function showError(msg) {
204
- 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();
205
348
  }
206
349
 
207
350
  function renderStats(data) {
@@ -211,36 +354,36 @@ function renderStats(data) {
211
354
  document.getElementById("stats-bar").innerHTML =
212
355
  '<div class="stat"><span class="num">'+(s.node_count||0)+'</span><span class="lbl">nodes</span></div>'+
213
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>'+
214
358
  '<div class="stat"><span class="num">'+(s.gotcha_count||0)+'</span><span class="lbl">gotchas</span></div>'+
215
- '<div class="stat"><span class="num">'+(s.trajectory_count||0)+'</span><span class="lbl">trajectories</span></div>'+
359
+ '<div class="stat"><span class="num" style="color:var(--accent)">'+currentDbSize+'</span><span class="lbl">kuma.db</span></div>'+
216
360
  '<div class="stat"><span class="num" style="color:'+hcColor+'">'+(hc||0)+'</span><span class="lbl">health</span></div>';
217
361
  }
218
362
 
219
- /** Truncate long labels for display */
220
363
  function truncateLabel(name, maxLen) {
221
364
  if (!name) return "";
222
- if (name.length <= maxLen) return name;
223
- return name.substring(0, maxLen-2) + "..";
365
+ return name.length <= maxLen ? name : name.substring(0, maxLen-2) + "..";
224
366
  }
225
367
 
226
- /** Calculate node size based on label length and type */
227
368
  function calcNodeSize(name, type) {
228
- var base = 14;
229
- if (type === "feature_domain") base = 24;
230
- if (type === "workflow") base = 18;
231
- if (type === "cross_service_link") base = 16;
369
+ var isGotcha = name && name.toLowerCase().indexOf("gotcha:") === 0;
370
+ if (isGotcha) return 14;
371
+ if (type === "variable") return 14;
372
+ var base = 18;
373
+ if (type === "feature") base = 32;
374
+ if (type === "feature_domain") base = 30;
375
+ if (type === "workflow") base = 24;
376
+ if (type === "cross_service_link") base = 20;
232
377
  if (type === "function") base = 16;
233
- if (type === "class") base = 18;
234
- if (type === "module") base = 12;
235
- if (type === "component") base = 16;
236
- if (type === "route") base = 14;
237
- if (type === "test") base = 13;
238
- // Gentle scaling for long labels
239
- var len = name ? Math.min(name.length, 20) : 4;
240
- return Math.min(32, Math.max(base, base + len * 0.4));
378
+ if (type === "class") base = 20;
379
+ if (type === "module") base = 22;
380
+ if (type === "component") base = 20;
381
+ if (type === "api_route") base = 18;
382
+ if (type === "test") base = 18;
383
+ var len = name ? Math.min(name.length, 25) : 4;
384
+ return Math.min(36, Math.max(base, base + len * 0.4));
241
385
  }
242
386
 
243
-
244
387
  function renderGraph(data) {
245
388
  allNodes = data.nodes || [];
246
389
  allEdges = data.edges || [];
@@ -249,223 +392,377 @@ function renderGraph(data) {
249
392
  return;
250
393
  }
251
394
 
252
- var types = [...new Set(allNodes.map(function(n){return n.type||'unknown'}))];
253
- var colors = {feature_domain:'#ef4444',workflow:'#3b82f6',cross_service_link:'#10b981',
254
- function:'#7c3aed',class:'#06b6d4',module:'#22c55e',
255
- component:'#f97316',file:'#eab308',route:'#ec4899',test:'#14b8a6'};
256
- var shapes = {feature_domain:'star',workflow:'ellipse',cross_service_link:'box',
257
- function:'box',class:'hexagon',module:'ellipse',
258
- component:'diamond',file:'square',route:'triangle',test:'star'};
259
-
260
- // Limit nodes for performance if > 200
395
+ var colors = {
396
+ feature: '#a855f7',
397
+ feature_domain: '#dc2626',
398
+ workflow: '#2563eb',
399
+ cross_service_link: '#059669',
400
+ function: '#7c3aed',
401
+ class: '#0891b2',
402
+ module: '#16a34a',
403
+ component: '#ea580c',
404
+ file: '#d97706',
405
+ api_route: '#db2777',
406
+ test: '#0d9488',
407
+ variable: '#6366f1',
408
+ gotcha: '#f97316',
409
+ };
410
+ var shapes = {
411
+ feature: 'star',
412
+ feature_domain: 'star',
413
+ workflow: 'ellipse',
414
+ cross_service_link: 'box',
415
+ function: 'box',
416
+ class: 'hexagon',
417
+ module: 'ellipse',
418
+ component: 'diamond',
419
+ file: 'box',
420
+ api_route: 'triangleDown',
421
+ test: 'star',
422
+ variable: 'box',
423
+ };
424
+
261
425
  var maxNodes = 300;
262
426
  var displayNodes = allNodes;
263
427
  var truncated = allNodes.length > maxNodes;
264
- if (truncated) {
265
- displayNodes = allNodes.slice(0, maxNodes);
266
- }
428
+ if (truncated) displayNodes = allNodes.slice(0, maxNodes);
267
429
 
268
430
  var visNodes = new vis.DataSet(displayNodes.map(function(n){
269
- var label = truncateLabel(n.name, 28);
270
- var size = calcNodeSize(n.name, n.type);
271
- var color = colors[n.type] || '#888';
431
+ var isGotcha = n.name && n.name.toLowerCase().indexOf("gotcha:") === 0;
432
+ var shape = isGotcha ? 'triangle' : (shapes[n.type] || 'dot');
433
+ var maxLen = isGotcha ? 20 : (shape === 'ellipse' ? 18 : 32);
434
+ var label = truncateLabel(n.name, maxLen);
435
+ var size = isGotcha ? 12 : calcNodeSize(n.name, n.type);
436
+ var color = isGotcha ? '#f97316' : (colors[n.type] || '#64748b');
272
437
  return {
273
- id: n.id,
274
- label: label,
275
- title: '<b>' + n.name + '</b> <span style="color:#888">' + n.type + '</span>' + (n.file_path ? '<br><span style="color:#06b6d4;font-size:11px">' + n.file_path + '</span>' : ''),
276
- color: { background: color, border: color, highlight: { background: color, border: '#fff' } },
277
- shape: shapes[n.type] || 'dot',
278
- font: { color: '#e0e0e0', size: Math.min(14, Math.max(10, size * 0.5)), face: 'monospace' },
279
- borderWidth: 0,
280
- size: size,
281
- mass: 1 + (size / 40),
438
+ id: n.id, label: label,
439
+ color: { background: color, border: color, highlight: { background: color, border: '#38bdf8' }, hover: { background: color, border: '#a855f7' } },
440
+ shape: shape,
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),
282
443
  };
283
444
  }));
284
445
 
285
446
  var nodeIds = new Set(displayNodes.map(function(n){return n.id}));
286
- var displayEdges = allEdges.filter(function(e){
287
- return nodeIds.has(e.source) && nodeIds.has(e.target);
288
- }).slice(0, 500);
447
+ var edgeList = allEdges.slice();
289
448
 
290
- var visEdges = new vis.DataSet(displayEdges.map(function(e){
291
- var edgeColor = '#64748b';
449
+ var displayEdges = edgeList.filter(function(e){ return nodeIds.has(e.source) && nodeIds.has(e.target); }).slice(0, 500);
450
+
451
+ edgeDefaults = {};
452
+ var visEdges = new vis.DataSet(displayEdges.map(function(e, idx){
453
+ var edgeColor = '#4a4a6a';
292
454
  var width = 1.5;
293
455
  var dashes = false;
294
456
  if (e.relation === 'flows_through') { edgeColor = '#3b82f6'; width = 2.5; }
457
+ if (e.relation === 'affects') { edgeColor = '#ef4444'; width = 2; dashes = [5, 5]; }
295
458
  if (e.relation === 'triggers') { edgeColor = '#ef4444'; width = 2; dashes = true; }
296
459
  if (e.relation === 'syncs_with') { edgeColor = '#10b981'; width = 2; }
297
- if (e.relation === 'contains') { edgeColor = '#475569'; width = 1; }
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; }
298
463
 
464
+ edgeDefaults['e'+idx] = { color: edgeColor, width: width };
299
465
  return {
300
- from: e.source, to: e.target,
466
+ id: 'e'+idx, from: e.source, to: e.target,
301
467
  label: e.relation || '',
302
468
  arrows: { to: { enabled: true, scaleFactor: 0.7, type: 'arrow' } },
303
469
  color: { color: edgeColor, highlight: '#a855f7', hover: '#38bdf8', opacity: 0.85 },
304
470
  font: { size: 10, color: '#94a3b8', strokeWidth: 0, background: 'transparent' },
305
471
  dashes: dashes,
306
472
  smooth: { type: 'continuous', roundness: 0.3 },
307
- width: width,
308
- hoverWidth: 3,
309
- selectionWidth: 3,
473
+ width: width, hoverWidth: 3, selectionWidth: 3,
310
474
  };
311
475
  }));
312
476
 
313
477
  var container = document.getElementById("graph-container");
314
478
  network = new vis.Network(container, {nodes: visNodes, edges: visEdges}, {
315
479
  physics: {
316
- enabled: true,
317
- solver: 'barnesHut',
318
- barnesHut: {
319
- gravitationalConstant: -2000,
320
- centralGravity: 0.3,
321
- springLength: 150,
322
- springConstant: 0.04,
323
- damping: 0.6,
324
- avoidOverlap: 0.5,
325
- },
326
- stabilization: { iterations: 400, updateInterval: 25, onlyDynamicEdges: false },
327
- adaptiveTimestep: true,
328
- },
329
- interaction: {
330
- hover: true,
331
- tooltipDelay: 300,
332
- zoomView: true,
333
- dragView: true,
334
- dragNodes: true,
335
- keyboard: true,
336
- multiselect: true,
337
- selectable: true,
338
- selectConnectedEdges: false,
339
- },
340
- nodes: {
341
- borderWidth: 0,
342
- font: { color: '#e0e0e0', size: 12, face: 'monospace' },
343
- margin: { top: 4, bottom: 4, left: 6, right: 6 },
344
- shadow: { enabled: true, color: 'rgba(124,58,237,0.15)', size: 6, x: 0, y: 2 },
345
- },
346
- edges: {
347
- smooth: { type: 'continuous', roundness: 0.3 },
348
- width: 1.5,
349
- 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 },
350
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' } },
351
487
  configure: { enabled: false },
352
488
  });
353
489
 
354
- // Click node show tooltip with file path + highlight connected neighborhood
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
+ }
355
531
  network.on('click', function(params) {
356
- var tip = document.getElementById("graph-tooltip");
532
+ hideCtxMenu();
357
533
  if (params.nodes.length) {
358
534
  var selectedId = params.nodes[0];
359
- var connectedNodeIds = new Set(network.getConnectedNodes(selectedId));
360
- connectedNodeIds.add(selectedId);
361
-
362
- network.body.data.nodes.forEach(function(n) {
363
- network.body.data.nodes.update({
364
- id: n.id,
365
- opacity: connectedNodeIds.has(n.id) ? 1.0 : 0.15
366
- });
367
- });
368
-
369
- var n = allNodes.find(function(x){return x.id===selectedId});
370
- if (n) {
371
- tip.style.display = 'block';
372
- tip.innerHTML = '<b>' + n.name + '</b> <span style="color:var(--muted)">' + n.type + '</span>' +
373
- (n.file_path ? '<br><span style="color:var(--accent2);font-size:11px;word-break:break-all">' + n.file_path + '</span>' : '');
374
- // Position tooltip near the clicked node
375
- var pos = network.getPositions([n.id]);
376
- if (pos && pos[n.id]) {
377
- var canvasPos = network.canvasToDOM(pos[n.id]);
378
- var panelRect = container.getBoundingClientRect();
379
- var tipX = Math.min(canvasPos.x + 12, panelRect.width - 340);
380
- var tipY = Math.max(canvasPos.y - 10, 0);
381
- tip.style.left = Math.max(tipX, 8) + 'px';
382
- tip.style.top = tipY + 'px';
383
- }
384
- }
535
+ if (lastFocusedNode === selectedId) { lastFocusedNode = null; resetHighlight(); return; }
536
+ lastFocusedNode = selectedId;
537
+ focusNode(selectedId);
385
538
  } else {
386
- tip.style.display = 'none';
387
- network.body.data.nodes.forEach(function(n) {
388
- network.body.data.nodes.update({ id: n.id, opacity: 1.0 });
389
- });
539
+ lastFocusedNode = null;
540
+ resetHighlight();
390
541
  }
391
542
  });
392
543
 
393
- // Hover highlight
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
+ });
566
+
567
+ // Hover → tooltip
394
568
  network.on('hoverNode', function(params) {
395
- var nodeId = params.node;
396
- network.body.data.nodes.updateOnly({
397
- id: nodeId,
398
- borderWidth: 2,
399
- borderWidthSelected: 2,
400
- });
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});
571
+ if (n) {
572
+ var tip = document.getElementById("graph-tooltip");
573
+ tip.style.display = 'block';
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>' +
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>';
577
+ var pos = network.getPositions([n.id]);
578
+ if (pos && pos[n.id]) {
579
+ var canvasPos = network.canvasToDOM(pos[n.id]);
580
+ var panelRect = container.getBoundingClientRect();
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';
583
+ }
584
+ }
401
585
  });
402
586
  network.on('blurNode', function(params) {
403
- network.body.data.nodes.updateOnly({
404
- id: params.node,
405
- borderWidth: 0,
406
- borderWidthSelected: 0,
407
- });
587
+ network.body.data.nodes.updateOnly({ id: params.node, borderWidth: 0, borderWidthSelected: 0 });
588
+ document.getElementById("graph-tooltip").style.display = 'none';
408
589
  });
409
590
 
591
+ var types = [...new Set(allNodes.map(function(n){return n.type||'unknown'}))];
410
592
  renderLegend(types, colors);
411
593
 
412
- // Show truncation warning
413
594
  if (truncated) {
414
595
  var warn = document.createElement('div');
415
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';
416
- 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';
417
598
  document.getElementById('graph-panel').appendChild(warn);
418
599
  }
600
+ }
419
601
 
420
- // Fit after stabilization
421
- network.once('stabilizationIterationsDone', function() {
422
- 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 });
423
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);
424
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 = "";
727
+ }
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 ============
425
735
 
426
736
  async function refetch() {
427
737
  var btn = document.getElementById('refetch-btn');
428
- btn.innerHTML = '&#8635; Refetching...';
738
+ btn.innerHTML = '<i data-lucide="loader" style="width:13px;height:13px;vertical-align:middle;margin-right:3px"></i> Loading...';
429
739
  btn.style.opacity = '0.6';
740
+ lucide.createIcons();
430
741
  try {
431
- // Update status too
432
742
  var status = await fetch(API+'/status').then(function(r){return r.json()});
433
- var dot = document.getElementById('status-dot');
434
- var txt = document.getElementById('status-text');
435
- if (status.ok) {
436
- dot.className = 'dot on';
437
- txt.textContent = status.project || 'connected';
438
- } else {
439
- dot.className = 'dot off';
440
- txt.textContent = 'no kuma.db found';
441
- }
442
- // 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'; }
443
746
  var data = await fetch(API+'/dashboard').then(function(r){return r.json()});
444
747
  if (data.error) { showError(data.error); return; }
445
- renderStats(data);
446
- renderGraph(data);
447
- renderGotchas(data);
448
- renderTrajectories(data);
449
- renderHealth(data);
450
- } catch(e) {
451
- showError('Refetch failed: ' + e.message);
452
- }
453
- 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';
454
752
  btn.style.opacity = '1';
753
+ lucide.createIcons();
455
754
  }
456
755
 
457
756
  function renderLegend(types, colors){
458
757
  document.getElementById("legend").innerHTML = '<div class="title">Legend</div>' +
459
- types.map(function(t){
460
- return '<div class="legend-item"><div class="legend-dot" style="background:'+(colors[t]||'#888')+'"></div>' + t + '</div>';
461
- }).join('');
758
+ types.map(function(t){ return '<div class="legend-item"><div class="legend-dot" style="background:'+(colors[t]||'#888')+'"></div>' + t + '</div>'; }).join('');
462
759
  }
463
760
 
464
761
  function setupTabs(){
465
762
  var content = document.getElementById("tab-content");
466
763
  content.innerHTML =
467
- '<div class="panel active" id="panel-gotchas"><div class="empty">Loading...</div></div>' +
468
- '<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>' +
469
766
  '<div class="panel" id="panel-health"><div class="empty">Loading...</div></div>';
470
767
  document.querySelectorAll('.tab').forEach(function(tab){
471
768
  tab.onclick = function(){
@@ -479,6 +776,28 @@ function setupTabs(){
479
776
  document.querySelector('.tab.active').click();
480
777
  }
481
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
+
482
801
  function renderGotchas(data){
483
802
  var items = data.gotchas || [];
484
803
  var el = document.getElementById("panel-gotchas");
@@ -488,24 +807,12 @@ function renderGotchas(data){
488
807
  var color = g.severity==='critical'?'red':g.severity==='high'?'orange':g.severity==='medium'?'yellow':'green';
489
808
  return '<div class="gotcha-item" style="border-color:var(--'+color+')">' +
490
809
  '<div class="file">'+(g.file_path||'global')+'</div>' +
491
- '<div class="desc">'+g.description+'</div>' +
810
+ '<div class="desc">'+escapeHtml(g.description)+'</div>' +
492
811
  '<span class="sev-tag sev-'+g.severity+'">'+g.severity+'</span>' +
493
- (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>' : '') +
494
813
  '</div>';
495
814
  }).join('');
496
- }
497
-
498
- function renderTrajectories(data){
499
- var items = data.trajectories || [];
500
- var el = document.getElementById("panel-trajectories");
501
- if(!el) return;
502
- if(!items.length){el.innerHTML='<div class="empty">No trajectories yet</div>';return;}
503
- el.innerHTML = items.map(function(t){
504
- var dur = t.total_duration_ms ? (t.total_duration_ms/1000).toFixed(1)+'s' : '-';
505
- var rate = t.success_rate != null ? (t.success_rate*100).toFixed(0)+'%' : '-';
506
- var date = t.created_at ? new Date(t.created_at*1000).toLocaleDateString() : '-';
507
- return '<div class="traj-item"><div class="goal">'+t.goal+'</div><div class="meta">&#9201; '+dur+' &middot; &#10004; '+rate+' &middot; '+date+'</div></div>';
508
- }).join('');
815
+ lucide.createIcons();
509
816
  }
510
817
 
511
818
  function renderHealth(data){
@@ -517,7 +824,7 @@ function renderHealth(data){
517
824
  var cls = h.score>=80?'good':h.score>=50?'ok':'bad';
518
825
  var summary = h.summary||h.risk_level||'-';
519
826
  var date = h.created_at ? new Date(h.created_at*1000).toLocaleDateString() : '-';
520
- 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>';
521
828
  }).join('');
522
829
  }
523
830
 
@@ -525,24 +832,12 @@ function setupSearch() {
525
832
  document.getElementById("node-search").oninput = function() {
526
833
  var query = this.value.toLowerCase().trim();
527
834
  if (!network || !allNodes) return;
528
- if (!query) {
529
- network.body.data.nodes.forEach(function(n){
530
- network.body.data.nodes.update({id:n.id,opacity:1.0});
531
- });
532
- return;
533
- }
835
+ if (!query) { network.body.data.nodes.forEach(function(n){ network.body.data.nodes.update({id:n.id,opacity:1.0}); }); return; }
534
836
  var matched = new Set();
535
837
  allNodes.forEach(function(n){
536
- if (n.name.toLowerCase().includes(query) || (n.file_path && n.file_path.toLowerCase().includes(query))) {
537
- matched.add(n.id);
538
- }
539
- });
540
- network.body.data.nodes.forEach(function(n){
541
- network.body.data.nodes.update({
542
- id: n.id,
543
- opacity: matched.has(n.id) ? 1.0 : 0.15,
544
- });
838
+ if (n.name.toLowerCase().includes(query) || (n.file_path && n.file_path.toLowerCase().includes(query))) matched.add(n.id);
545
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 }); });
546
841
  };
547
842
  }
548
843
  </script>