@plumpslabs/kuma 2.4.3 → 2.4.4

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.
@@ -3,1342 +3,977 @@
3
3
  <head>
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>Kuma Studio</title>
6
+ <title>Kuma Studio — Context & Safety Dashboard</title>
7
7
  <link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><circle cx='50' cy='50' r='48' fill='%23D97706'/><circle cx='50' cy='50' r='44' fill='%23F59E0B'/><circle cx='50' cy='45' r='30' fill='%2392400E'/><circle cx='35' cy='38' r='5' fill='%231F2937'/><circle cx='65' cy='38' r='5' fill='%231F2937'/><ellipse cx='50' cy='50' rx='10' ry='6' fill='%23B45309'/><path d='M35 58 Q50 72 65 58' stroke='%231F2937' stroke-width='3.5' fill='none' stroke-linecap='round'/></svg>">
8
8
  <script src="https://unpkg.com/vis-network@9.1.9/dist/vis-network.min.js"></script>
9
9
  <script src="https://unpkg.com/lucide@0.344.0/dist/umd/lucide.min.js"></script>
10
10
  <style>
11
11
  :root {
12
- --bg: #0f172a; --surface: #1e293b; --border: #334155;
13
- --text: #f1f5f9; --muted: #94a3b8; --accent: #a78bfa; --accent2: #22d3ee;
14
- --green: #34d399; --yellow: #fbbf24; --orange: #fb923c; --red: #f87171;
15
- --accent-bright: #c4b5fd; --accent2-bright: #67e8f9;
12
+ --bg: #0b0f19;
13
+ --surface: #111827;
14
+ --surface-hover: #1f293d;
15
+ --card-bg: #151e32;
16
+ --border: #243049;
17
+ --text: #f8fafc;
18
+ --muted: #94a3b8;
19
+ --amber: #f59e0b;
20
+ --amber-dark: #d97706;
21
+ --amber-light: #fde68a;
22
+ --emerald: #10b981;
23
+ --cyan: #06b6d4;
24
+ --indigo: #6366f1;
25
+ --rose: #f43f5e;
26
+ --font: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
27
+ --mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
16
28
  }
17
29
  * { box-sizing: border-box; margin: 0; padding: 0; }
18
- html, body {
19
- font-family: -apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;
20
- background: var(--bg); color: var(--text); height: 100vh; overflow: hidden;
21
- touch-action: none; -webkit-user-select: none; user-select: none;
30
+ body {
31
+ font-family: var(--font);
32
+ background: var(--bg);
33
+ color: var(--text);
34
+ height: 100vh;
35
+ display: flex;
36
+ flex-direction: column;
37
+ overflow: hidden;
22
38
  }
23
- header {
24
- display: flex; align-items: center; gap: 12px; padding: 8px 16px;
25
- background: var(--surface); border-bottom: 1px solid var(--border);
26
- -webkit-app-region: drag;
27
- }
28
- header h1 { font-size: 15px; font-weight: 700; color: var(--accent-bright); letter-spacing: -0.5px; text-shadow: 0 0 12px rgba(167,139,250,0.3); }
29
- header .status { font-size: 12px; color: var(--muted); }
30
- header .status .dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin-right: 6px; }
31
- header .status .dot.on { background: var(--green); }
32
- header .status .dot.off { background: var(--red); }
33
- .stats-bar {
34
- display: flex; gap: 6px; padding: 6px 16px; background: var(--surface);
35
- border-bottom: 1px solid var(--border); font-size: 11px;
36
- overflow-x: auto; flex-wrap: nowrap;
37
- }
38
- .stat { display: flex; align-items: center; gap: 3px; padding: 3px 8px; border-radius: 5px; background: var(--bg); white-space: nowrap; }
39
- .stat .num { font-weight: 700; color: var(--accent2); font-size: 12px; }
40
- .stat .lbl { color: var(--muted); font-size: 10px; }
41
- #main { display: flex; height: calc(100vh - 80px); }
42
- #graph-panel { flex: 1; position: relative; min-width: 0; overflow: hidden; }
43
- #graph-container { width: 100%; height: 100%; background: radial-gradient(ellipse at center, #1e1e3a 0%, var(--bg) 70%); }
44
- #sidebar { width: 320px; min-width: 320px; background: var(--surface); border-left: 1px solid var(--border); display: flex; flex-direction: column; }
45
- .tabs { display: flex; gap: 3px; padding: 8px 12px 4px; background: var(--surface); border-bottom: 1px solid var(--border); overflow-x: auto; flex-wrap: nowrap; }
46
- .tab {
47
- padding: 5px 10px; text-align: center; cursor: pointer; font-size: 11px;
48
- color: var(--muted); border-radius: 6px; transition: all 0.15s; white-space: nowrap;
49
- display: flex; align-items: center; gap: 4px; border: 1px solid transparent;
50
- }
51
- .tab:hover { color: var(--text); background: rgba(167,139,250,0.08); }
52
- .tab.active { color: var(--accent-bright); background: rgba(167,139,250,0.12); border-color: rgba(167,139,250,0.25); }
53
- .tab-content { flex: 1; overflow-y: auto; padding: 10px 12px; }
54
- .panel { display: none; }
55
- .panel.active { display: block; animation: fadeIn 0.15s; }
56
- .tab-label { font-size: 11px; }
57
- @media (max-width: 400px) { .tab-label { display: none; } .tab { padding: 5px 8px; } }
58
-
59
- /* Gotcha items */
60
- .gotcha-item {
61
- padding: 8px 10px; margin-bottom: 6px; border-radius: 6px;
62
- background: var(--bg); border-left: 3px solid var(--muted); cursor: pointer;
63
- transition: background 0.15s;
64
- }
65
- .gotcha-item:hover { background: #1e293b; }
66
- .gotcha-item .file { font-size: 11px; color: var(--accent2); margin-bottom: 4px; word-break: break-all; }
67
- .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; }
68
- .gotcha-item .sev-tag { display: inline-block; font-size: 10px; padding: 2px 6px; border-radius: 4px; font-weight: 600; }
69
- .sev-critical { background: #ef444420; color: var(--red); border-color: var(--red); }
70
- .sev-high { background: #f9731620; color: var(--orange); border-color: var(--orange); }
71
- .sev-medium { background: #eab30820; color: var(--yellow); border-color: var(--yellow); }
72
- .sev-low { background: #22c55e20; color: var(--green); border-color: var(--green); }
73
-
74
- /* Feature items */
75
- .feature-item {
76
- padding: 8px 10px; margin-bottom: 6px; border-radius: 6px;
77
- background: var(--bg); border-left: 3px solid var(--accent); cursor: pointer;
78
- transition: background 0.15s;
79
- }
80
- .feature-item:hover { background: #1e293b; }
81
- .feature-item .name { font-size: 13px; font-weight: 600; color: var(--accent); margin-bottom: 2px; }
82
- .feature-item .meta { font-size: 11px; color: var(--muted); }
83
- .feature-item .tags { display: flex; gap: 4px; flex-wrap: wrap; margin-top: 4px; }
84
- .feature-item .tag { font-size: 10px; padding: 1px 6px; border-radius: 4px; background: rgba(167,139,250,0.15); color: var(--accent-bright); }
85
-
86
- /* Health items */
87
- .health-item { padding: 6px 10px; margin-bottom: 5px; background: var(--bg); border-radius: 5px; }
88
- .health-item .score { font-size: 18px; font-weight: 700; }
89
- .health-item .score.good { color: var(--green); }
90
- .health-item .score.ok { color: var(--yellow); }
91
- .health-item .score.bad { color: var(--red); }
92
- .health-item .meta { font-size: 11px; color: var(--muted); }
93
39
 
94
- .empty { text-align: center; color: var(--muted); padding: 40px 20px; font-size: 14px; }
95
-
96
- /* Legend */
97
- #legend {
98
- position: fixed; bottom: 16px; left: 16px; background: rgba(15,23,42,0.95);
99
- border: 1px solid var(--border); border-radius: 8px; padding: 10px 14px;
100
- font-size: 11px; z-index: 10; max-height: 200px; overflow-y: auto;
101
- backdrop-filter: blur(8px); color: #f8fafc;
102
- }
103
- #legend .title { font-weight: 600; margin-bottom: 6px; color: #94a3b8; }
104
- .legend-item { display: flex; align-items: center; gap: 6px; margin-bottom: 3px; color: #f1f5f9; }
105
- .legend-dot { width: 10px; height: 10px; border-radius: 50%; }
106
-
107
- /* Tooltip */
108
- #graph-tooltip {
109
- position: absolute; display: none; background: rgba(15, 23, 42, 0.96);
110
- border: 1px solid var(--border); border-radius: 8px; padding: 10px 14px;
111
- font-size: 12px; z-index: 20; max-width: 420px; word-break: break-all;
112
- overflow-wrap: anywhere; white-space: normal; color: #f8fafc;
113
- box-shadow: 0 8px 32px rgba(0,0,0,0.6); backdrop-filter: blur(12px);
114
- pointer-events: none;
40
+ header {
41
+ background: var(--surface);
42
+ border-bottom: 1px solid var(--border);
43
+ padding: 10px 20px;
44
+ display: flex;
45
+ align-items: center;
46
+ justify-content: space-between;
47
+ gap: 16px;
48
+ z-index: 50;
49
+ flex-shrink: 0;
50
+ }
51
+ .brand-group {
52
+ display: flex;
53
+ align-items: center;
54
+ gap: 12px;
55
+ }
56
+ .brand-title {
57
+ font-size: 16px;
58
+ font-weight: 700;
59
+ color: var(--amber);
60
+ display: flex;
61
+ align-items: center;
62
+ gap: 8px;
63
+ letter-spacing: -0.02em;
64
+ }
65
+ .badge-project {
66
+ font-size: 11px;
67
+ font-family: var(--mono);
68
+ background: rgba(245,158,11,0.12);
69
+ color: var(--amber-light);
70
+ border: 1px solid rgba(245,158,11,0.25);
71
+ padding: 2px 8px;
72
+ border-radius: 6px;
73
+ }
74
+ .db-status {
75
+ font-size: 12px;
76
+ color: var(--muted);
77
+ display: flex;
78
+ align-items: center;
79
+ gap: 6px;
80
+ }
81
+ .dot-status {
82
+ width: 8px;
83
+ height: 8px;
84
+ border-radius: 50%;
85
+ background: var(--emerald);
86
+ box-shadow: 0 0 8px rgba(16,185,129,0.5);
87
+ }
88
+
89
+ .nav-tabs {
90
+ display: flex;
91
+ background: var(--bg);
92
+ border: 1px solid var(--border);
93
+ padding: 3px;
94
+ border-radius: 8px;
95
+ gap: 2px;
96
+ }
97
+ .nav-tab {
98
+ display: flex;
99
+ align-items: center;
100
+ gap: 6px;
101
+ padding: 6px 14px;
102
+ font-size: 12px;
103
+ font-weight: 600;
104
+ color: var(--muted);
105
+ border-radius: 6px;
106
+ cursor: pointer;
107
+ transition: all 0.15s ease;
108
+ border: none;
109
+ background: transparent;
110
+ }
111
+ .nav-tab:hover {
112
+ color: var(--text);
113
+ background: var(--surface-hover);
114
+ }
115
+ .nav-tab.active {
116
+ color: #fff;
117
+ background: var(--amber-dark);
118
+ box-shadow: 0 1px 4px rgba(217,119,6,0.4);
119
+ }
120
+ .nav-tab .tab-badge {
121
+ font-size: 10px;
122
+ background: rgba(255,255,255,0.2);
123
+ padding: 1px 6px;
124
+ border-radius: 10px;
125
+ font-weight: 700;
126
+ }
127
+
128
+ .action-group {
129
+ display: flex;
130
+ align-items: center;
131
+ gap: 8px;
132
+ }
133
+ .btn-action {
134
+ background: var(--surface-hover);
135
+ border: 1px solid var(--border);
136
+ color: var(--text);
137
+ padding: 6px 12px;
138
+ border-radius: 6px;
139
+ font-size: 12px;
140
+ font-weight: 500;
141
+ cursor: pointer;
142
+ display: flex;
143
+ align-items: center;
144
+ gap: 5px;
145
+ transition: all 0.15s;
146
+ }
147
+ .btn-action:hover {
148
+ border-color: var(--amber);
149
+ color: var(--amber-light);
150
+ }
151
+
152
+ #viewport {
153
+ flex: 1;
154
+ position: relative;
155
+ overflow: hidden;
156
+ }
157
+ .view-panel {
158
+ display: none;
159
+ width: 100%;
160
+ height: 100%;
161
+ overflow-y: auto;
162
+ padding: 24px;
163
+ }
164
+ .view-panel.active {
165
+ display: block;
166
+ }
167
+
168
+ .view-header {
169
+ margin-bottom: 20px;
170
+ display: flex;
171
+ justify-content: space-between;
172
+ align-items: center;
173
+ flex-wrap: wrap;
174
+ gap: 16px;
175
+ }
176
+ .view-title {
177
+ font-size: 18px;
178
+ font-weight: 700;
179
+ color: #fff;
180
+ display: flex;
181
+ align-items: center;
182
+ gap: 8px;
183
+ }
184
+ .view-desc {
185
+ font-size: 12px;
186
+ color: var(--muted);
187
+ margin-top: 4px;
188
+ }
189
+ .search-input {
190
+ background: var(--surface);
191
+ border: 1px solid var(--border);
192
+ padding: 8px 14px;
193
+ border-radius: 8px;
194
+ color: #fff;
195
+ font-size: 13px;
196
+ outline: none;
197
+ min-width: 280px;
198
+ transition: border-color 0.15s;
199
+ }
200
+ .search-input:focus {
201
+ border-color: var(--amber);
202
+ box-shadow: 0 0 0 2px rgba(245,158,11,0.2);
203
+ }
204
+
205
+ .filter-pills {
206
+ display: flex;
207
+ gap: 6px;
208
+ margin-bottom: 16px;
209
+ flex-wrap: wrap;
210
+ }
211
+ .pill {
212
+ padding: 4px 10px;
213
+ font-size: 11px;
214
+ font-weight: 600;
215
+ border-radius: 20px;
216
+ cursor: pointer;
217
+ background: var(--surface);
218
+ color: var(--muted);
219
+ border: 1px solid var(--border);
220
+ transition: all 0.15s;
221
+ }
222
+ .pill:hover {
223
+ color: #fff;
224
+ border-color: var(--muted);
225
+ }
226
+ .pill.active {
227
+ background: rgba(245,158,11,0.15);
228
+ border-color: var(--amber);
229
+ color: var(--amber-light);
230
+ }
231
+
232
+ .gotcha-grid {
233
+ display: grid;
234
+ grid-template-columns: repeat(auto-fill, minmax(360px, 1fr));
235
+ gap: 16px;
236
+ }
237
+ .gotcha-card {
238
+ background: var(--card-bg);
239
+ border: 1px solid var(--border);
240
+ border-radius: 10px;
241
+ padding: 16px;
242
+ display: flex;
243
+ flex-direction: column;
244
+ gap: 10px;
245
+ transition: transform 0.15s, border-color 0.15s;
246
+ position: relative;
247
+ }
248
+ .gotcha-card:hover {
249
+ transform: translateY(-2px);
250
+ border-color: rgba(245,158,11,0.4);
251
+ }
252
+ .gotcha-card-header {
253
+ display: flex;
254
+ justify-content: space-between;
255
+ align-items: flex-start;
256
+ gap: 8px;
257
+ }
258
+ .gotcha-file {
259
+ font-family: var(--mono);
260
+ font-size: 12px;
261
+ font-weight: 600;
262
+ color: var(--cyan);
263
+ word-break: break-all;
264
+ }
265
+ .gotcha-badges {
266
+ display: flex;
267
+ gap: 5px;
268
+ flex-shrink: 0;
269
+ }
270
+ .badge-sev {
271
+ font-size: 10px;
272
+ font-weight: 700;
273
+ text-transform: uppercase;
274
+ padding: 2px 6px;
275
+ border-radius: 4px;
276
+ }
277
+ .badge-sev-critical { background: rgba(239,68,68,0.2); color: #fca5a5; border: 1px solid #ef4444; }
278
+ .badge-sev-high { background: rgba(249,115,22,0.2); color: #fdba74; border: 1px solid #f97316; }
279
+ .badge-sev-medium { background: rgba(245,158,11,0.2); color: #fde68a; border: 1px solid #f59e0b; }
280
+ .badge-sev-low { background: rgba(16,185,129,0.2); color: #86efac; border: 1px solid #10b981; }
281
+
282
+ .badge-status {
283
+ font-size: 10px;
284
+ font-weight: 600;
285
+ padding: 2px 6px;
286
+ border-radius: 4px;
287
+ background: var(--surface);
288
+ border: 1px solid var(--border);
289
+ color: var(--muted);
290
+ }
291
+ .status-active { color: var(--amber); border-color: rgba(245,158,11,0.3); }
292
+ .status-verified { color: var(--emerald); border-color: rgba(16,185,129,0.3); }
293
+ .status-candidate { color: var(--cyan); border-color: rgba(6,182,212,0.3); }
294
+ .status-resolved { color: #94a3b8; text-decoration: line-through; }
295
+ .status-deprecated { color: #64748b; font-style: italic; }
296
+
297
+ .gotcha-desc {
298
+ font-size: 13px;
299
+ color: #e2e8f0;
300
+ line-height: 1.5;
301
+ }
302
+ .gotcha-workaround {
303
+ background: var(--bg);
304
+ border: 1px solid var(--border);
305
+ border-radius: 6px;
306
+ padding: 8px 10px;
307
+ font-size: 12px;
308
+ color: var(--muted);
309
+ position: relative;
310
+ }
311
+ .gotcha-workaround strong {
312
+ color: var(--amber-light);
313
+ display: block;
314
+ margin-bottom: 2px;
315
+ }
316
+ .btn-copy-workaround {
317
+ position: absolute;
318
+ top: 6px;
319
+ right: 6px;
320
+ background: var(--surface);
321
+ border: 1px solid var(--border);
322
+ color: var(--muted);
323
+ border-radius: 4px;
324
+ font-size: 10px;
325
+ padding: 2px 6px;
326
+ cursor: pointer;
327
+ }
328
+ .btn-copy-workaround:hover { color: #fff; border-color: var(--amber); }
329
+
330
+ .gotcha-footer {
331
+ font-size: 11px;
332
+ color: #64748b;
333
+ display: flex;
334
+ justify-content: space-between;
335
+ align-items: center;
336
+ border-top: 1px solid rgba(255,255,255,0.05);
337
+ padding-top: 8px;
338
+ }
339
+
340
+ .flow-list {
341
+ display: flex;
342
+ flex-direction: column;
343
+ gap: 16px;
344
+ }
345
+ .flow-card {
346
+ background: var(--card-bg);
347
+ border: 1px solid var(--border);
348
+ border-radius: 10px;
349
+ padding: 18px;
350
+ }
351
+ .flow-card-header {
352
+ display: flex;
353
+ justify-content: space-between;
354
+ margin-bottom: 12px;
355
+ }
356
+ .flow-anchor {
357
+ font-size: 15px;
358
+ font-weight: 700;
359
+ color: var(--indigo);
360
+ display: flex;
361
+ align-items: center;
362
+ gap: 6px;
363
+ }
364
+ .flow-hops-chain {
365
+ display: flex;
366
+ align-items: center;
367
+ flex-wrap: wrap;
368
+ gap: 8px;
369
+ background: var(--bg);
370
+ padding: 12px;
371
+ border-radius: 8px;
372
+ border: 1px solid var(--border);
373
+ margin-bottom: 12px;
374
+ }
375
+ .hop-node {
376
+ background: var(--surface);
377
+ border: 1px solid var(--border);
378
+ padding: 6px 12px;
379
+ border-radius: 6px;
380
+ font-family: var(--mono);
381
+ font-size: 12px;
382
+ color: #cbd5e1;
383
+ }
384
+ .hop-arrow {
385
+ color: var(--amber);
386
+ font-weight: 700;
387
+ }
388
+
389
+ .workspace-grid {
390
+ display: grid;
391
+ grid-template-columns: 1fr 1fr;
392
+ gap: 20px;
393
+ }
394
+ .ws-card {
395
+ background: var(--card-bg);
396
+ border: 1px solid var(--border);
397
+ border-radius: 10px;
398
+ padding: 20px;
399
+ }
400
+ .ws-card h3 {
401
+ font-size: 14px;
402
+ margin-bottom: 12px;
403
+ color: var(--amber-light);
404
+ display: flex;
405
+ align-items: center;
406
+ gap: 6px;
407
+ }
408
+
409
+ .kpi-row {
410
+ display: grid;
411
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
412
+ gap: 16px;
413
+ margin-bottom: 24px;
414
+ }
415
+ .kpi-card {
416
+ background: var(--card-bg);
417
+ border: 1px solid var(--border);
418
+ border-radius: 10px;
419
+ padding: 16px;
420
+ text-align: left;
421
+ }
422
+ .kpi-num {
423
+ font-size: 26px;
424
+ font-weight: 800;
425
+ color: var(--cyan);
426
+ margin-bottom: 4px;
427
+ }
428
+ .kpi-label {
429
+ font-size: 12px;
430
+ color: var(--muted);
431
+ font-weight: 500;
432
+ }
433
+
434
+ .table-container {
435
+ background: var(--card-bg);
436
+ border: 1px solid var(--border);
437
+ border-radius: 10px;
438
+ overflow: hidden;
439
+ margin-bottom: 24px;
440
+ }
441
+ table.clean-table {
442
+ width: 100%;
443
+ border-collapse: collapse;
444
+ font-size: 12px;
445
+ }
446
+ table.clean-table th {
447
+ background: var(--surface);
448
+ text-align: left;
449
+ padding: 10px 14px;
450
+ color: var(--muted);
451
+ font-weight: 600;
452
+ border-bottom: 1px solid var(--border);
453
+ }
454
+ table.clean-table td {
455
+ padding: 10px 14px;
456
+ border-bottom: 1px solid rgba(255,255,255,0.04);
457
+ color: #cbd5e1;
458
+ }
459
+ table.clean-table tr:hover td {
460
+ background: var(--surface-hover);
461
+ }
462
+
463
+ #graph-viewport {
464
+ width: 100%;
465
+ height: 100%;
466
+ position: relative;
467
+ }
468
+ #graph-canvas {
469
+ width: 100%;
470
+ height: 100%;
471
+ background: radial-gradient(ellipse at center, #161e31 0%, var(--bg) 75%);
472
+ }
473
+ .graph-toolbar {
474
+ position: absolute;
475
+ top: 16px;
476
+ left: 16px;
477
+ display: flex;
478
+ gap: 8px;
479
+ background: rgba(17,24,39,0.9);
480
+ padding: 6px 10px;
481
+ border-radius: 8px;
482
+ border: 1px solid var(--border);
483
+ backdrop-filter: blur(8px);
484
+ z-index: 10;
485
+ }
486
+ .graph-toolbar button {
487
+ background: var(--surface-hover);
488
+ border: 1px solid var(--border);
489
+ color: #fff;
490
+ padding: 4px 8px;
491
+ border-radius: 5px;
492
+ font-size: 11px;
493
+ cursor: pointer;
494
+ }
495
+ .graph-toolbar button.active {
496
+ background: var(--amber-dark);
497
+ }
498
+
499
+ .empty-placeholder {
500
+ text-align: center;
501
+ padding: 60px 20px;
502
+ color: var(--muted);
503
+ }
504
+ .empty-placeholder i {
505
+ width: 42px;
506
+ height: 42px;
507
+ margin-bottom: 12px;
508
+ opacity: 0.5;
115
509
  }
116
-
117
- /* Search */
118
- #search-box { position: absolute; top: 16px; right: 16px; z-index: 10; }
119
-
120
- /* Context Menu */
121
- #ctx-menu {
122
- position: fixed; display: none; z-index: 60; background: var(--surface);
123
- border: 1px solid var(--border); border-radius: 8px; padding: 4px;
124
- box-shadow: 0 8px 32px rgba(0,0,0,0.5); min-width: 160px;
125
- backdrop-filter: blur(12px);
126
- }
127
- #ctx-menu .ctx-item {
128
- display: flex; align-items: center; gap: 8px; padding: 7px 12px;
129
- border-radius: 6px; cursor: pointer; font-size: 12px; color: var(--text);
130
- transition: background 0.1s;
131
- }
132
- #ctx-menu .ctx-item:hover { background: rgba(167,139,250,0.15); color: var(--accent-bright); }
133
- #ctx-menu .ctx-item i { width: 14px; height: 14px; flex-shrink: 0; }
134
- #ctx-menu .ctx-sep { height: 1px; background: var(--border); margin: 4px 8px; }
135
- #search-box input {
136
- background: rgba(15,23,42,0.92); border: 1px solid var(--border);
137
- border-radius: 8px; padding: 8px 12px; color: var(--text); font-size: 12px;
138
- width: 200px; outline: none; backdrop-filter: blur(8px);
139
- transition: border-color 0.2s;
140
- }
141
- #search-box input:focus { border-color: var(--accent-bright); box-shadow: 0 0 8px rgba(167,139,250,0.3); }
142
- #search-box input::placeholder { color: var(--muted); }
143
-
144
- /* Graph controls */
145
- #graph-controls {
146
- position: fixed; bottom: 16px; right: 336px; z-index: 10;
147
- display: flex; gap: 6px;
148
- }
149
- #graph-controls button {
150
- background: rgba(15,23,42,0.92); border: 1px solid var(--border);
151
- border-radius: 6px; color: var(--text); padding: 6px 10px;
152
- font-size: 11px; cursor: pointer; backdrop-filter: blur(8px);
153
- transition: background 0.2s;
154
- }
155
- #graph-controls button:hover { background: var(--accent); border-color: var(--accent-bright); }
156
- #graph-controls button.active {
157
- background: var(--accent) !important; border-color: var(--accent-bright) !important;
158
- color: #ffffff !important; box-shadow: 0 0 12px rgba(167,139,250,0.5);
159
- }
160
- #graph-controls .ctrl-sep { width: 1px; background: var(--border); margin: 0 2px; }
161
- .depth-btn { min-width: 28px !important; padding: 4px 6px !important; font-size: 11px !important; font-weight: 600; }
162
- .depth-btn.active { background: rgba(167,139,250,0.25) !important; border-color: var(--accent-bright) !important; color: var(--accent-bright) !important; }
163
-
164
- /* Loading */
165
- #loading {
166
- position: fixed; inset: 0; background: var(--bg); display: flex;
167
- align-items: center; justify-content: center; z-index: 100;
168
- font-size: 18px; color: var(--accent-bright); flex-direction: column; gap: 16px;
169
- }
170
- #loading .spinner { width: 32px; height: 32px; border: 3px solid var(--border); border-top-color: var(--accent-bright); border-radius: 50%; animation: spin 0.8s linear infinite; }
171
- @keyframes spin { to { transform: rotate(360deg); } }
172
-
173
- /* Node Detail Modal */
174
- .modal-overlay {
175
- position: fixed; inset: 0; background: rgba(0,0,0,0.6); z-index: 50;
176
- display: flex; align-items: center; justify-content: center;
177
- backdrop-filter: blur(4px); animation: fadeIn 0.15s;
178
- }
179
- @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
180
- .modal {
181
- background: var(--surface); border: 1px solid var(--border); border-radius: 10px;
182
- width: 520px; max-width: 90vw; max-height: 85vh; overflow: hidden; box-shadow: 0 16px 64px rgba(0,0,0,0.5);
183
- display: flex; flex-direction: column; animation: slideUp 0.2s;
184
- }
185
- @keyframes slideUp { from { transform: translateY(20px); opacity: 0; } to { transform: translateY(0); opacity: 1; } }
186
- .modal-header {
187
- display: flex; align-items: center; justify-content: space-between;
188
- padding: 10px 16px; border-bottom: 1px solid var(--border); flex-shrink: 0;
189
- }
190
- .modal-header h2 { font-size: 13px; font-weight: 700; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
191
- .modal-header .close {
192
- background: none; border: none; color: var(--muted); font-size: 18px;
193
- cursor: pointer; width: 28px; height: 28px; display: flex; align-items: center; justify-content: center;
194
- border-radius: 6px; transition: all 0.15s; flex-shrink: 0;
195
- }
196
- .modal-header .close:hover { background: rgba(255,255,255,0.1); color: var(--text); }
197
- .modal-body { padding: 12px 16px; overflow-y: auto; flex: 1; min-height: 0; }
198
- .modal-section { margin-bottom: 10px; }
199
- .modal-section h3 { font-size: 10px; font-weight: 600; color: var(--muted); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 4px; }
200
- .modal-kv { display: flex; gap: 6px; margin-bottom: 3px; font-size: 11px; line-height: 1.3; }
201
- .modal-kv .key { color: var(--muted); min-width: 64px; flex-shrink: 0; }
202
- .modal-kv .val { color: var(--text); word-break: break-word; overflow-wrap: anywhere; min-width: 0; }
203
- .relation-item {
204
- display: flex; align-items: center; gap: 5px; padding: 4px 7px;
205
- margin-bottom: 2px; border-radius: 5px; background: var(--bg);
206
- font-size: 11px; cursor: pointer; transition: background 0.15s;
207
- }
208
- .relation-item:hover { background: #1e293b; }
209
- .relation-item .arrow { color: var(--accent); font-weight: 700; flex-shrink: 0; }
210
- .relation-item .name { color: var(--text); flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
211
- .relation-item .type { color: var(--muted); font-size: 10px; flex-shrink: 0; max-width: 80px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
212
- .relation-item .weight { color: var(--accent2); font-size: 10px; flex-shrink: 0; }
213
-
214
- ::-webkit-scrollbar { width: 4px; height: 4px; }
215
- ::-webkit-scrollbar-track { background: transparent; }
216
- ::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }
217
510
  </style>
218
511
  </head>
219
512
  <body>
220
- <div id="loading"><div class="spinner"></div>Connecting to Kuma...</div>
513
+
221
514
  <header>
222
- <h1><svg width="22" height="22" viewBox="0 0 100 100" style="vertical-align:middle;margin-right:6px"><circle cx="50" cy="50" r="48" fill="#D97706"/><circle cx="50" cy="50" r="44" fill="#F59E0B"/><circle cx="50" cy="45" r="30" fill="#92400E"/><circle cx="35" cy="38" r="5" fill="#1F2937"/><circle cx="65" cy="38" r="5" fill="#1F2937"/><ellipse cx="50" cy="50" rx="10" ry="6" fill="#B45309"/><path d="M35 58 Q50 72 65 58" stroke="#1F2937" stroke-width="3.5" fill="none" stroke-linecap="round"/></svg> Kuma Studio</h1>
223
- <div class="status"><span class="dot" id="status-dot"></span><span id="status-text">connecting...</span></div>
224
- <div style="margin-left:auto;display:flex;gap:8px;-webkit-app-region:no-drag">
225
- <button onclick="refetch()" id="refetch-btn" style="background:rgba(167,139,250,0.12);border:1px solid rgba(167,139,250,0.25);border-radius:5px;color:var(--accent-bright);padding:4px 10px;font-size:10px;cursor:pointer;transition:all 0.2s;display:flex;align-items:center;gap:3px"><i data-lucide="refresh-cw" style="width:11px;height:11px"></i> Refetch</button>
226
- <button onclick="resetKumaDb()" style="background:rgba(239,68,68,0.12);border:1px solid rgba(239,68,68,0.25);border-radius:5px;color:var(--red);padding:4px 10px;font-size:10px;cursor:pointer;transition:all 0.2s;display:flex;align-items:center;gap:3px"><i data-lucide="trash-2" style="width:11px;height:11px"></i> Reset</button>
227
- <button onclick="stopKumaStudio()" style="background:rgba(148,163,184,0.12);border:1px solid rgba(148,163,184,0.25);border-radius:5px;color:#94a3b8;padding:4px 10px;font-size:10px;cursor:pointer;transition:all 0.2s;display:flex;align-items:center;gap:3px"><i data-lucide="square" style="width:11px;height:11px"></i> Stop</button>
228
- </div>
229
- </header>
230
- <div class="stats-bar" id="stats-bar"></div>
231
- <div id="main">
232
- <div id="graph-panel">
233
- <div id="graph-container"></div>
234
- <div id="search-box"><input id="node-search" type="text" placeholder="Search nodes..." /></div>
235
- <div id="graph-controls">
236
- <button onclick="if(network)network.fit()" title="Fit to viewport"><i data-lucide="maximize-2" style="width:12px;height:12px"></i> Fit</button>
237
- <div class="ctrl-sep"></div>
238
- <button class="depth-btn" data-depth="1" onclick="setDepth(1)">1</button>
239
- <button class="depth-btn active" data-depth="2" onclick="setDepth(2)">2</button>
240
- <button class="depth-btn" data-depth="3" onclick="setDepth(3)">3</button>
241
- <button class="depth-btn" data-depth="99" onclick="setDepth(99)">All</button>
242
- <div class="ctrl-sep"></div>
243
- <button id="btn-freeze-physics" onclick="stopPhysics()"><i data-lucide="pause" style="width:12px;height:12px"></i></button>
244
- <button id="btn-unfreeze-physics" class="active" onclick="restartPhysics()"><i data-lucide="play" style="width:12px;height:12px"></i></button>
515
+ <div class="brand-group">
516
+ <div class="brand-title">
517
+ <svg width="22" height="22" viewBox="0 0 100 100" style="vertical-align:middle"><circle cx="50" cy="50" r="48" fill="#D97706"/><circle cx="50" cy="50" r="44" fill="#F59E0B"/><circle cx="50" cy="45" r="30" fill="#92400E"/><circle cx="35" cy="38" r="5" fill="#1F2937"/><circle cx="65" cy="38" r="5" fill="#1F2937"/><ellipse cx="50" cy="50" rx="10" ry="6" fill="#B45309"/><path d="M35 58 Q50 72 65 58" stroke="#1F2937" stroke-width="3.5" fill="none" stroke-linecap="round"/></svg>
518
+ Kuma Studio
245
519
  </div>
246
- <div id="graph-tooltip"></div>
247
- <div id="legend"></div>
248
- </div>
249
- <div id="sidebar">
250
- <div class="tabs">
251
- <div class="tab active" data-tab="features" title="Features"><i data-lucide="star" style="width:12px;height:12px"></i><span class="tab-label">Features</span></div>
252
- <div class="tab" data-tab="gotchas" title="Gotchas"><i data-lucide="alert-triangle" style="width:12px;height:12px"></i><span class="tab-label">Gotchas</span></div>
253
- <div class="tab" data-tab="injections" title="Shadow Memory"><i data-lucide="zap" style="width:12px;height:12px"></i><span class="tab-label">Injections</span></div>
254
- <div class="tab" data-tab="efficiency" title="Efficiency"><i data-lucide="zap" style="width:12px;height:12px"></i><span class="tab-label">Efficiency</span></div>
255
- <div class="tab" data-tab="staleness" title="Staleness"><i data-lucide="activity" style="width:12px;height:12px"></i><span class="tab-label">Staleness</span></div>
256
- <div class="tab" data-tab="activity" title="Activity"><i data-lucide="bar-chart-3" style="width:12px;height:12px"></i><span class="tab-label">Activity</span></div>
520
+ <span class="badge-project" id="project-tag">loading...</span>
521
+ <div class="db-status">
522
+ <span class="dot-status" id="db-dot"></span>
523
+ <span id="db-size-text">0 KB</span>
257
524
  </div>
258
- <div class="tab-content" id="tab-content"></div>
259
525
  </div>
260
- </div>
261
- <div id="modal-root"></div>
262
- <div id="ctx-menu">
263
- <div class="ctx-item" onclick="ctxViewDetail()"><i data-lucide="eye"></i> View Details</div>
264
- <div class="ctx-item" onclick="ctxCopyId()"><i data-lucide="copy"></i> Copy ID</div>
265
- <div class="ctx-sep"></div>
266
- <div class="ctx-item" onclick="ctxZoomTo()"><i data-lucide="focus"></i> Zoom to Node</div>
267
- </div>
268
526
 
269
- <script>
270
- var API = "/api";
271
- var network = null, nodesData = null, allNodes = null, allEdges = null, currentDbSize = "0 KB";
272
- var edgeDefaults = {};
273
-
274
- async function resetKumaDb() {
275
- var confirmation = prompt("⚠️ TYPE 'DELETE' TO CONFIRM RESETTING KUMA DB\nThis will permanently wipe all knowledge:");
276
- if (confirmation !== "DELETE") { if (confirmation !== null) alert("Reset cancelled."); return; }
277
- try {
278
- var res = await fetch(API + "/reset-db?confirm=1", { method: "POST" }).then(function(r){return r.json()});
279
- if (res.ok) { alert("Kuma DB reset."); refetch(); }
280
- } catch (e) { alert("Reset failed: " + e.message); }
281
- }
527
+ <nav class="nav-tabs">
528
+ <button class="nav-tab active" data-view="gotchas">
529
+ <i data-lucide="shield-alert" style="width:14px;height:14px"></i>
530
+ Gotcha Shield
531
+ <span class="tab-badge" id="badge-gotcha-count">0</span>
532
+ </button>
533
+ <button class="nav-tab" data-view="flows">
534
+ <i data-lucide="git-merge" style="width:14px;height:14px"></i>
535
+ Sequence Flows
536
+ <span class="tab-badge" id="badge-flow-count">0</span>
537
+ </button>
538
+ <button class="nav-tab" data-view="workspace">
539
+ <i data-lucide="box" style="width:14px;height:14px"></i>
540
+ Workspace & Impact
541
+ </button>
542
+ <button class="nav-tab" data-view="health">
543
+ <i data-lucide="activity" style="width:14px;height:14px"></i>
544
+ Efficiency & Health
545
+ </button>
546
+ <button class="nav-tab" data-view="graph">
547
+ <i data-lucide="network" style="width:14px;height:14px"></i>
548
+ Knowledge Graph
549
+ </button>
550
+ </nav>
551
+
552
+ <div class="action-group">
553
+ <button class="btn-action" onclick="fetchDashboard()"><i data-lucide="refresh-cw" style="width:12px;height:12px"></i> Refresh</button>
554
+ <button class="btn-action" onclick="resetDb()" style="color:var(--rose)"><i data-lucide="trash-2" style="width:12px;height:12px"></i> Reset DB</button>
555
+ </div>
556
+ </header>
282
557
 
283
- async function stopKumaStudio() {
284
- if (!confirm("Stop Kuma Studio?")) return;
285
- try {
286
- await fetch(API + "/stop-server?confirm=1", { method: "POST" });
287
- 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>';
288
- } catch (e) { alert("Stop failed: " + e.message); }
289
- }
558
+ <main id="viewport">
559
+ <!-- VIEW 1: GOTCHA SHIELD -->
560
+ <section class="view-panel active" id="view-gotchas">
561
+ <div class="view-header">
562
+ <div>
563
+ <div class="view-title"><i data-lucide="shield-alert" style="color:var(--amber)"></i> Known Code Gotchas</div>
564
+ <div class="view-desc">Catalog of fragile legacy contracts, edge cases, and unexpected quirks injected right before edits.</div>
565
+ </div>
566
+ <input type="text" id="gotcha-search" class="search-input" placeholder="Search file, issue, or workaround..." oninput="filterGotchas()" />
567
+ </div>
290
568
 
291
- function resetGraphFocus() {
292
- if (!network) return;
293
- network.body.data.nodes.forEach(function(n) {
294
- network.body.data.nodes.update({ id: n.id, opacity: 1.0, font: { color: '#ffffff', bold: true } });
295
- });
296
- document.getElementById("graph-tooltip").style.display = 'none';
297
- var s = document.getElementById("node-search"); if (s) s.value = "";
298
- }
569
+ <div class="filter-pills" id="status-pills">
570
+ <div class="pill active" data-status="all" onclick="setStatusFilter('all')">All Statuses</div>
571
+ <div class="pill" data-status="active" onclick="setStatusFilter('active')">Active</div>
572
+ <div class="pill" data-status="verified" onclick="setStatusFilter('verified')">Verified</div>
573
+ <div class="pill" data-status="candidate" onclick="setStatusFilter('candidate')">Candidate</div>
574
+ <div class="pill" data-status="resolved" onclick="setStatusFilter('resolved')">Resolved</div>
575
+ <div class="pill" data-status="deprecated" onclick="setStatusFilter('deprecated')">Deprecated</div>
576
+ </div>
299
577
 
300
- function exportGraphPNG() {
301
- if (!network) return;
302
- var canvas = document.querySelector("#graph-container canvas");
303
- if (!canvas) return;
304
- var link = document.createElement("a");
305
- link.download = "kuma-knowledge-graph.png";
306
- link.href = canvas.toDataURL("image/png");
307
- link.click();
308
- }
578
+ <div class="gotcha-grid" id="gotcha-cards-container"></div>
579
+ </section>
309
580
 
310
- function restartPhysics() {
311
- if (!network) return;
312
- network.setOptions({ physics: { enabled: true } });
313
- network.startSimulation();
314
- document.getElementById('btn-unfreeze-physics').classList.add('active');
315
- document.getElementById('btn-freeze-physics').classList.remove('active');
316
- }
581
+ <!-- VIEW 2: SEQUENCE FLOWS -->
582
+ <section class="view-panel" id="view-flows">
583
+ <div class="view-header">
584
+ <div>
585
+ <div class="view-title"><i data-lucide="git-merge" style="color:var(--indigo)"></i> Architecture & Sequence Flows</div>
586
+ <div class="view-desc">Execution call chains and architectural dependencies connecting entry points to services and database.</div>
587
+ </div>
588
+ </div>
589
+ <div class="flow-list" id="flow-cards-container"></div>
590
+ </section>
591
+
592
+ <!-- VIEW 3: WORKSPACE & IMPACT -->
593
+ <section class="view-panel" id="view-workspace">
594
+ <div class="view-header">
595
+ <div>
596
+ <div class="view-title"><i data-lucide="box" style="color:var(--cyan)"></i> Workspace & Blast Radius Topology</div>
597
+ <div class="view-desc">Monorepo packages, package graph, and blast radius simulation.</div>
598
+ </div>
599
+ </div>
600
+ <div class="workspace-grid">
601
+ <div class="ws-card">
602
+ <h3><i data-lucide="layers" style="width:16px;height:16px"></i> Repository Topology</h3>
603
+ <div id="ws-topology-info" style="font-size:13px;color:#cbd5e1;line-height:1.6">Loading workspace information...</div>
604
+ </div>
605
+ <div class="ws-card">
606
+ <h3><i data-lucide="target" style="width:16px;height:16px"></i> Blast Radius Checker</h3>
607
+ <p style="font-size:12px;color:var(--muted);margin-bottom:12px">Check downstream impact and consumer packages for a target file:</p>
608
+ <div style="display:flex;gap:8px;margin-bottom:12px">
609
+ <input type="text" id="blast-input" class="search-input" placeholder="e.g. src/auth.ts or package/core" style="flex:1" />
610
+ <button class="btn-action" onclick="simulateBlastRadius()">Check</button>
611
+ </div>
612
+ <div id="blast-result" style="font-size:12px;color:#cbd5e1;background:var(--bg);padding:12px;border-radius:6px;border:1px solid var(--border)">Enter a target path to inspect blast radius.</div>
613
+ </div>
614
+ </div>
615
+ </section>
616
+
617
+ <!-- VIEW 4: HEALTH & EFFICIENCY -->
618
+ <section class="view-panel" id="view-health">
619
+ <div class="view-header">
620
+ <div>
621
+ <div class="view-title"><i data-lucide="activity" style="color:var(--emerald)"></i> Efficiency & Health Metrics</div>
622
+ <div class="view-desc">Proving that the more coding agents use Kuma, the more efficient and regression-free development becomes.</div>
623
+ </div>
624
+ </div>
317
625
 
318
- function stopPhysics() {
319
- if (!network) return;
320
- network.stopSimulation();
321
- network.setOptions({ physics: { enabled: false } });
322
- document.getElementById('btn-freeze-physics').classList.add('active');
323
- document.getElementById('btn-unfreeze-physics').classList.remove('active');
324
- }
626
+ <div class="kpi-row" id="kpi-container"></div>
627
+
628
+ <div class="table-container">
629
+ <div style="padding:12px 16px;font-size:13px;font-weight:700;border-bottom:1px solid var(--border)">Recent Sessions & Tool Calls</div>
630
+ <table class="clean-table" id="sessions-table">
631
+ <thead>
632
+ <tr>
633
+ <th>Time</th>
634
+ <th>Goal</th>
635
+ <th>Tool Calls</th>
636
+ <th>Edits</th>
637
+ <th>Failures</th>
638
+ <th>Rollbacks</th>
639
+ </tr>
640
+ </thead>
641
+ <tbody></tbody>
642
+ </table>
643
+ </div>
325
644
 
326
- async function init() {
327
- var status = await fetch(API+"/status").then(function(r){return r.json()});
328
- var dot = document.getElementById("status-dot");
329
- var txt = document.getElementById("status-text");
330
- if (status.ok) { dot.className = "dot on"; txt.textContent = status.project || "connected"; currentDbSize = status.dbSize || "0 KB"; }
331
- else { dot.className = "dot off"; txt.textContent = "no kuma.db found"; }
332
- document.getElementById("loading").style.display = "none";
333
- var data = await fetch(API+"/dashboard").then(function(r){return r.json()});
334
- if (data.error) return showError(data.error);
335
- setupTabs();
336
- renderStats(data);
337
- renderGraph(data);
338
- renderFeatures(data);
339
- renderGotchas(data);
340
- renderInjections(data);
341
- renderEfficiency(data);
342
- renderStaleness(data);
343
- renderActivity(data);
344
- setupSearch();
345
- lucide.createIcons();
346
- }
347
- init();
645
+ <div class="table-container">
646
+ <div style="padding:12px 16px;font-size:13px;font-weight:700;border-bottom:1px solid var(--border)">Stale Asset Detection (Self-Heal Audit)</div>
647
+ <div id="staleness-container" style="padding:16px;font-size:12px;color:var(--muted)">Checking staleness...</div>
648
+ </div>
649
+ </section>
650
+
651
+ <!-- VIEW 5: KNOWLEDGE GRAPH -->
652
+ <section class="view-panel" id="view-graph" style="padding:0">
653
+ <div id="graph-viewport">
654
+ <div class="graph-toolbar">
655
+ <button onclick="fitGraph()"><i data-lucide="maximize" style="width:11px;height:11px"></i> Fit</button>
656
+ <button id="btn-toggle-physics" onclick="togglePhysics()">Physics: Off</button>
657
+ <span style="color:var(--muted);font-size:11px;align-self:center;margin-left:4px" id="graph-stats-label">0 nodes</span>
658
+ </div>
659
+ <div id="graph-canvas"></div>
660
+ </div>
661
+ </section>
662
+ </main>
348
663
 
664
+ <script>
665
+ var API = window.location.origin + "/api";
666
+ var currentData = null;
667
+ var activeStatusFilter = "all";
668
+ var graphNetwork = null;
669
+ var physicsEnabled = false;
670
+
671
+ document.querySelectorAll(".nav-tab").forEach(function(tab) {
672
+ tab.addEventListener("click", function() {
673
+ document.querySelectorAll(".nav-tab").forEach(t => t.classList.remove("active"));
674
+ document.querySelectorAll(".view-panel").forEach(p => p.classList.remove("active"));
675
+ tab.classList.add("active");
676
+ var viewId = "view-" + tab.getAttribute("data-view");
677
+ var panel = document.getElementById(viewId);
678
+ if (panel) panel.classList.add("active");
679
+ if (tab.getAttribute("data-view") === "graph") {
680
+ setTimeout(initOrRefreshGraph, 50);
681
+ }
682
+ });
683
+ });
349
684
 
685
+ async function fetchDashboard() {
686
+ try {
687
+ var statusRes = await fetch(API + "/status").then(r => r.json());
688
+ if (statusRes.ok) {
689
+ document.getElementById("project-tag").textContent = statusRes.project || "kuma-project";
690
+ document.getElementById("db-size-text").textContent = statusRes.dbSize || "0 KB";
691
+ }
350
692
 
351
- function showError(msg) {
352
- 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>';
353
- lucide.createIcons();
693
+ var data = await fetch(API + "/dashboard").then(r => r.json());
694
+ currentData = data;
695
+ renderAll(data);
696
+ } catch (err) {
697
+ console.error("Failed to load dashboard:", err);
698
+ }
354
699
  }
355
700
 
356
- function renderStats(data) {
357
- var s = data.stats || {};
358
- var inj = data.injections || {};
359
- var savedMin = inj.savedFormatted || '0 min';
360
- document.getElementById("stats-bar").innerHTML =
361
- '<div class="stat"><span class="num">'+(s.node_count||0)+'</span><span class="lbl">nodes</span></div>'+
362
- '<div class="stat"><span class="num">'+(s.edge_count||0)+'</span><span class="lbl">edges</span></div>'+
363
- '<div class="stat"><span class="num">'+(s.gotcha_count||0)+'</span><span class="lbl">gotchas</span></div>'+
364
- '<div class="stat"><span class="num" style="color:var(--accent)">'+currentDbSize+'</span><span class="lbl">kuma.db</span></div>'+
365
- '<div class="stat"><span class="num" style="color:var(--blue)">'+inj.count+'</span><span class="lbl">injections</span></div>'+
366
- '<div class="stat"><span class="num" style="font-size:13px;color:var(--green)">'+savedMin+'</span><span class="lbl">time saved</span></div>';
701
+ function renderAll(data) {
702
+ renderGotchas(data.gotchas || []);
703
+ renderFlows(data.flows || []);
704
+ renderWorkspace(data.workspace, data.nodes || []);
705
+ renderHealth(data);
706
+ if (window.lucide && window.lucide.createIcons) window.lucide.createIcons();
367
707
  }
368
708
 
369
- function truncateLabel(name, maxLen) {
370
- if (!name) return "";
371
- return name.length <= maxLen ? name : name.substring(0, maxLen-2) + "..";
709
+ function setStatusFilter(status) {
710
+ activeStatusFilter = status;
711
+ document.querySelectorAll("#status-pills .pill").forEach(p => {
712
+ p.classList.toggle("active", p.getAttribute("data-status") === status);
713
+ });
714
+ filterGotchas();
715
+ }
716
+
717
+ function filterGotchas() {
718
+ if (!currentData || !currentData.gotchas) return;
719
+ var q = (document.getElementById("gotcha-search").value || "").toLowerCase();
720
+ var filtered = currentData.gotchas.filter(function(g) {
721
+ var matchStatus = activeStatusFilter === "all" || (g.status || "active").toLowerCase() === activeStatusFilter;
722
+ var matchSearch = !q ||
723
+ (g.file_path || "").toLowerCase().includes(q) ||
724
+ (g.description || "").toLowerCase().includes(q) ||
725
+ (g.workaround || "").toLowerCase().includes(q);
726
+ return matchStatus && matchSearch;
727
+ });
728
+ renderGotchaCards(filtered);
372
729
  }
373
730
 
374
- function calcNodeSize(name, type) {
375
- var isGotcha = name && name.toLowerCase().indexOf("gotcha:") === 0;
376
- if (isGotcha) return 14;
377
- if (type === "variable") return 14;
378
- var base = 18;
379
- if (type === "feature") base = 32;
380
- if (type === "feature_domain") base = 30;
381
- if (type === "workflow") base = 24;
382
- if (type === "cross_service_link") base = 20;
383
- if (type === "function") base = 16;
384
- if (type === "class") base = 20;
385
- if (type === "module") base = 22;
386
- if (type === "component") base = 20;
387
- if (type === "api_route") base = 18;
388
- if (type === "test") base = 18;
389
- var len = name ? Math.min(name.length, 25) : 4;
390
- return Math.min(36, Math.max(base, base + len * 0.4));
731
+ function renderGotchas(gotchas) {
732
+ document.getElementById("badge-gotcha-count").textContent = gotchas.length;
733
+ filterGotchas();
391
734
  }
392
735
 
393
- function renderGraph(data) {
394
- allNodes = data.nodes || [];
395
- allEdges = data.edges || [];
396
- if (!allNodes.length) {
397
- document.getElementById("graph-container").innerHTML = '<div class="empty">Knowledge Graph is empty</div>';
736
+ function renderGotchaCards(list) {
737
+ var container = document.getElementById("gotcha-cards-container");
738
+ if (!list.length) {
739
+ container.innerHTML = '<div class="empty-placeholder"><i data-lucide="check-circle"></i><div>No gotchas matching current filter.</div></div>';
740
+ if (window.lucide && window.lucide.createIcons) window.lucide.createIcons();
741
+ return;
742
+ }
743
+ container.innerHTML = list.map(function(g) {
744
+ var sev = (g.severity || "medium").toLowerCase();
745
+ var status = (g.status || "active").toLowerCase();
746
+ return `
747
+ <div class="gotcha-card">
748
+ <div class="gotcha-card-header">
749
+ <span class="gotcha-file">${escapeHtml(g.file_path || "general")}</span>
750
+ <div class="gotcha-badges">
751
+ <span class="badge-sev badge-sev-${sev}">${sev}</span>
752
+ <span class="badge-status status-${status}">${status}</span>
753
+ </div>
754
+ </div>
755
+ <div class="gotcha-desc">${escapeHtml(g.description || "")}</div>
756
+ ${g.workaround ? `
757
+ <div class="gotcha-workaround">
758
+ <strong>Workaround:</strong>
759
+ ${escapeHtml(g.workaround)}
760
+ <button class="btn-copy-workaround" onclick="copyText('${escapeAttr(g.workaround)}')">Copy</button>
761
+ </div>
762
+ ` : ''}
763
+ <div class="gotcha-footer">
764
+ <span>${g.scope_package ? `📦 ${escapeHtml(g.scope_package)}` : ''}</span>
765
+ <span>Added: ${new Date(g.created_at * 1000).toLocaleDateString()}</span>
766
+ </div>
767
+ </div>
768
+ `;
769
+ }).join("");
770
+ if (window.lucide && window.lucide.createIcons) window.lucide.createIcons();
771
+ }
772
+
773
+ function renderFlows(flows) {
774
+ document.getElementById("badge-flow-count").textContent = flows.length;
775
+ var container = document.getElementById("flow-cards-container");
776
+ if (!flows.length) {
777
+ container.innerHTML = '<div class="empty-placeholder"><i data-lucide="git-branch"></i><div>No domain sequence flows recorded yet.</div></div>';
778
+ if (window.lucide && window.lucide.createIcons) window.lucide.createIcons();
779
+ return;
780
+ }
781
+ container.innerHTML = flows.map(function(f) {
782
+ var meta = {};
783
+ try { meta = typeof f.metadata === "string" ? JSON.parse(f.metadata) : f.metadata; } catch {}
784
+ var hops = meta.hops || [];
785
+ return `
786
+ <div class="flow-card">
787
+ <div class="flow-card-header">
788
+ <div class="flow-anchor"><i data-lucide="anchor" style="width:16px;height:16px"></i> ${escapeHtml(f.name || "Flow")}</div>
789
+ <span style="font-size:11px;color:var(--muted)">${hops.length} hop(s)</span>
790
+ </div>
791
+ <div class="flow-hops-chain">
792
+ ${hops.map(function(h, idx) {
793
+ return `<span class="hop-node">${escapeHtml(h.to || h)}</span>${idx < hops.length - 1 ? '<span class="hop-arrow">→</span>' : ''}`;
794
+ }).join("")}
795
+ </div>
796
+ ${meta.gotchas && meta.gotchas.length ? `<div style="font-size:11px;color:var(--amber)">⚠️ Linked Gotchas: ${meta.gotchas.map(escapeHtml).join(", ")}</div>` : ''}
797
+ </div>
798
+ `;
799
+ }).join("");
800
+ if (window.lucide && window.lucide.createIcons) window.lucide.createIcons();
801
+ }
802
+
803
+ function renderWorkspace(ws, nodes) {
804
+ var container = document.getElementById("ws-topology-info");
805
+ if (!ws) {
806
+ container.innerHTML = "<div>Single package workspace.</div>";
398
807
  return;
399
808
  }
809
+ container.innerHTML = `
810
+ <div style="margin-bottom:8px"><strong>Workspace Type:</strong> <span class="badge-project">${escapeHtml(ws.type || "standard")}</span></div>
811
+ <div style="margin-bottom:8px"><strong>Project Root:</strong> <code>${escapeHtml(ws.name || "root")}</code> (v${escapeHtml(ws.version || "1.0.0")})</div>
812
+ <div style="margin-top:12px;color:var(--muted)">Total indexed knowledge entities: <strong>${nodes.length}</strong></div>
813
+ `;
814
+ }
815
+
816
+ function simulateBlastRadius() {
817
+ var val = (document.getElementById("blast-input").value || "").trim();
818
+ var out = document.getElementById("blast-result");
819
+ if (!val) {
820
+ out.innerHTML = "⚠️ Please enter a target path or symbol.";
821
+ return;
822
+ }
823
+ if (!currentData || !currentData.edges) {
824
+ out.innerHTML = "No graph data available.";
825
+ return;
826
+ }
827
+ var matchingEdges = currentData.edges.filter(e => String(e.source).includes(val) || String(e.target).includes(val));
828
+ out.innerHTML = `
829
+ <div style="font-weight:700;color:var(--amber-light);margin-bottom:6px">Blast Radius for "${escapeHtml(val)}":</div>
830
+ <div>Found <strong>${matchingEdges.length}</strong> directly connected relationship(s).</div>
831
+ <div style="margin-top:8px">${matchingEdges.slice(0, 5).map(e => `• <code>${escapeHtml(e.source)}</code> <strong>${escapeHtml(e.relation)}</strong> <code>${escapeHtml(e.target)}</code>`).join("<br>")}</div>
832
+ `;
833
+ }
400
834
 
401
- var colors = {
402
- feature: '#a855f7',
403
- feature_domain: '#dc2626',
404
- workflow: '#2563eb',
405
- cross_service_link: '#059669',
406
- function: '#7c3aed',
407
- class: '#0891b2',
408
- module: '#16a34a',
409
- component: '#ea580c',
410
- file: '#d97706',
411
- api_route: '#db2777',
412
- test: '#0d9488',
413
- variable: '#6366f1',
414
- gotcha: '#f97316',
415
- };
416
- var shapes = {
417
- feature: 'star',
418
- feature_domain: 'star',
419
- workflow: 'ellipse',
420
- cross_service_link: 'box',
421
- function: 'box',
422
- class: 'hexagon',
423
- module: 'ellipse',
424
- component: 'diamond',
425
- file: 'box',
426
- api_route: 'triangleDown',
427
- test: 'star',
428
- variable: 'box',
429
- };
835
+ function renderHealth(data) {
836
+ var eff = data.efficiency || {};
837
+ var kpiContainer = document.getElementById("kpi-container");
838
+ var passedRate = eff.verificationPassRate != null ? eff.verificationPassRate + "%" : "100%";
839
+ var savedMinutes = data.injections ? data.injections.savedFormatted : "0 min";
840
+
841
+ kpiContainer.innerHTML = `
842
+ <div class="kpi-card">
843
+ <div class="kpi-num">${data.gotchas ? data.gotchas.length : 0}</div>
844
+ <div class="kpi-label">Known Gotchas Tracked</div>
845
+ </div>
846
+ <div class="kpi-card">
847
+ <div class="kpi-num" style="color:var(--emerald)">${passedRate}</div>
848
+ <div class="kpi-label">Auto-Verification Pass Rate</div>
849
+ </div>
850
+ <div class="kpi-card">
851
+ <div class="kpi-num" style="color:var(--amber)">~${savedMinutes}</div>
852
+ <div class="kpi-label">Dev Time Saved (Shadow Memory)</div>
853
+ </div>
854
+ <div class="kpi-card">
855
+ <div class="kpi-num" style="color:var(--indigo)">${eff.toolCalls || 0}</div>
856
+ <div class="kpi-label">Agent Tool Interventions</div>
857
+ </div>
858
+ `;
859
+
860
+ var tbody = document.querySelector("#sessions-table tbody");
861
+ var sessions = eff.recentSessions || [];
862
+ if (!sessions.length) {
863
+ tbody.innerHTML = '<tr><td colspan="6" style="text-align:center;color:var(--muted)">No session records logged yet.</td></tr>';
864
+ } else {
865
+ tbody.innerHTML = sessions.map(function(s) {
866
+ return `
867
+ <tr>
868
+ <td>${s.startedAt ? new Date(s.startedAt).toLocaleTimeString() : "Recent"}</td>
869
+ <td>${escapeHtml(s.goal || "General development")}</td>
870
+ <td>${s.toolCalls}</td>
871
+ <td>${s.edits}</td>
872
+ <td>${s.failures}</td>
873
+ <td>${s.rollbacks}</td>
874
+ </tr>
875
+ `;
876
+ }).join("");
877
+ }
430
878
 
431
- var maxNodes = 300;
432
- var displayNodes = allNodes;
433
- var truncated = allNodes.length > maxNodes;
434
- if (truncated) displayNodes = allNodes.slice(0, maxNodes);
879
+ var staleBox = document.getElementById("staleness-container");
880
+ if (data.staleness && data.staleness.missing && data.staleness.missing.length) {
881
+ staleBox.innerHTML = `
882
+ <div style="color:var(--rose);margin-bottom:8px">⚠️ Detected ${data.staleness.missing.length} reference(s) to deleted files:</div>
883
+ ${data.staleness.missing.map(m => `<div>• <code>${escapeHtml(m.filePath)}</code> (${m.nodeCount} nodes) — ${escapeHtml(m.reason)}</div>`).join("")}
884
+ `;
885
+ } else {
886
+ staleBox.innerHTML = '<div style="color:var(--emerald)">✅ All indexed gotchas and graph references point to valid, existing files.</div>';
887
+ }
888
+ }
889
+
890
+ function initOrRefreshGraph() {
891
+ if (!currentData || !currentData.nodes) return;
892
+ var container = document.getElementById("graph-canvas");
893
+ document.getElementById("graph-stats-label").textContent = `${currentData.nodes.length} nodes, ${currentData.edges ? currentData.edges.length : 0} edges`;
435
894
 
436
- var visNodes = new vis.DataSet(displayNodes.map(function(n){
437
- var isGotcha = n.name && n.name.toLowerCase().indexOf("gotcha:") === 0;
438
- var shape = isGotcha ? 'triangle' : (shapes[n.type] || 'dot');
439
- var maxLen = isGotcha ? 20 : (shape === 'ellipse' ? 18 : 32);
440
- var label = truncateLabel(n.name, maxLen);
441
- var size = isGotcha ? 12 : calcNodeSize(n.name, n.type);
442
- var color = isGotcha ? '#f97316' : (colors[n.type] || '#64748b');
895
+ var visNodes = new vis.DataSet(currentData.nodes.slice(0, 300).map(function(n) {
896
+ var color = "#64748b";
897
+ if (n.type === "file") color = "#06b6d4";
898
+ else if (n.type === "gotcha") color = "#f59e0b";
899
+ else if (n.type === "arch_flow") color = "#6366f1";
900
+ else if (n.type === "decision") color = "#10b981";
443
901
  return {
444
- id: n.id, label: label,
445
- color: { background: color, border: color, highlight: { background: color, border: '#67e8f9' }, hover: { background: color, border: '#c4b5fd' } },
446
- shape: shape,
447
- font: { color: '#ffffff', size: Math.min(13, Math.max(10, size * 0.55)), face: 'sans-serif', multi: false, bold: true },
448
- borderWidth: 0, size: size, mass: 1 + (size / 40),
902
+ id: n.id,
903
+ label: n.name || n.id,
904
+ shape: "box",
905
+ color: { background: color, border: "#243049", highlight: "#fde68a" },
906
+ font: { color: "#fff", size: 11, face: "ui-monospace, monospace" },
907
+ margin: 8,
449
908
  };
450
909
  }));
451
910
 
452
- var nodeIds = new Set(displayNodes.map(function(n){return n.id}));
453
- var edgeList = allEdges.slice();
454
-
455
- var displayEdges = edgeList.filter(function(e){ return nodeIds.has(e.source) && nodeIds.has(e.target); }).slice(0, 500);
456
-
457
- edgeDefaults = {};
458
- var visEdges = new vis.DataSet(displayEdges.map(function(e, idx){
459
- var edgeColor = '#4a4a6a';
460
- var width = 1.5;
461
- var dashes = false;
462
- if (e.relation === 'flows_through') { edgeColor = '#3b82f6'; width = 2.5; }
463
- if (e.relation === 'affects') { edgeColor = '#ef4444'; width = 2; dashes = [5, 5]; }
464
- if (e.relation === 'triggers') { edgeColor = '#ef4444'; width = 2; dashes = true; }
465
- if (e.relation === 'syncs_with') { edgeColor = '#10b981'; width = 2; }
466
- if (e.relation === 'owns') { edgeColor = '#a855f7'; width = 2; }
467
- if (e.relation === 'contains') { edgeColor = '#64748b'; width = 1; dashes = true; }
468
- if (e.relation === 'depends_on') { edgeColor = '#eab308'; width = 1.5; }
469
-
470
- edgeDefaults['e'+idx] = { color: edgeColor, width: width };
911
+ var visEdges = new vis.DataSet((currentData.edges || []).slice(0, 500).map(function(e) {
471
912
  return {
472
- id: 'e'+idx, from: e.source, to: e.target,
473
- label: e.relation || '',
474
- arrows: { to: { enabled: true, scaleFactor: 0.7, type: 'arrow' } },
475
- color: { color: edgeColor, highlight: '#c4b5fd', hover: '#67e8f9', opacity: 0.85 },
476
- font: { size: 10, color: '#94a3b8', strokeWidth: 0, background: 'transparent' },
477
- dashes: dashes,
478
- smooth: { type: 'continuous', roundness: 0.3 },
479
- width: width, hoverWidth: 3, selectionWidth: 3,
913
+ from: e.source,
914
+ to: e.target,
915
+ arrows: "to",
916
+ color: { color: "#334155", highlight: "#f59e0b" },
917
+ width: 1,
480
918
  };
481
919
  }));
482
920
 
483
- var container = document.getElementById("graph-container");
484
- network = new vis.Network(container, {nodes: visNodes, edges: visEdges}, {
921
+ var options = {
485
922
  physics: {
486
- enabled: true, solver: 'barnesHut',
487
- barnesHut: { gravitationalConstant: -3000, centralGravity: 0.05, springLength: 150, springConstant: 0.02, damping: 0.4, avoidOverlap: 0.5 },
488
- stabilization: { iterations: 120, updateInterval: 50, onlyDynamicEdges: false, fit: false },
923
+ enabled: physicsEnabled,
924
+ barnesHut: { gravitationalConstant: -3000, springLength: 80 },
489
925
  },
490
- interaction: { hover: true, tooltipDelay: 300, zoomView: true, dragView: true, dragNodes: true, keyboard: true, multiselect: true, selectable: true, selectConnectedEdges: false },
491
- nodes: { borderWidth: 0, font: { color: '#ffffff', size: 11, face: 'sans-serif' }, margin: { top: 6, bottom: 6, left: 10, right: 10 }, shapeProperties: { borderRadius: 6 } },
492
- edges: { smooth: { type: 'continuous', roundness: 0.2 }, width: 1, color: { color: '#4a4a6a', highlight: '#a78bfa' } },
493
- configure: { enabled: false },
494
- });
495
-
496
- // Auto-freeze physics after stabilization (saves CPU)
497
- network.once('stabilizationIterationsDone', function() {
498
- network.setOptions({ physics: { enabled: false } });
499
- document.getElementById('btn-freeze-physics').classList.add('active');
500
- document.getElementById('btn-unfreeze-physics').classList.remove('active');
501
- });
502
-
503
- // Left-click node → focus only (no modal)
504
- var lastFocusedNode = null;
505
- var currentFocusDepth = 2;
506
- window.setDepth = function(d) {
507
- currentFocusDepth = d;
508
- document.querySelectorAll('.depth-btn').forEach(function(b) { b.classList.toggle('active', parseInt(b.dataset.depth) === d); });
509
- if (lastFocusedNode) focusNode(lastFocusedNode);
926
+ interaction: { hover: true, tooltipDelay: 100 },
510
927
  };
511
- function focusNode(selectedId) {
512
- // BFS with depth limit
513
- var connected = new Map();
514
- var queue = [[selectedId, 0]];
515
- connected.set(selectedId, 0);
516
- while (queue.length) {
517
- var pair = queue.shift();
518
- var cur = pair[0], depth = pair[1];
519
- if (depth >= currentFocusDepth) continue;
520
- network.getConnectedNodes(cur).forEach(function(nid) {
521
- if (!connected.has(nid)) { connected.set(nid, depth + 1); queue.push([nid, depth + 1]); }
522
- });
523
- }
524
- var nodeUpdates = [], edgeUpdates = [];
525
- network.body.data.nodes.forEach(function(n) {
526
- var hit = connected.has(n.id);
527
- nodeUpdates.push({ id: n.id, opacity: hit ? 1.0 : 0.08, font: { color: hit ? '#ffffff' : 'rgba(255,255,255,0.04)', bold: hit } });
528
- });
529
- network.body.data.nodes.update(nodeUpdates);
530
- network.body.data.edges.forEach(function(e) {
531
- var hit = connected.has(e.from) && connected.has(e.to);
532
- var def = edgeDefaults[e.id] || {};
533
- 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 });
534
- });
535
- network.body.data.edges.update(edgeUpdates);
536
- }
537
- network.on('click', function(params) {
538
- hideCtxMenu();
539
- if (params.nodes.length) {
540
- var selectedId = params.nodes[0];
541
- if (lastFocusedNode === selectedId) { lastFocusedNode = null; resetHighlight(); return; }
542
- lastFocusedNode = selectedId;
543
- focusNode(selectedId);
544
- } else {
545
- lastFocusedNode = null;
546
- resetHighlight();
547
- }
548
- });
549
-
550
- // Right-click node → context menu (document level, capture phase)
551
- document.addEventListener('contextmenu', function(e) {
552
- var containerEl = document.getElementById("graph-container");
553
- if (!containerEl || !containerEl.contains(e.target)) return;
554
- e.preventDefault();
555
- var rect = containerEl.getBoundingClientRect();
556
- var x = e.clientX - rect.left;
557
- var y = e.clientY - rect.top;
558
- var nodeId = network.getNodeAt({ x: x, y: y });
559
- if (nodeId && typeof nodeId === 'string') {
560
- var menu = document.getElementById('ctx-menu');
561
- menu.style.display = 'block';
562
- menu.style.left = e.clientX + 'px';
563
- menu.style.top = e.clientY + 'px';
564
- menu.dataset.nodeId = nodeId;
565
- lucide.createIcons();
566
- }
567
- }, true);
568
- // Close context menu on mousedown outside
569
- document.addEventListener('mousedown', function(e) {
570
- if (!e.target.closest('#ctx-menu')) hideCtxMenu();
571
- });
572
928
 
573
- // Hover → tooltip
574
- network.on('hoverNode', function(params) {
575
- network.body.data.nodes.updateOnly({ id: params.node, borderWidth: 2, borderWidthSelected: 2 });
576
- var n = allNodes.find(function(x){return x.id===params.node});
577
- if (n) {
578
- var tip = document.getElementById("graph-tooltip");
579
- tip.style.display = 'block';
580
- 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>' +
581
- (n.file_path ? '<br><span style="color:#38bdf8;font-size:11px;word-break:break-all;overflow-wrap:anywhere">' + n.file_path + '</span>' : '') +
582
- '<br><span style="font-size:10px;color:#64748b">Right-click for options</span></div>';
583
- var pos = network.getPositions([n.id]);
584
- if (pos && pos[n.id]) {
585
- var canvasPos = network.canvasToDOM(pos[n.id]);
586
- var panelRect = container.getBoundingClientRect();
587
- tip.style.left = Math.max(Math.min(canvasPos.x + 12, panelRect.width - 340), 8) + 'px';
588
- tip.style.top = Math.max(canvasPos.y - 10, 0) + 'px';
589
- }
590
- }
591
- });
592
- network.on('blurNode', function(params) {
593
- network.body.data.nodes.updateOnly({ id: params.node, borderWidth: 0, borderWidthSelected: 0 });
594
- document.getElementById("graph-tooltip").style.display = 'none';
595
- });
596
-
597
- var types = [...new Set(allNodes.map(function(n){return n.type||'unknown'}))];
598
- renderLegend(types, colors);
599
-
600
- if (truncated) {
601
- var warn = document.createElement('div');
602
- 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';
603
- warn.innerHTML = '<i data-lucide="alert-triangle" style="width:14px;height:14px;vertical-align:middle;margin-right:4px"></i> Showing ' + maxNodes + ' / ' + allNodes.length + ' nodes';
604
- document.getElementById('graph-panel').appendChild(warn);
929
+ if (graphNetwork) {
930
+ graphNetwork.destroy();
605
931
  }
932
+ graphNetwork = new vis.Network(container, { nodes: visNodes, edges: visEdges }, options);
606
933
  }
607
934
 
608
- function hideCtxMenu() { document.getElementById('ctx-menu').style.display = 'none'; }
609
- function resetHighlight() {
610
- if (!network) return;
611
- var nUp = [], eUp = [];
612
- network.body.data.nodes.forEach(function(n) { nUp.push({ id: n.id, opacity: 1.0, font: { color: '#ffffff', bold: true } }); });
613
- network.body.data.nodes.update(nUp);
614
- network.body.data.edges.forEach(function(e) {
615
- var def = edgeDefaults[e.id];
616
- eUp.push({ id: e.id, color: { color: def ? def.color : '#64748b', highlight: '#c4b5fd', hover: '#67e8f9', opacity: 0.85 }, width: def ? def.width : 1 });
617
- });
618
- network.body.data.edges.update(eUp);
619
- document.getElementById("graph-tooltip").style.display = 'none';
620
- }
621
- function ctxViewDetail() {
622
- var id = document.getElementById('ctx-menu').dataset.nodeId;
623
- hideCtxMenu();
624
- if (id) openNodeModal(id);
625
- }
626
- function ctxCopyId() {
627
- var id = document.getElementById('ctx-menu').dataset.nodeId;
628
- hideCtxMenu();
629
- if (id) navigator.clipboard.writeText(id).catch(function(){});
630
- }
631
- function ctxZoomTo() {
632
- var id = document.getElementById('ctx-menu').dataset.nodeId;
633
- hideCtxMenu();
634
- if (id && network) network.focus(id, { scale: 1.5, animation: { duration: 400, easingFunction: 'easeOutQuad' } });
635
- }
636
-
637
- // ============ NODE DETAIL MODAL ============
638
-
639
- async function openNodeModal(nodeId) {
640
- var node = allNodes.find(function(n){return n.id === nodeId;});
641
- if (!node) return;
642
-
643
- var root = document.getElementById("modal-root");
644
- 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>';
645
-
646
- try {
647
- var detail = await fetch(API+"/node/"+encodeURIComponent(nodeId)).then(function(r){return r.json()});
648
- var body = root.querySelector('.modal-body');
649
- if (!detail || detail.error) {
650
- body.innerHTML = '<div class="empty">Node not found</div>';
651
- return;
652
- }
653
-
654
- var n = typeof detail.node === 'string' ? JSON.parse(detail.node) : detail.node;
655
- var outgoing = typeof detail.outgoing === 'string' ? JSON.parse(detail.outgoing) : (detail.outgoing || []);
656
- var incoming = typeof detail.incoming === 'string' ? JSON.parse(detail.incoming) : (detail.incoming || []);
657
- var gotchas = typeof detail.gotchas === 'string' ? JSON.parse(detail.gotchas) : (detail.gotchas || []);
658
-
659
- var meta = {};
660
- try { meta = typeof n.metadata === 'string' ? JSON.parse(n.metadata) : (n.metadata || {}); } catch(e) {}
661
-
662
- var html = '';
663
-
664
- // Basic info
665
- html += '<div class="modal-section">';
666
- html += '<h3>Info</h3>';
667
- html += '<div class="modal-kv"><span class="key">Type</span><span class="val"><span style="color:var(--accent)">'+escapeHtml(n.type)+'</span></span></div>';
668
- html += '<div class="modal-kv"><span class="key">ID</span><span class="val" style="font-family:monospace;font-size:10px;color:var(--muted)">'+escapeHtml(n.id)+'</span></div>';
669
- 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>';
670
- if (n.severity) html += '<div class="modal-kv"><span class="key">Severity</span><span class="val" style="color:'+(n.severity==='critical'?'var(--red)':n.severity==='high'?'var(--orange)':n.severity==='medium'?'var(--yellow)':'var(--green)')+'">'+escapeHtml(n.severity)+'</span></div>';
671
- if (n.confidence !== undefined && n.confidence !== null) html += '<div class="modal-kv"><span class="key">Confidence</span><span class="val" style="color:var(--accent)">'+escapeHtml(String(n.confidence))+'</span></div>';
672
- if (n.last_verified_at) html += '<div class="modal-kv"><span class="key">Last Verified</span><span class="val">'+new Date(n.last_verified_at*1000).toLocaleString()+'</span></div>';
673
- if (n.created_at) html += '<div class="modal-kv"><span class="key">Created</span><span class="val">'+new Date(n.created_at*1000).toLocaleString()+'</span></div>';
674
- 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>';
675
- html += '</div>';
676
-
677
- // ALL metadata fields — render every key dynamically
678
- var metaKeys = Object.keys(meta);
679
- if (metaKeys.length > 0) {
680
- html += '<div class="modal-section">';
681
- html += '<h3>Metadata ('+metaKeys.length+' fields)</h3>';
682
- metaKeys.forEach(function(key) {
683
- var val = meta[key];
684
- if (val === null || val === undefined || val === '') return;
685
- var display = '';
686
- var color = 'var(--text)';
687
- if (Array.isArray(val)) {
688
- if (val.length === 0) return;
689
- display = val.join(' → ');
690
- color = 'var(--accent2)';
691
- } else if (typeof val === 'object') {
692
- display = JSON.stringify(val, null, 2);
693
- color = 'var(--muted)';
694
- } else if (typeof val === 'number') {
695
- display = String(val);
696
- color = 'var(--accent)';
697
- } else if (key === 'severity' || key === 'risk') {
698
- display = String(val);
699
- color = val === 'critical' ? 'var(--red)' : val === 'high' ? 'var(--orange)' : val === 'medium' ? 'var(--yellow)' : 'var(--green)';
700
- } else if (key === 'stale') {
701
- display = val === 1 ? 'YES — needs heal' : 'No';
702
- color = val === 1 ? 'var(--red)' : 'var(--green)';
703
- } else {
704
- display = String(val);
705
- }
706
- html += '<div class="modal-kv"><span class="key">'+escapeHtml(key)+'</span><span class="val" style="color:'+color+'">'+escapeHtml(display)+'</span></div>';
707
- });
708
- html += '</div>';
709
- }
710
-
711
- // Outgoing relations
712
- if (outgoing.length) {
713
- html += '<div class="modal-section"><h3>Outgoing ('+outgoing.length+')</h3>';
714
- outgoing.forEach(function(r) {
715
- var rel = typeof r === 'string' ? JSON.parse(r) : r;
716
- html += '<div class="relation-item" onclick="closeModal();setTimeout(function(){openNodeModal(\''+escapeHtml(rel.target)+'\')},200)">';
717
- html += '<span class="arrow"><i data-lucide="arrow-right" style="width:12px;height:12px"></i></span>';
718
- html += '<span class="name">'+escapeHtml(rel.target_name || rel.target.split('::').pop())+'</span>';
719
- html += '<span class="type">'+escapeHtml(rel.target_type||'')+'</span>';
720
- html += '<span class="weight">w:'+((rel.weight||1).toFixed(2))+'</span>';
721
- html += '<span style="color:var(--accent);font-size:10px;flex-shrink:0">'+escapeHtml(rel.relation)+'</span>';
722
- html += '</div>';
723
- });
724
- html += '</div>';
725
- }
726
-
727
- // Incoming relations
728
- if (incoming.length) {
729
- html += '<div class="modal-section"><h3>Incoming ('+incoming.length+')</h3>';
730
- incoming.forEach(function(r) {
731
- var rel = typeof r === 'string' ? JSON.parse(r) : r;
732
- html += '<div class="relation-item" onclick="closeModal();setTimeout(function(){openNodeModal(\''+escapeHtml(rel.source)+'\')},200)">';
733
- html += '<span class="arrow"><i data-lucide="arrow-left" style="width:12px;height:12px"></i></span>';
734
- html += '<span class="name">'+escapeHtml(rel.source_name || rel.source.split('::').pop())+'</span>';
735
- html += '<span class="type">'+escapeHtml(rel.source_type||'')+'</span>';
736
- html += '<span class="weight">w:'+((rel.weight||1).toFixed(2))+'</span>';
737
- html += '<span style="color:var(--accent);font-size:10px;flex-shrink:0">'+escapeHtml(rel.relation)+'</span>';
738
- html += '</div>';
739
- });
740
- html += '</div>';
741
- }
742
-
743
- // Gotchas
744
- if (gotchas.length) {
745
- html += '<div class="modal-section"><h3>Gotchas ('+gotchas.length+')</h3>';
746
- gotchas.forEach(function(g) {
747
- var got = typeof g === 'string' ? JSON.parse(g) : g;
748
- html += '<div class="gotcha-item" style="border-color:var(--'+(got.severity==='critical'?'red':got.severity==='high'?'orange':got.severity==='medium'?'yellow':'green')+')">';
749
- html += '<div class="desc">'+escapeHtml(got.description)+'</div>';
750
- html += '<div style="display:flex;gap:8px;align-items:center;margin-top:4px">';
751
- html += '<span class="sev-tag sev-'+got.severity+'">'+escapeHtml(got.severity)+'</span>';
752
- if (got.workaround) html += '<span style="color:var(--accent2);font-size:10px">Workaround: '+escapeHtml(got.workaround)+'</span>';
753
- if (got.added_by) html += '<span style="color:var(--muted);font-size:10px">by: '+escapeHtml(got.added_by)+'</span>';
754
- if (got.created_at) html += '<span style="color:var(--muted);font-size:10px">'+new Date(got.created_at*1000).toLocaleDateString()+'</span>';
755
- html += '</div></div>';
756
- });
757
- html += '</div>';
758
- }
759
-
760
- if (!outgoing.length && !incoming.length && !gotchas.length) {
761
- html += '<div class="empty" style="padding:20px">No relations recorded yet.</div>';
762
- }
763
-
764
- body.innerHTML = html;
765
- lucide.createIcons();
766
- } catch(e) {
767
- root.querySelector('.modal-body').innerHTML = '<div class="empty">Failed to load details: '+escapeHtml(e.message)+'</div>';
935
+ function togglePhysics() {
936
+ physicsEnabled = !physicsEnabled;
937
+ document.getElementById("btn-toggle-physics").textContent = physicsEnabled ? "Physics: On" : "Physics: Off";
938
+ document.getElementById("btn-toggle-physics").classList.toggle("active", physicsEnabled);
939
+ if (graphNetwork) {
940
+ graphNetwork.setOptions({ physics: { enabled: physicsEnabled } });
768
941
  }
769
942
  }
770
943
 
771
- function closeModal() {
772
- document.getElementById("modal-root").innerHTML = "";
773
- }
774
-
775
- function escapeHtml(str) {
776
- if (!str) return '';
777
- return String(str).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
778
- }
779
-
780
- // ============ SIDEBAR ============
781
-
782
- async function refetch() {
783
- var btn = document.getElementById('refetch-btn');
784
- btn.innerHTML = '<i data-lucide="loader" style="width:13px;height:13px;vertical-align:middle;margin-right:3px"></i> Loading...';
785
- btn.style.opacity = '0.6';
786
- lucide.createIcons();
787
- try {
788
- var status = await fetch(API+'/status').then(function(r){return r.json()});
789
- var dot = document.getElementById("status-dot");
790
- var txt = document.getElementById("status-text");
791
- if (status.ok) { dot.className = 'dot on'; txt.textContent = status.project || 'connected'; } else { dot.className = 'dot off'; txt.textContent = 'no kuma.db found'; }
792
- var data = await fetch(API+'/dashboard').then(function(r){return r.json()});
793
- if (data.error) { showError(data.error); return; }
794
- renderStats(data); renderGraph(data); renderGotchas(data); renderEfficiency(data); renderStaleness(data); renderInjections(data);
795
- lucide.createIcons();
796
- } catch(e) { showError('Refetch failed: ' + e.message); }
797
- btn.innerHTML = '<i data-lucide="refresh-cw" style="width:13px;height:13px;vertical-align:middle;margin-right:3px"></i> Refetch';
798
- btn.style.opacity = '1';
799
- lucide.createIcons();
800
- }
801
-
802
- function renderLegend(types, colors){
803
- document.getElementById("legend").innerHTML = '<div class="title">Legend</div>' +
804
- types.map(function(t){ return '<div class="legend-item"><div class="legend-dot" style="background:'+(colors[t]||'#888')+'"></div>' + t + '</div>'; }).join('');
805
- }
806
-
807
- function setupTabs(){
808
- var content = document.getElementById("tab-content");
809
- content.innerHTML =
810
- '<div class="panel active" id="panel-features"><div class="empty">Loading...</div></div>' +
811
- '<div class="panel" id="panel-gotchas"><div class="empty">Loading...</div></div>' +
812
- '<div class="panel" id="panel-injections"><div class="empty">Loading...</div></div>' +
813
- '<div class="panel" id="panel-efficiency"><div class="empty">Loading...</div></div>' +
814
- '<div class="panel" id="panel-staleness"><div class="empty">Loading...</div></div>' +
815
- '<div class="panel" id="panel-activity"><div class="empty">Loading...</div></div>';
816
- document.querySelectorAll('.tab').forEach(function(tab){
817
- tab.onclick = function(){
818
- document.querySelectorAll('.tab').forEach(function(t){t.classList.remove('active')});
819
- this.classList.add('active');
820
- document.querySelectorAll('.panel').forEach(function(p){p.classList.remove('active')});
821
- var p = document.getElementById('panel-'+this.dataset.tab);
822
- if(p) p.classList.add('active');
823
- };
824
- });
825
- document.querySelector('.tab.active').click();
826
- }
827
-
828
- function renderFeatures(data) {
829
- var items = data.features || [];
830
- var el = document.getElementById("panel-features");
831
- var tab=document.querySelector('[data-tab="features"]');
832
- if(!el) return;
833
- if(!items.length){if(tab)tab.style.display='none';if(tab&&tab.classList.contains('active')){tab.classList.remove('active');var gt=document.querySelector('[data-tab="gotchas"]');if(gt){gt.classList.add('active');}var gp=document.getElementById('panel-gotchas');if(gp){gp.classList.add('active');}el.classList.remove('active');}return;}
834
- if(tab)tab.style.display='';
835
- el.innerHTML = items.map(function(f){
836
- var feat = typeof f === 'string' ? JSON.parse(f) : f;
837
- var meta = {};
838
- try { meta = typeof feat.metadata === 'string' ? JSON.parse(feat.metadata) : (feat.metadata || {}); } catch(e) {}
839
- var tags = meta.tags || [];
840
- var risk = meta.risk || 'medium';
841
- var riskColor = risk==='critical'?'var(--red)':risk==='high'?'var(--orange)':risk==='medium'?'var(--yellow)':'var(--green)';
842
- return '<div class="feature-item" onclick="openNodeModal(\''+escapeHtml(feat.id)+'\')">' +
843
- '<div class="name"><i data-lucide="star" style="width:14px;height:14px;vertical-align:middle;margin-right:4px"></i> '+escapeHtml(feat.name)+'</div>' +
844
- (meta.description ? '<div class="meta">'+escapeHtml(meta.description)+'</div>' : '') +
845
- '<div class="meta">Risk: <span style="color:'+riskColor+'">'+risk+'</span></div>' +
846
- (tags.length ? '<div class="tags">'+tags.map(function(t){return '<span class="tag">'+escapeHtml(t)+'</span>';}).join('')+'</div>' : '') +
847
- '</div>';
848
- }).join('');
849
- lucide.createIcons();
944
+ function fitGraph() {
945
+ if (graphNetwork) graphNetwork.fit();
850
946
  }
851
947
 
852
- function renderGotchas(data){
853
- var items = data.gotchas || [];
854
- var el = document.getElementById("panel-gotchas");
855
- if(!el) return;
856
- if(!items.length){el.innerHTML='<div class="empty">No gotchas yet</div>';return;}
857
- el.innerHTML = items.map(function(g){
858
- var color = g.severity==='critical'?'red':g.severity==='high'?'orange':g.severity==='medium'?'yellow':'green';
859
- return '<div class="gotcha-item" style="border-color:var(--'+color+')">' +
860
- '<div class="file">'+(g.file_path||'global')+'</div>' +
861
- '<div class="desc">'+escapeHtml(g.description)+'</div>' +
862
- '<span class="sev-tag sev-'+g.severity+'">'+g.severity+'</span>' +
863
- (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>' : '') +
864
- '</div>';
865
- }).join('');
866
- lucide.createIcons();
867
- }
868
-
869
- function renderInjections(data) {
870
- var inj = data.injections || {};
871
- var el = document.getElementById("panel-injections");
872
- if(!el) return;
873
- if(!inj.count){el.innerHTML='<div class="empty">No shadow memory injections yet. Enable hooks via `kuma init --claude`.</div>';return;}
874
- el.innerHTML = '<div class="section-title">Shadow Memory (24h)</div>'+
875
- '<div class="efficiency-grid">'+
876
- '<div class="eff-item"><span class="eff-num">'+inj.count+'</span><span class="eff-lbl">Injections</span></div>'+
877
- '<div class="eff-item"><span class="eff-num" style="color:var(--green)">'+inj.savedFormatted+'</span><span class="eff-lbl">Time Saved</span></div>'+
878
- '<div class="eff-item"><span class="eff-num" style="color:var(--blue)">~'+Math.round(inj.savedMs/5000)+'x</span><span class="eff-lbl">Multiplier</span></div>'+
879
- '</div>'+
880
- '<div class="section-title" style="margin-top:16px">Auto-Inject Hooks</div>'+
881
- '<div class="gotcha-item">Pre-Edit: <code>kuma hook pre-edit</code> — injects gotchas + decisions + history</div>'+
882
- '<div class="gotcha-item">Pre-Bash: <code>kuma hook pre-bash</code> — injects command-triggered gotchas</div>'+
883
- '<div class="gotcha-item">Dedupe: files not re-injected within 15 min (I5)</div>'+
884
- '<div class="gotcha-item">Loop Capture: 4+ edits within 30 min auto-records a gotcha (I3)</div>';
885
- }
886
-
887
- // ============ EFFICIENCY PANEL (GAP 1) ============
888
- // Proves the philosophy: "the more you use Kuma, the more efficient it gets."
889
- // Every saved decision / record is an asset — here is the evidence.
890
-
891
- function fmtDuration(sec) {
892
- if (!sec || sec <= 0) return "0s";
893
- if (sec >= 3600) return (sec/3600).toFixed(1)+"h";
894
- if (sec >= 60) return Math.round(sec/60)+"m "+(sec%60)+"s";
895
- return sec+"s";
896
- }
897
-
898
- function renderEfficiency(data){
899
- var el = document.getElementById("panel-efficiency");
900
- if(!el) return;
901
- var eff = data.efficiency || {};
902
- var m = eff.metrics || {};
903
- var r = eff.recordings || {};
904
- var savedSec = m.researchTimeSaved ? Math.round(m.researchTimeSaved/1000) : 0;
905
- var passRate = eff.verificationPassRate;
906
- var passColor = passRate >= 80 ? 'var(--green)' : passRate >= 50 ? 'var(--yellow)' : 'var(--red)';
907
-
908
- var stat = function(num, lbl, color){
909
- return '<div style="flex:1;min-width:110px;padding:10px 12px;background:var(--bg);border-radius:8px;border:1px solid var(--border)"><div style="font-size:20px;font-weight:700;color:'+(color||'var(--accent2)')+'">'+num+'</div><div style="font-size:11px;color:var(--muted);margin-top:2px">'+lbl+'</div></div>';
910
- };
911
-
912
- var html = '';
913
- html += '<div style="display:flex;flex-wrap:wrap;gap:8px;margin-bottom:12px">' +
914
- stat(eff.sessions||0, 'sessions') +
915
- stat(eff.toolCalls||0, 'tool calls') +
916
- stat(eff.gotchas||0, 'gotchas', 'var(--orange)') +
917
- stat(eff.researchCacheScopes||0, 'research cached', 'var(--green)') +
918
- stat(eff.decisions||0, 'decisions', 'var(--accent)') +
919
- stat(eff.verifications||0, 'verifications', passRate==null?'var(--muted)':'var(--green)') +
920
- '</div>';
921
-
922
- // The headline: time saved from cache — the compounding asset
923
- html += '<div class="health-item" style="border-left:3px solid var(--green);margin-bottom:12px">' +
924
- '<div style="font-size:13px;color:var(--muted)">⏱ Estimated time saved from research cache</div>' +
925
- '<div class="score good" style="font-size:28px">'+fmtDuration(savedSec)+'</div>' +
926
- '<div class="meta">'+ (m.filesRead||0) +' files read &middot; '+ (m.filesEdited||0) +' files edited (current session)</div>' +
927
- '</div>';
928
-
929
- // Recording assets — what we've banked
930
- var recTotal = r.total||0;
931
- html += '<div style="font-size:11px;font-weight:600;color:var(--muted);text-transform:uppercase;letter-spacing:0.5px;margin:10px 0 6px">📦 Recorded Assets</div>';
932
- html += '<div class="health-item" style="margin-bottom:12px">';
933
- html += '<div style="display:flex;flex-wrap:wrap;gap:6px">';
934
- html += '<span class="sev-tag" style="background:rgba(124,58,237,0.15);color:var(--accent)">arch_flow: '+(r.archFlows||0)+'</span>';
935
- html += '<span class="sev-tag" style="background:#f9731620;color:var(--orange)">gotcha: '+(r.gotchas||0)+'</span>';
936
- html += '<span class="sev-tag" style="background:#eab30820;color:var(--yellow)">decision: '+(r.decisions||0)+'</span>';
937
- html += '<span class="sev-tag" style="background:#06b6d420;color:var(--accent2)">research_save: '+(r.researchSaves||0)+'</span>';
938
- html += '</div>';
939
- html += '<div class="meta" style="margin-top:6px">Total assets banked: <b style="color:var(--accent2)">'+recTotal+'</b> — each one saves re-research next session</div>';
940
- html += '</div>';
941
-
942
- // Verification pass rate
943
- html += '<div style="font-size:11px;font-weight:600;color:var(--muted);text-transform:uppercase;letter-spacing:0.5px;margin:10px 0 6px">🧪 Verification</div>';
944
- html += '<div class="health-item" style="margin-bottom:12px">' +
945
- '<div class="score" style="color:'+passColor+'">'+(passRate==null?'—':passRate+'%')+'</div>' +
946
- '<div class="meta">'+(eff.verifications||0)+' runs &middot; pass rate</div>' +
947
- '</div>';
948
-
949
- // Recent sessions breakdown
950
- var sess = eff.recentSessions || [];
951
- if (sess.length) {
952
- html += '<div style="font-size:11px;font-weight:600;color:var(--muted);text-transform:uppercase;letter-spacing:0.5px;margin:10px 0 6px">🕐 Recent Sessions</div>';
953
- html += sess.map(function(s){
954
- var d = s.startedAt ? new Date(s.startedAt*1000) : null;
955
- var dateStr = d ? d.toLocaleDateString()+' '+d.toLocaleTimeString([],{hour:'2-digit',minute:'2-digit'}) : '-';
956
- return '<div class="health-item">' +
957
- '<div style="font-size:12px;color:var(--text)">'+escapeHtml(s.goal || 'untitled goal')+'</div>' +
958
- '<div class="meta">'+dateStr+' &middot; '+s.toolCalls+' calls &middot; '+s.edits+' edits'+(s.failures?' &middot; <span style="color:var(--red)">'+s.failures+' failures</span>':'')+'</div>' +
959
- '</div>';
960
- }).join('');
948
+ async function resetDb() {
949
+ if (!confirm("Are you sure you want to reset kuma.db?")) return;
950
+ var res = await fetch(API + "/reset-db?confirm=1", { method: "POST" }).then(r => r.json());
951
+ if (res.ok) {
952
+ alert("Kuma DB reset successfully.");
953
+ fetchDashboard();
961
954
  }
962
-
963
- el.innerHTML = html;
964
- lucide.createIcons();
965
955
  }
966
956
 
967
- // ============ STALENESS PANEL (GAP 4) ============
968
- // Assets can become liabilities when stale. Surface them before they bite.
969
-
970
- function renderStaleness(data){
971
- var el = document.getElementById("panel-staleness");
972
- if(!el) return;
973
- var st = data.staleness || { checked: 0, staleNodes: 0, missing: [] };
974
- var html = '';
975
-
976
- if (!st.staleNodes) {
977
- html += '<div class="health-item" style="border-left:3px solid var(--green)">' +
978
- '<div class="score good" style="font-size:22px">✅ All fresh</div>' +
979
- '<div class="meta">'+st.checked+' file references checked — no stale assets found.</div>' +
980
- '</div>';
981
- } else {
982
- html += '<div class="health-item" style="border-left:3px solid var(--red)">' +
983
- '<div class="score bad" style="font-size:22px">'+st.staleNodes+' stale node(s)</div>' +
984
- '<div class="meta">'+st.missing.length+' file(s) referenced but missing on disk ('+st.checked+' checked).</div>' +
985
- '</div>';
986
- html += '<div style="display:flex;gap:6px;margin:10px 0">' +
987
- '<button onclick="copyHealCommand(this)" style="background:rgba(124,58,237,0.15);border:1px solid rgba(124,58,237,0.3);border-radius:6px;color:var(--accent);padding:6px 12px;font-size:11px;cursor:pointer">🛠 Copy heal command</button>' +
988
- '</div>';
989
- html += st.missing.map(function(m){
990
- return '<div class="gotcha-item" style="border-color:var(--red)">' +
991
- '<div class="file">'+escapeHtml(m.filePath)+'</div>' +
992
- '<div class="desc">Referenced by <b style="color:var(--red)">'+m.nodeCount+'</b> node(s) but the file no longer exists.</div>' +
993
- '</div>';
994
- }).join('');
957
+ function copyText(str) {
958
+ if (navigator.clipboard) {
959
+ navigator.clipboard.writeText(str);
995
960
  }
996
-
997
- el.innerHTML = html;
998
- lucide.createIcons();
999
961
  }
1000
962
 
1001
- function copyHealCommand(btn) {
1002
- var cmd = "kuma_memory({ action: 'heal' })";
1003
- if (navigator.clipboard && navigator.clipboard.writeText) {
1004
- navigator.clipboard.writeText(cmd).then(function() {
1005
- if (btn) { btn.textContent = "✅ Copied!"; setTimeout(function(){ btn.textContent = "🛠 Copy heal command"; }, 1500); }
1006
- }).catch(function() {
1007
- if (btn) { btn.textContent = "⚠️ Copy failed"; setTimeout(function(){ btn.textContent = "🛠 Copy heal command"; }, 1500); }
1008
- });
1009
- } else {
1010
- // Fallback for non-secure contexts
1011
- var ta = document.createElement("textarea");
1012
- ta.value = cmd; document.body.appendChild(ta); ta.select();
1013
- try { document.execCommand("copy"); if (btn) { btn.textContent = "✅ Copied!"; setTimeout(function(){ btn.textContent = "🛠 Copy heal command"; }, 1500); } } catch(e) {}
1014
- document.body.removeChild(ta);
1015
- }
963
+ function escapeHtml(str) {
964
+ if (!str) return "";
965
+ return String(str).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
1016
966
  }
1017
967
 
1018
- function setupSearch() {
1019
- var searchTimeout = null;
1020
- document.getElementById("node-search").oninput = function() {
1021
- var self = this;
1022
- if (searchTimeout) clearTimeout(searchTimeout);
1023
- searchTimeout = setTimeout(function() {
1024
- var query = self.value.toLowerCase().trim();
1025
- if (!network || !allNodes) return;
1026
-
1027
- if (!query) {
1028
- resetHighlight();
1029
- return;
1030
- }
1031
-
1032
- var matched = new Set();
1033
- var firstMatchedId = null;
1034
- allNodes.forEach(function(n) {
1035
- if (n.name.toLowerCase().includes(query) || (n.file_path && n.file_path.toLowerCase().includes(query))) {
1036
- matched.add(n.id);
1037
- if (!firstMatchedId) firstMatchedId = n.id;
1038
- }
1039
- });
1040
-
1041
- // Update Nodes
1042
- var nodeBatchUpdates = [];
1043
- network.body.data.nodes.forEach(function(n) {
1044
- var hit = matched.has(n.id);
1045
- nodeBatchUpdates.push({ id: n.id, opacity: hit ? 1.0 : 0.08, font: { color: hit ? '#ffffff' : 'rgba(255,255,255,0.04)', bold: hit } });
1046
- });
1047
- network.body.data.nodes.update(nodeBatchUpdates);
1048
-
1049
- // Update Edges (Dim edges whose endpoints are both not in search results)
1050
- var edgeBatchUpdates = [];
1051
- network.body.data.edges.forEach(function(e) {
1052
- var hit = matched.has(e.from) || matched.has(e.to);
1053
- var def = edgeDefaults[e.id] || {};
1054
- var strokeColor = hit ? (def.color || '#64748b') : 'rgba(100,116,139,0.05)';
1055
- var fontColor = hit ? '#94a3b8' : 'rgba(148,163,184,0.03)';
1056
- edgeBatchUpdates.push({
1057
- id: e.id,
1058
- color: { color: strokeColor, highlight: '#c084fc', hover: '#38bdf8', opacity: hit ? 0.95 : 0.05 },
1059
- font: { color: fontColor, strokeWidth: 0, background: 'transparent' },
1060
- width: hit ? (def.width || 1.5) : 0.2
1061
- });
1062
- });
1063
- network.body.data.edges.update(edgeBatchUpdates);
1064
-
1065
- if (firstMatchedId && matched.size < 10) {
1066
- network.focus(firstMatchedId, { scale: 1.2, animation: { duration: 300 } });
1067
- }
1068
- }, 150);
1069
- };
968
+ function escapeAttr(str) {
969
+ if (!str) return "";
970
+ return String(str).replace(/'/g, "\'");
1070
971
  }
1071
972
 
1072
- // ============ ACTIVITY PANEL — Agent Usage Intensity ============
1073
- // Shows how intensely agents use Kuma: tool calls per day, sessions frequency,
1074
- // and activity heatmap to understand adoption patterns.
1075
-
1076
- function renderActivity(data) {
1077
- var el = document.getElementById("panel-activity");
1078
- if (!el) return;
1079
- var eff = data.efficiency || {};
1080
- var sess = eff.recentSessions || [];
1081
- var totalSessions = eff.sessions || 0;
1082
- var totalToolCalls = eff.toolCalls || 0;
1083
- var totalGotchas = eff.gotchas || 0;
1084
- var totalArchFlows = eff.archFlows || 0;
1085
- var totalDecisions = eff.decisions || 0;
1086
- var totalEdits = eff.metrics ? (eff.metrics.filesEdited || 0) : 0;
1087
-
1088
- var html = '';
1089
-
1090
- // === COPY REPORT BUTTON ===
1091
- html += '<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:10px">';
1092
- html += '<div style="font-size:11px;font-weight:600;color:var(--muted);text-transform:uppercase;letter-spacing:0.5px">🤖 Agent Usage Intensity</div>';
1093
- html += '<button onclick="copyActivityReport()" id="copy-report-btn" style="background:rgba(34,211,238,0.12);border:1px solid rgba(34,211,238,0.25);border-radius:6px;color:var(--accent2);padding:4px 10px;font-size:10px;cursor:pointer;transition:all 0.2s;display:flex;align-items:center;gap:4px"><i data-lucide="copy" style="width:11px;height:11px"></i> Copy Report</button>';
1094
- html += '</div>';
1095
-
1096
- // Intensity score: tool_calls per session
1097
- var avgCallsPerSession = totalSessions > 0 ? (totalToolCalls / totalSessions).toFixed(1) : '0';
1098
- var intensityLevel = totalToolCalls > 100 ? 'High' : totalToolCalls > 30 ? 'Medium' : totalToolCalls > 0 ? 'Low' : 'None';
1099
- var intensityColor = totalToolCalls > 100 ? 'var(--green)' : totalToolCalls > 30 ? 'var(--yellow)' : totalToolCalls > 0 ? 'var(--orange)' : 'var(--muted)';
1100
- var intensityIcon = totalToolCalls > 100 ? '🔥' : totalToolCalls > 30 ? '⚡' : totalToolCalls > 0 ? '💤' : '⚫';
1101
-
1102
- html += '<div class="health-item" style="margin-bottom:12px;border-left:3px solid ' + intensityColor + '">';
1103
- html += '<div style="display:flex;align-items:center;gap:8px;margin-bottom:6px">';
1104
- html += '<span style="font-size:24px">' + intensityIcon + '</span>';
1105
- html += '<div><div style="font-size:16px;font-weight:700;color:' + intensityColor + '">' + intensityLevel + '</div>';
1106
- html += '<div style="font-size:11px;color:var(--muted)">' + avgCallsPerSession + ' calls/session average</div></div>';
1107
- html += '</div>';
1108
- html += '<div style="display:flex;flex-wrap:wrap;gap:6px;margin-top:6px">';
1109
- html += '<span style="font-size:10px;padding:2px 8px;border-radius:4px;background:rgba(34,211,238,0.15);color:var(--accent2)">' + totalToolCalls + ' total calls</span>';
1110
- html += '<span style="font-size:10px;padding:2px 8px;border-radius:4px;background:rgba(52,211,153,0.15);color:var(--green)">' + totalSessions + ' sessions</span>';
1111
- html += '<span style="font-size:10px;padding:2px 8px;border-radius:4px;background:rgba(251,191,36,0.15);color:var(--yellow)">' + totalEdits + ' edits</span>';
1112
- html += '</div>';
1113
- html += '</div>';
1114
-
1115
- // === HEALTH SCORE TREND ===
1116
- html += '<div style="font-size:11px;font-weight:600;color:var(--muted);text-transform:uppercase;letter-spacing:0.5px;margin:10px 0 6px">📈 Activity Summary</div>';
1117
- html += '<div style="display:flex;flex-wrap:wrap;gap:8px;margin-bottom:12px">';
1118
-
1119
- var summaryCard = function(val, label, color) {
1120
- return '<div style="flex:1;min-width:90px;padding:10px;background:var(--bg);border-radius:8px;border:1px solid var(--border);text-align:center">' +
1121
- '<div style="font-size:20px;font-weight:700;color:' + (color || 'var(--accent2)') + '">' + val + '</div>' +
1122
- '<div style="font-size:10px;color:var(--muted);margin-top:2px">' + label + '</div></div>';
1123
- };
1124
-
1125
- html += summaryCard(totalSessions, 'Sessions', 'var(--accent2)');
1126
- html += summaryCard(totalToolCalls, 'Tool Calls', 'var(--accent)');
1127
- html += summaryCard(totalGotchas, 'Gotchas', 'var(--orange)');
1128
- html += summaryCard(totalArchFlows, 'Arch Flows', 'var(--accent)');
1129
- html += summaryCard(totalDecisions, 'Decisions', 'var(--accent2)');
1130
- html += '</div>';
1131
-
1132
- // === RECENT SESSION ACTIVITY (last 8) ===
1133
- if (sess.length) {
1134
- html += '<div style="font-size:11px;font-weight:600;color:var(--muted);text-transform:uppercase;letter-spacing:0.5px;margin:10px 0 6px">🕐 Recent Activity</div>';
1135
- html += '<div style="display:flex;flex-direction:column;gap:4px;margin-bottom:12px">';
1136
-
1137
- var maxCalls = Math.max.apply(null, sess.map(function(s) { return s.toolCalls || 0; }).concat([1]));
1138
-
1139
- sess.forEach(function(s) {
1140
- var calls = s.toolCalls || 0;
1141
- var edits = s.edits || 0;
1142
- var rollbacks = s.rollbacks || 0;
1143
- var failures = s.failures || 0;
1144
- var barWidth = Math.round((calls / maxCalls) * 100);
1145
- var barColor = failures > 0 ? 'var(--red)' : rollbacks > 0 ? 'var(--yellow)' : 'var(--accent)';
1146
- var goal = s.goal || 'No goal';
1147
- var date = s.startedAt ? new Date(s.startedAt * 1000) : null;
1148
- var dateStr = date ? date.toLocaleDateString() + ' ' + date.toLocaleTimeString([], {hour: '2-digit', minute: '2-digit'}) : '-';
1149
-
1150
- html += '<div style="background:var(--bg);border-radius:6px;padding:8px 10px;border:1px solid var(--border)">';
1151
- html += '<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:4px">';
1152
- html += '<span style="font-size:11px;color:var(--text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:180px" title="' + escapeHtml(goal) + '">' + escapeHtml(goal) + '</span>';
1153
- html += '<span style="font-size:10px;color:var(--muted);flex-shrink:0;margin-left:8px">' + dateStr + '</span>';
1154
- html += '</div>';
1155
- // Activity bar
1156
- html += '<div style="height:6px;background:var(--border);border-radius:3px;overflow:hidden;margin-bottom:4px">';
1157
- html += '<div style="height:100%;width:' + barWidth + '%;background:' + barColor + ';border-radius:3px;transition:width 0.3s"></div>';
1158
- html += '</div>';
1159
- // Metrics row
1160
- html += '<div style="display:flex;gap:8px;font-size:10px;color:var(--muted)">';
1161
- html += '<span>🔧 ' + calls + ' calls</span>';
1162
- if (edits > 0) html += '<span>📝 ' + edits + ' edits</span>';
1163
- if (rollbacks > 0) html += '<span style="color:var(--yellow)">↩ ' + rollbacks + '</span>';
1164
- if (failures > 0) html += '<span style="color:var(--red)">❌ ' + failures + '</span>';
1165
- if (s.safetyScore != null) html += '<span style="color:var(--green)">🛡 ' + s.safetyScore + '</span>';
1166
- html += '</div>';
1167
- html += '</div>';
1168
- });
1169
- html += '</div>';
1170
- }
1171
-
1172
- // === EFFICIENCY TRENDS ===
1173
- var successRate = eff.verificationPassRate || 100;
1174
- var passColor = successRate >= 90 ? 'var(--green)' : successRate >= 70 ? 'var(--yellow)' : 'var(--red)';
1175
- var recordingRate = totalToolCalls > 0 ? Math.round(((totalGotchas + totalArchFlows + totalDecisions) / totalToolCalls) * 100) : 0;
1176
- // Rollback rate from recent sessions (rollbacks / calls, %)
1177
- var totalRollbacks = sess.reduce(function(a, s) { return a + (s.rollbacks || 0); }, 0);
1178
- var totalCalls = sess.reduce(function(a, s) { return a + (s.toolCalls || 0); }, 0);
1179
- var rollbackRate = totalCalls > 0 ? Math.round((totalRollbacks / totalCalls) * 100) : 0;
1180
-
1181
- html += '<div style="font-size:11px;font-weight:600;color:var(--muted);text-transform:uppercase;letter-spacing:0.5px;margin:10px 0 6px">📊 Efficiency Trends</div>';
1182
- html += '<div class="health-item" style="margin-bottom:8px">';
1183
- html += '<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:6px">';
1184
- html += '<span style="font-size:12px;color:var(--text)">Verification Pass Rate</span>';
1185
- html += '<span style="font-size:14px;font-weight:700;color:' + passColor + '">' + (successRate || '—') + '%</span>';
1186
- html += '</div>';
1187
- html += '<div style="height:8px;background:var(--border);border-radius:4px;overflow:hidden">';
1188
- html += '<div style="height:100%;width:' + (successRate || 0) + '%;background:' + passColor + ';border-radius:4px"></div>';
1189
- html += '</div>';
1190
- html += '</div>';
1191
-
1192
- html += '<div class="health-item" style="margin-bottom:8px">';
1193
- html += '<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:6px">';
1194
- html += '<span style="font-size:12px;color:var(--text)">Recording Rate</span>';
1195
- html += '<span style="font-size:14px;font-weight:700;color:' + (recordingRate >= 10 ? 'var(--green)' : recordingRate >= 5 ? 'var(--yellow)' : 'var(--orange)') + '">' + recordingRate + '%</span>';
1196
- html += '</div>';
1197
- html += '<div style="height:8px;background:var(--border);border-radius:4px;overflow:hidden">';
1198
- html += '<div style="height:100%;width:' + Math.min(recordingRate, 100) + '%;background:' + (recordingRate >= 10 ? 'var(--green)' : recordingRate >= 5 ? 'var(--yellow)' : 'var(--orange)') + ';border-radius:4px"></div>';
1199
- html += '</div>';
1200
- html += '</div>';
1201
-
1202
- // === INSIGHT ===
1203
- html += '<div style="margin-top:12px;padding:10px 12px;background:rgba(167,139,250,0.1);border:1px solid rgba(167,139,250,0.2);border-radius:8px;font-size:11px;color:var(--accent-bright)">';
1204
- html += '💡 <b>Activity Insight:</b> ';
1205
- if (totalSessions === 0) {
1206
- html += 'No sessions recorded yet. Start using Kuma tools to track activity!';
1207
- } else if (totalToolCalls < 10) {
1208
- html += 'Getting started! Kuma learns more with each tool call.';
1209
- } else if (successRate >= 90) {
1210
- html += 'Excellent! High success rate shows Kuma is well-calibrated for your workflow.';
1211
- } else if (rollbackRate > 10) {
1212
- html += 'High rollback rate — Kuma gotchas may help avoid repeated mistakes.';
1213
- } else {
1214
- html += 'Healthy usage pattern. The more you use Kuma, the smarter it gets!';
1215
- }
1216
- html += '</div>';
1217
-
1218
- el.innerHTML = html;
1219
- lucide.createIcons();
1220
- }
1221
-
1222
- // ============ COPY ACTIVITY REPORT ============
1223
- function copyActivityReport() {
1224
- var btn = document.getElementById('copy-report-btn');
1225
- if (!btn) return;
1226
-
1227
- // Find the data from the last render
1228
- var panel = document.getElementById('panel-activity');
1229
- if (!panel) return;
1230
-
1231
- // Build a clean text report
1232
- var report = [];
1233
- report.push('=== KUMA STUDIO — ACTIVITY REPORT ===');
1234
- report.push('Generated: ' + new Date().toLocaleString());
1235
- report.push('');
1236
-
1237
- // Get all stat cards
1238
- var stats = panel.querySelectorAll('[style*="font-size:20px"]');
1239
- var labels = panel.querySelectorAll('[style*="font-size:10px;color:var(--muted)"]');
1240
-
1241
- // Extract intensity info
1242
- var intensityEl = panel.querySelector('[style*="font-size:16px;font-weight:700"]');
1243
- var intensity = intensityEl ? intensityEl.textContent : 'Unknown';
1244
- var avgCallsEl = panel.querySelector('[style*="font-size:11px;color:var(--muted)"]');
1245
- var avgCalls = avgCallsEl ? avgCallsEl.textContent : '';
1246
-
1247
- report.push('INTENSITY: ' + intensity);
1248
- if (avgCalls) report.push('Average: ' + avgCalls);
1249
- report.push('');
1250
-
1251
- // Get all section headers and content
1252
- var sections = panel.querySelectorAll('[style*="text-transform:uppercase"]');
1253
- sections.forEach(function(section) {
1254
- var title = section.textContent.trim();
1255
- report.push('--- ' + title + ' ---');
1256
-
1257
- // Get next sibling content
1258
- var next = section.nextElementSibling;
1259
- if (next) {
1260
- // Extract text content, cleaning up HTML
1261
- var text = next.textContent.trim()
1262
- .replace(/\s+/g, ' ')
1263
- .replace(/([0-9]+) sessions/g, '$1 sessions')
1264
- .replace(/([0-9]+) tool calls/g, '$1 tool calls')
1265
- .replace(/([0-9]+) file edits/g, '$1 file edits')
1266
- .replace(/([0-9]+) rollbacks/g, '$1 rollbacks')
1267
- .replace(/([0-9]+) failures/g, '$1 failures')
1268
- .replace(/([0-9]+) total calls/g, '$1 total calls');
1269
- report.push(text);
1270
- }
1271
- report.push('');
1272
- });
1273
-
1274
- // Get recent sessions
1275
- var sessionItems = panel.querySelectorAll('[style*="background:var(--bg);border-radius:6px"]');
1276
- if (sessionItems.length > 0) {
1277
- report.push('--- RECENT SESSIONS ---');
1278
- sessionItems.forEach(function(item) {
1279
- var goal = item.querySelector('[style*="font-size:11px"]');
1280
- var metrics = item.querySelector('[style*="font-size:10px"]');
1281
- if (goal) report.push('• ' + goal.textContent.trim());
1282
- if (metrics) report.push(' ' + metrics.textContent.trim());
1283
- });
1284
- report.push('');
1285
- }
1286
-
1287
- // Get efficiency trends
1288
- var trends = panel.querySelectorAll('.health-item');
1289
- if (trends.length > 0) {
1290
- report.push('--- EFFICIENCY TRENDS ---');
1291
- trends.forEach(function(trend) {
1292
- var label = trend.querySelector('[style*="font-size:12px"]');
1293
- var value = trend.querySelector('[style*="font-size:14px"]');
1294
- if (label && value) {
1295
- report.push(label.textContent.trim() + ': ' + value.textContent.trim());
1296
- }
1297
- });
1298
- report.push('');
1299
- }
1300
-
1301
- // Get insight
1302
- var insight = panel.querySelector('[style*="rgba(167,139,250,0.1)"]');
1303
- if (insight) {
1304
- report.push('--- INSIGHT ---');
1305
- report.push(insight.textContent.trim());
1306
- }
1307
-
1308
- var reportText = report.join('\n');
1309
-
1310
- // Copy to clipboard
1311
- navigator.clipboard.writeText(reportText).then(function() {
1312
- btn.innerHTML = '<i data-lucide="check" style="width:11px;height:11px"></i> Copied!';
1313
- btn.style.background = 'rgba(52,211,153,0.15)';
1314
- btn.style.borderColor = 'rgba(52,211,153,0.3)';
1315
- btn.style.color = 'var(--green)';
1316
- lucide.createIcons();
1317
- setTimeout(function() {
1318
- btn.innerHTML = '<i data-lucide="copy" style="width:11px;height:11px"></i> Copy Report';
1319
- btn.style.background = 'rgba(34,211,238,0.12)';
1320
- btn.style.borderColor = 'rgba(34,211,238,0.25)';
1321
- btn.style.color = 'var(--accent2)';
1322
- lucide.createIcons();
1323
- }, 2000);
1324
- }).catch(function() {
1325
- // Fallback: select text for manual copy
1326
- var textarea = document.createElement('textarea');
1327
- textarea.value = reportText;
1328
- textarea.style.position = 'fixed';
1329
- textarea.style.left = '-9999px';
1330
- document.body.appendChild(textarea);
1331
- textarea.select();
1332
- document.execCommand('copy');
1333
- document.body.removeChild(textarea);
1334
- btn.innerHTML = '<i data-lucide="check" style="width:11px;height:11px"></i> Copied!';
1335
- lucide.createIcons();
1336
- setTimeout(function() {
1337
- btn.innerHTML = '<i data-lucide="copy" style="width:11px;height:11px"></i> Copy Report';
1338
- lucide.createIcons();
1339
- }, 2000);
1340
- });
1341
- }
973
+ window.addEventListener("DOMContentLoaded", function() {
974
+ fetchDashboard();
975
+ });
1342
976
  </script>
977
+
1343
978
  </body>
1344
979
  </html>