@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.
- package/.claude-plugin/marketplace.json +17 -0
- package/.claude-plugin/plugin.json +10 -0
- package/INSTALL.md +265 -0
- package/README.md +32 -20
- package/dist/index.js +2354 -542
- package/install.sh +129 -0
- package/mcp_config.json +11 -0
- package/package.json +8 -3
- package/packages/ide/studio/dist/index.js +25 -2
- package/packages/ide/studio/public/index.html +901 -1266
- package/plugin.json +18 -0
|
@@ -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: #
|
|
13
|
-
--
|
|
14
|
-
--
|
|
15
|
-
--
|
|
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
|
-
|
|
19
|
-
font-family:
|
|
20
|
-
background: var(--bg);
|
|
21
|
-
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
.
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
font-size:
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
|
|
513
|
+
|
|
221
514
|
<header>
|
|
222
|
-
<
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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
|
-
<
|
|
247
|
-
<div
|
|
248
|
-
|
|
249
|
-
|
|
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
|
-
<
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
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
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
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
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
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
|
-
|
|
301
|
-
|
|
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
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
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
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
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
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
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
|
-
|
|
352
|
-
|
|
353
|
-
|
|
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
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
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
|
|
370
|
-
|
|
371
|
-
|
|
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
|
|
375
|
-
|
|
376
|
-
|
|
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
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
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
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
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
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
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(
|
|
437
|
-
var
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
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,
|
|
445
|
-
|
|
446
|
-
shape:
|
|
447
|
-
|
|
448
|
-
|
|
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
|
|
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
|
-
|
|
473
|
-
|
|
474
|
-
arrows:
|
|
475
|
-
color: { color:
|
|
476
|
-
|
|
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
|
|
484
|
-
network = new vis.Network(container, {nodes: visNodes, edges: visEdges}, {
|
|
921
|
+
var options = {
|
|
485
922
|
physics: {
|
|
486
|
-
enabled:
|
|
487
|
-
barnesHut: { gravitationalConstant: -3000,
|
|
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:
|
|
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
|
-
|
|
574
|
-
|
|
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
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
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()">×</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
|
|
772
|
-
|
|
773
|
-
}
|
|
774
|
-
|
|
775
|
-
function escapeHtml(str) {
|
|
776
|
-
if (!str) return '';
|
|
777
|
-
return String(str).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
|
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
|
|
853
|
-
|
|
854
|
-
var
|
|
855
|
-
if(
|
|
856
|
-
|
|
857
|
-
|
|
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 · '+ (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 · 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+' · '+s.toolCalls+' calls · '+s.edits+' edits'+(s.failures?' · <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
|
-
|
|
968
|
-
|
|
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
|
|
1002
|
-
|
|
1003
|
-
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
1016
966
|
}
|
|
1017
967
|
|
|
1018
|
-
function
|
|
1019
|
-
|
|
1020
|
-
|
|
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
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
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>
|