@openqa/cli 1.3.4 → 2.1.0
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/README.md +203 -6
- package/dist/agent/brain/diff-analyzer.js +140 -0
- package/dist/agent/brain/diff-analyzer.js.map +1 -0
- package/dist/agent/brain/llm-cache.js +47 -0
- package/dist/agent/brain/llm-cache.js.map +1 -0
- package/dist/agent/brain/llm-resilience.js +252 -0
- package/dist/agent/brain/llm-resilience.js.map +1 -0
- package/dist/agent/config/index.js +588 -0
- package/dist/agent/config/index.js.map +1 -0
- package/dist/agent/coverage/index.js +74 -0
- package/dist/agent/coverage/index.js.map +1 -0
- package/dist/agent/export/index.js +158 -0
- package/dist/agent/export/index.js.map +1 -0
- package/dist/agent/index-v2.js +2795 -0
- package/dist/agent/index-v2.js.map +1 -0
- package/dist/agent/index.js +369 -105
- package/dist/agent/index.js.map +1 -1
- package/dist/agent/logger.js +41 -0
- package/dist/agent/logger.js.map +1 -0
- package/dist/agent/metrics.js +39 -0
- package/dist/agent/metrics.js.map +1 -0
- package/dist/agent/notifications/index.js +106 -0
- package/dist/agent/notifications/index.js.map +1 -0
- package/dist/agent/openapi/spec.js +338 -0
- package/dist/agent/openapi/spec.js.map +1 -0
- package/dist/agent/tools/project-runner.js +481 -0
- package/dist/agent/tools/project-runner.js.map +1 -0
- package/dist/cli/config.html.js +454 -0
- package/dist/cli/daemon.js +8810 -0
- package/dist/cli/dashboard.html.js +1622 -0
- package/dist/cli/env-config.js +391 -0
- package/dist/cli/env-routes.js +820 -0
- package/dist/cli/env.html.js +679 -0
- package/dist/cli/index.js +5980 -1896
- package/dist/cli/kanban.html.js +577 -0
- package/dist/cli/routes.js +895 -0
- package/dist/cli/routes.js.map +1 -0
- package/dist/cli/server.js +5855 -1860
- package/dist/database/index.js +485 -60
- package/dist/database/index.js.map +1 -1
- package/dist/database/sqlite.js +281 -0
- package/dist/database/sqlite.js.map +1 -0
- package/install.sh +19 -10
- package/package.json +19 -5
|
@@ -0,0 +1,1622 @@
|
|
|
1
|
+
// cli/dashboard.html.ts
|
|
2
|
+
function getDashboardHTML() {
|
|
3
|
+
return `<!DOCTYPE html>
|
|
4
|
+
<html lang="en">
|
|
5
|
+
<head>
|
|
6
|
+
<meta charset="UTF-8">
|
|
7
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
8
|
+
<title>OpenQA \u2014 Dashboard</title>
|
|
9
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
10
|
+
<link href="https://fonts.googleapis.com/css2?family=DM+Mono:wght@300;400;500&family=Syne:wght@400;600;700;800&display=swap" rel="stylesheet">
|
|
11
|
+
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
|
12
|
+
<style>
|
|
13
|
+
:root {
|
|
14
|
+
--bg: #080b10;
|
|
15
|
+
--surface: #0d1117;
|
|
16
|
+
--panel: #111720;
|
|
17
|
+
--border: rgba(255,255,255,0.06);
|
|
18
|
+
--border-hi: rgba(255,255,255,0.12);
|
|
19
|
+
--accent: #f97316;
|
|
20
|
+
--accent-lo: rgba(249,115,22,0.08);
|
|
21
|
+
--accent-md: rgba(249,115,22,0.18);
|
|
22
|
+
--green: #22c55e;
|
|
23
|
+
--green-lo: rgba(34,197,94,0.08);
|
|
24
|
+
--red: #ef4444;
|
|
25
|
+
--red-lo: rgba(239,68,68,0.08);
|
|
26
|
+
--amber: #f59e0b;
|
|
27
|
+
--blue: #38bdf8;
|
|
28
|
+
--text-1: #f1f5f9;
|
|
29
|
+
--text-2: #8b98a8;
|
|
30
|
+
--text-3: #4b5563;
|
|
31
|
+
--mono: 'DM Mono', monospace;
|
|
32
|
+
--sans: 'Syne', sans-serif;
|
|
33
|
+
--radius: 10px;
|
|
34
|
+
--radius-lg: 16px;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
38
|
+
|
|
39
|
+
body {
|
|
40
|
+
font-family: var(--sans);
|
|
41
|
+
background: var(--bg);
|
|
42
|
+
color: var(--text-1);
|
|
43
|
+
min-height: 100vh;
|
|
44
|
+
overflow-x: hidden;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/* Layout */
|
|
48
|
+
.shell {
|
|
49
|
+
display: grid;
|
|
50
|
+
grid-template-columns: 220px 1fr;
|
|
51
|
+
min-height: 100vh;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/* Sidebar */
|
|
55
|
+
aside {
|
|
56
|
+
background: var(--surface);
|
|
57
|
+
border-right: 1px solid var(--border);
|
|
58
|
+
display: flex;
|
|
59
|
+
flex-direction: column;
|
|
60
|
+
padding: 28px 0;
|
|
61
|
+
position: sticky;
|
|
62
|
+
top: 0;
|
|
63
|
+
height: 100vh;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.logo {
|
|
67
|
+
display: flex;
|
|
68
|
+
align-items: center;
|
|
69
|
+
gap: 10px;
|
|
70
|
+
padding: 0 24px 32px;
|
|
71
|
+
border-bottom: 1px solid var(--border);
|
|
72
|
+
margin-bottom: 12px;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.logo-mark {
|
|
76
|
+
width: 34px;
|
|
77
|
+
height: 34px;
|
|
78
|
+
background: var(--accent);
|
|
79
|
+
border-radius: 8px;
|
|
80
|
+
display: grid;
|
|
81
|
+
place-items: center;
|
|
82
|
+
font-size: 16px;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.logo-name {
|
|
86
|
+
font-weight: 800;
|
|
87
|
+
font-size: 18px;
|
|
88
|
+
letter-spacing: -0.5px;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.logo-version {
|
|
92
|
+
font-family: var(--mono);
|
|
93
|
+
font-size: 10px;
|
|
94
|
+
color: var(--text-3);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.nav-section { padding: 8px 12px; flex: 1; }
|
|
98
|
+
|
|
99
|
+
.nav-label {
|
|
100
|
+
font-family: var(--mono);
|
|
101
|
+
font-size: 10px;
|
|
102
|
+
color: var(--text-3);
|
|
103
|
+
letter-spacing: 1.5px;
|
|
104
|
+
text-transform: uppercase;
|
|
105
|
+
padding: 0 12px;
|
|
106
|
+
margin: 16px 0 6px;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.nav-item {
|
|
110
|
+
display: flex;
|
|
111
|
+
align-items: center;
|
|
112
|
+
gap: 10px;
|
|
113
|
+
padding: 9px 12px;
|
|
114
|
+
border-radius: var(--radius);
|
|
115
|
+
color: var(--text-2);
|
|
116
|
+
text-decoration: none;
|
|
117
|
+
font-size: 14px;
|
|
118
|
+
font-weight: 600;
|
|
119
|
+
transition: all 0.15s ease;
|
|
120
|
+
cursor: pointer;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.nav-item:hover { color: var(--text-1); background: var(--panel); }
|
|
124
|
+
.nav-item.active { color: var(--accent); background: var(--accent-lo); }
|
|
125
|
+
.nav-item .icon { font-size: 15px; width: 20px; text-align: center; }
|
|
126
|
+
.nav-item .badge {
|
|
127
|
+
margin-left: auto;
|
|
128
|
+
background: var(--accent);
|
|
129
|
+
color: white;
|
|
130
|
+
font-size: 10px;
|
|
131
|
+
padding: 2px 6px;
|
|
132
|
+
border-radius: 10px;
|
|
133
|
+
font-weight: 700;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
.sidebar-footer {
|
|
137
|
+
padding: 16px 24px;
|
|
138
|
+
border-top: 1px solid var(--border);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
.status-pill {
|
|
142
|
+
display: flex;
|
|
143
|
+
align-items: center;
|
|
144
|
+
gap: 8px;
|
|
145
|
+
font-family: var(--mono);
|
|
146
|
+
font-size: 11px;
|
|
147
|
+
color: var(--text-2);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
.dot {
|
|
151
|
+
width: 7px;
|
|
152
|
+
height: 7px;
|
|
153
|
+
border-radius: 50%;
|
|
154
|
+
background: var(--green);
|
|
155
|
+
box-shadow: 0 0 8px var(--green);
|
|
156
|
+
}
|
|
157
|
+
.dot.disconnected { background: var(--red); box-shadow: 0 0 8px var(--red); }
|
|
158
|
+
|
|
159
|
+
/* Main */
|
|
160
|
+
main {
|
|
161
|
+
display: flex;
|
|
162
|
+
flex-direction: column;
|
|
163
|
+
min-height: 100vh;
|
|
164
|
+
overflow-y: auto;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
.topbar {
|
|
168
|
+
display: flex;
|
|
169
|
+
align-items: center;
|
|
170
|
+
justify-content: space-between;
|
|
171
|
+
padding: 20px 32px;
|
|
172
|
+
border-bottom: 1px solid var(--border);
|
|
173
|
+
background: var(--surface);
|
|
174
|
+
position: sticky;
|
|
175
|
+
top: 0;
|
|
176
|
+
z-index: 10;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
.page-title { font-size: 15px; font-weight: 700; letter-spacing: -0.2px; }
|
|
180
|
+
.page-breadcrumb { font-family: var(--mono); font-size: 11px; color: var(--text-3); margin-top: 2px; }
|
|
181
|
+
|
|
182
|
+
.topbar-actions { display: flex; align-items: center; gap: 12px; }
|
|
183
|
+
|
|
184
|
+
.btn-sm {
|
|
185
|
+
font-family: var(--sans);
|
|
186
|
+
font-weight: 700;
|
|
187
|
+
font-size: 12px;
|
|
188
|
+
padding: 8px 16px;
|
|
189
|
+
border-radius: 8px;
|
|
190
|
+
border: none;
|
|
191
|
+
cursor: pointer;
|
|
192
|
+
transition: all 0.15s ease;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
.btn-ghost {
|
|
196
|
+
background: var(--panel);
|
|
197
|
+
color: var(--text-2);
|
|
198
|
+
border: 1px solid var(--border);
|
|
199
|
+
}
|
|
200
|
+
.btn-ghost:hover { border-color: var(--border-hi); color: var(--text-1); }
|
|
201
|
+
|
|
202
|
+
.btn-primary {
|
|
203
|
+
background: var(--accent);
|
|
204
|
+
color: #fff;
|
|
205
|
+
display: flex;
|
|
206
|
+
align-items: center;
|
|
207
|
+
gap: 6px;
|
|
208
|
+
}
|
|
209
|
+
.btn-primary:hover { background: #ea580c; box-shadow: 0 0 20px rgba(249,115,22,0.35); }
|
|
210
|
+
|
|
211
|
+
/* Content */
|
|
212
|
+
.content {
|
|
213
|
+
padding: 28px 32px;
|
|
214
|
+
display: flex;
|
|
215
|
+
flex-direction: column;
|
|
216
|
+
gap: 24px;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/* Metrics Grid */
|
|
220
|
+
.metrics-grid {
|
|
221
|
+
display: grid;
|
|
222
|
+
grid-template-columns: repeat(4, 1fr);
|
|
223
|
+
gap: 16px;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
.metric-card {
|
|
227
|
+
background: var(--panel);
|
|
228
|
+
border: 1px solid var(--border);
|
|
229
|
+
border-radius: var(--radius-lg);
|
|
230
|
+
padding: 20px;
|
|
231
|
+
position: relative;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
.metric-header {
|
|
235
|
+
display: flex;
|
|
236
|
+
justify-content: space-between;
|
|
237
|
+
align-items: flex-start;
|
|
238
|
+
margin-bottom: 12px;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
.metric-label {
|
|
242
|
+
font-family: var(--mono);
|
|
243
|
+
font-size: 11px;
|
|
244
|
+
color: var(--text-3);
|
|
245
|
+
text-transform: uppercase;
|
|
246
|
+
letter-spacing: 0.5px;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
.metric-icon {
|
|
250
|
+
width: 32px;
|
|
251
|
+
height: 32px;
|
|
252
|
+
background: var(--accent-lo);
|
|
253
|
+
border-radius: 8px;
|
|
254
|
+
display: flex;
|
|
255
|
+
align-items: center;
|
|
256
|
+
justify-content: center;
|
|
257
|
+
font-size: 16px;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
.metric-value {
|
|
261
|
+
font-size: 36px;
|
|
262
|
+
font-weight: 800;
|
|
263
|
+
color: var(--text-1);
|
|
264
|
+
line-height: 1;
|
|
265
|
+
margin-bottom: 8px;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
.metric-change {
|
|
269
|
+
font-family: var(--mono);
|
|
270
|
+
font-size: 11px;
|
|
271
|
+
display: flex;
|
|
272
|
+
align-items: center;
|
|
273
|
+
gap: 4px;
|
|
274
|
+
}
|
|
275
|
+
.metric-change.positive { color: var(--green); }
|
|
276
|
+
.metric-change.negative { color: var(--red); }
|
|
277
|
+
|
|
278
|
+
/* Main Grid */
|
|
279
|
+
.main-grid {
|
|
280
|
+
display: grid;
|
|
281
|
+
grid-template-columns: 1.2fr 1fr;
|
|
282
|
+
gap: 24px;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/* Panel */
|
|
286
|
+
.panel {
|
|
287
|
+
background: var(--panel);
|
|
288
|
+
border: 1px solid var(--border);
|
|
289
|
+
border-radius: var(--radius-lg);
|
|
290
|
+
overflow: hidden;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
.panel-head {
|
|
294
|
+
display: flex;
|
|
295
|
+
align-items: center;
|
|
296
|
+
justify-content: space-between;
|
|
297
|
+
padding: 16px 20px;
|
|
298
|
+
border-bottom: 1px solid var(--border);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
.panel-title {
|
|
302
|
+
font-size: 13px;
|
|
303
|
+
font-weight: 700;
|
|
304
|
+
letter-spacing: -0.1px;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
.panel-badge {
|
|
308
|
+
font-family: var(--mono);
|
|
309
|
+
font-size: 10px;
|
|
310
|
+
color: var(--text-3);
|
|
311
|
+
background: var(--surface);
|
|
312
|
+
padding: 4px 8px;
|
|
313
|
+
border-radius: 6px;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
.panel-body { padding: 20px; }
|
|
317
|
+
|
|
318
|
+
/* Tabs */
|
|
319
|
+
.tabs {
|
|
320
|
+
display: flex;
|
|
321
|
+
gap: 4px;
|
|
322
|
+
background: var(--surface);
|
|
323
|
+
padding: 4px;
|
|
324
|
+
border-radius: 8px;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
.tab {
|
|
328
|
+
padding: 6px 14px;
|
|
329
|
+
background: transparent;
|
|
330
|
+
border: none;
|
|
331
|
+
border-radius: 6px;
|
|
332
|
+
color: var(--text-3);
|
|
333
|
+
font-size: 12px;
|
|
334
|
+
font-weight: 600;
|
|
335
|
+
cursor: pointer;
|
|
336
|
+
transition: all 0.15s ease;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
.tab.active { background: var(--panel); color: var(--text-1); }
|
|
340
|
+
.tab:hover:not(.active) { color: var(--text-2); }
|
|
341
|
+
|
|
342
|
+
/* Chart */
|
|
343
|
+
.chart-container {
|
|
344
|
+
height: 220px;
|
|
345
|
+
position: relative;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/* Agent Hierarchy */
|
|
349
|
+
.hierarchy-container {
|
|
350
|
+
height: 300px;
|
|
351
|
+
position: relative;
|
|
352
|
+
background: var(--surface);
|
|
353
|
+
border-radius: var(--radius);
|
|
354
|
+
overflow: hidden;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
.hierarchy-badge {
|
|
358
|
+
position: absolute;
|
|
359
|
+
top: 12px;
|
|
360
|
+
right: 12px;
|
|
361
|
+
font-family: var(--mono);
|
|
362
|
+
font-size: 10px;
|
|
363
|
+
color: var(--green);
|
|
364
|
+
background: var(--green-lo);
|
|
365
|
+
padding: 4px 8px;
|
|
366
|
+
border-radius: 6px;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/* Agent Node */
|
|
370
|
+
.agent-node {
|
|
371
|
+
position: absolute;
|
|
372
|
+
padding: 8px 16px;
|
|
373
|
+
border-radius: 8px;
|
|
374
|
+
font-size: 12px;
|
|
375
|
+
font-weight: 600;
|
|
376
|
+
display: flex;
|
|
377
|
+
align-items: center;
|
|
378
|
+
gap: 6px;
|
|
379
|
+
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
|
|
380
|
+
cursor: pointer;
|
|
381
|
+
transition: transform 0.15s ease, box-shadow 0.15s ease;
|
|
382
|
+
}
|
|
383
|
+
.agent-node:hover { transform: scale(1.05); box-shadow: 0 6px 20px rgba(0,0,0,0.4); }
|
|
384
|
+
|
|
385
|
+
.agent-node.main { background: var(--accent); color: white; }
|
|
386
|
+
.agent-node.browser { background: var(--green); color: white; }
|
|
387
|
+
.agent-node.api { background: var(--amber); color: white; }
|
|
388
|
+
.agent-node.auth { background: var(--red); color: white; }
|
|
389
|
+
.agent-node.ui { background: #8b5cf6; color: white; }
|
|
390
|
+
.agent-node.perf { background: #06b6d4; color: white; }
|
|
391
|
+
.agent-node.security { background: #ec4899; color: white; }
|
|
392
|
+
|
|
393
|
+
/* Bottom Grid */
|
|
394
|
+
.bottom-grid {
|
|
395
|
+
display: grid;
|
|
396
|
+
grid-template-columns: 1fr 1fr;
|
|
397
|
+
gap: 24px;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/* Table */
|
|
401
|
+
.table-header {
|
|
402
|
+
display: grid;
|
|
403
|
+
grid-template-columns: 2fr 1fr 1fr 1fr;
|
|
404
|
+
gap: 16px;
|
|
405
|
+
padding: 12px 20px;
|
|
406
|
+
font-family: var(--mono);
|
|
407
|
+
font-size: 10px;
|
|
408
|
+
color: var(--text-3);
|
|
409
|
+
text-transform: uppercase;
|
|
410
|
+
letter-spacing: 0.5px;
|
|
411
|
+
border-bottom: 1px solid var(--border);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
.table-row {
|
|
415
|
+
display: grid;
|
|
416
|
+
grid-template-columns: 2fr 1fr 1fr 1fr;
|
|
417
|
+
gap: 16px;
|
|
418
|
+
padding: 14px 20px;
|
|
419
|
+
font-size: 13px;
|
|
420
|
+
border-bottom: 1px solid var(--border);
|
|
421
|
+
transition: background 0.15s ease;
|
|
422
|
+
}
|
|
423
|
+
.table-row:hover { background: var(--surface); }
|
|
424
|
+
.table-row:last-child { border-bottom: none; }
|
|
425
|
+
|
|
426
|
+
.agent-name { font-weight: 600; }
|
|
427
|
+
|
|
428
|
+
.status-badge {
|
|
429
|
+
font-family: var(--mono);
|
|
430
|
+
font-size: 10px;
|
|
431
|
+
padding: 4px 8px;
|
|
432
|
+
border-radius: 6px;
|
|
433
|
+
text-transform: uppercase;
|
|
434
|
+
font-weight: 600;
|
|
435
|
+
}
|
|
436
|
+
.status-badge.running { background: var(--green-lo); color: var(--green); }
|
|
437
|
+
.status-badge.idle { background: var(--accent-lo); color: var(--amber); }
|
|
438
|
+
|
|
439
|
+
.empty-state {
|
|
440
|
+
padding: 40px 20px;
|
|
441
|
+
text-align: center;
|
|
442
|
+
color: var(--text-3);
|
|
443
|
+
font-size: 13px;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/* Activity */
|
|
447
|
+
.activity-list { max-height: 280px; overflow-y: auto; }
|
|
448
|
+
|
|
449
|
+
.activity-item {
|
|
450
|
+
display: flex;
|
|
451
|
+
align-items: flex-start;
|
|
452
|
+
gap: 12px;
|
|
453
|
+
padding: 14px 20px;
|
|
454
|
+
border-bottom: 1px solid var(--border);
|
|
455
|
+
transition: background 0.15s ease;
|
|
456
|
+
}
|
|
457
|
+
.activity-item:hover { background: var(--surface); }
|
|
458
|
+
.activity-item:last-child { border-bottom: none; }
|
|
459
|
+
|
|
460
|
+
.activity-dot {
|
|
461
|
+
width: 8px;
|
|
462
|
+
height: 8px;
|
|
463
|
+
border-radius: 50%;
|
|
464
|
+
margin-top: 6px;
|
|
465
|
+
flex-shrink: 0;
|
|
466
|
+
}
|
|
467
|
+
.activity-dot.info { background: var(--blue); }
|
|
468
|
+
.activity-dot.success { background: var(--green); }
|
|
469
|
+
.activity-dot.warning { background: var(--amber); }
|
|
470
|
+
.activity-dot.error { background: var(--red); }
|
|
471
|
+
|
|
472
|
+
.activity-content { flex: 1; }
|
|
473
|
+
.activity-message { font-size: 13px; font-weight: 500; margin-bottom: 4px; }
|
|
474
|
+
.activity-time { font-family: var(--mono); font-size: 11px; color: var(--text-3); }
|
|
475
|
+
|
|
476
|
+
/* Triple Grid */
|
|
477
|
+
.triple-grid {
|
|
478
|
+
display: grid;
|
|
479
|
+
grid-template-columns: repeat(3, 1fr);
|
|
480
|
+
gap: 24px;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/* Task List */
|
|
484
|
+
.task-list, .issue-list, .intervention-list {
|
|
485
|
+
max-height: 250px;
|
|
486
|
+
overflow-y: auto;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
.task-item, .issue-item {
|
|
490
|
+
display: flex;
|
|
491
|
+
align-items: flex-start;
|
|
492
|
+
gap: 12px;
|
|
493
|
+
padding: 14px 20px;
|
|
494
|
+
border-bottom: 1px solid var(--border);
|
|
495
|
+
transition: background 0.15s ease;
|
|
496
|
+
}
|
|
497
|
+
.task-item:hover, .issue-item:hover { background: var(--surface); }
|
|
498
|
+
.task-item:last-child, .issue-item:last-child { border-bottom: none; }
|
|
499
|
+
|
|
500
|
+
.task-icon, .issue-icon {
|
|
501
|
+
width: 28px;
|
|
502
|
+
height: 28px;
|
|
503
|
+
border-radius: 6px;
|
|
504
|
+
display: flex;
|
|
505
|
+
align-items: center;
|
|
506
|
+
justify-content: center;
|
|
507
|
+
font-size: 12px;
|
|
508
|
+
flex-shrink: 0;
|
|
509
|
+
}
|
|
510
|
+
.task-icon { background: var(--accent-lo); }
|
|
511
|
+
.issue-icon.critical { background: var(--red-lo); }
|
|
512
|
+
.issue-icon.high { background: rgba(249,115,22,0.15); }
|
|
513
|
+
.issue-icon.medium { background: rgba(245,158,11,0.15); }
|
|
514
|
+
.issue-icon.low { background: var(--green-lo); }
|
|
515
|
+
|
|
516
|
+
.task-content, .issue-content { flex: 1; min-width: 0; }
|
|
517
|
+
.task-name, .issue-title {
|
|
518
|
+
font-size: 13px;
|
|
519
|
+
font-weight: 600;
|
|
520
|
+
margin-bottom: 4px;
|
|
521
|
+
white-space: nowrap;
|
|
522
|
+
overflow: hidden;
|
|
523
|
+
text-overflow: ellipsis;
|
|
524
|
+
}
|
|
525
|
+
.task-meta, .issue-meta {
|
|
526
|
+
font-family: var(--mono);
|
|
527
|
+
font-size: 11px;
|
|
528
|
+
color: var(--text-3);
|
|
529
|
+
display: flex;
|
|
530
|
+
gap: 12px;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
.task-progress {
|
|
534
|
+
width: 60px;
|
|
535
|
+
height: 6px;
|
|
536
|
+
background: var(--surface);
|
|
537
|
+
border-radius: 3px;
|
|
538
|
+
overflow: hidden;
|
|
539
|
+
}
|
|
540
|
+
.task-progress-fill {
|
|
541
|
+
height: 100%;
|
|
542
|
+
background: var(--accent);
|
|
543
|
+
transition: width 0.3s ease;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
.severity-badge {
|
|
547
|
+
font-family: var(--mono);
|
|
548
|
+
font-size: 9px;
|
|
549
|
+
padding: 2px 6px;
|
|
550
|
+
border-radius: 4px;
|
|
551
|
+
text-transform: uppercase;
|
|
552
|
+
font-weight: 700;
|
|
553
|
+
}
|
|
554
|
+
.severity-badge.critical { background: var(--red); color: white; }
|
|
555
|
+
.severity-badge.high { background: var(--accent); color: white; }
|
|
556
|
+
.severity-badge.medium { background: var(--amber); color: white; }
|
|
557
|
+
.severity-badge.low { background: var(--green); color: white; }
|
|
558
|
+
|
|
559
|
+
/* Intervention */
|
|
560
|
+
.intervention-item {
|
|
561
|
+
padding: 16px 20px;
|
|
562
|
+
border-bottom: 1px solid var(--border);
|
|
563
|
+
background: linear-gradient(135deg, rgba(239,68,68,0.05), transparent);
|
|
564
|
+
}
|
|
565
|
+
.intervention-item:last-child { border-bottom: none; }
|
|
566
|
+
|
|
567
|
+
.intervention-header {
|
|
568
|
+
display: flex;
|
|
569
|
+
align-items: center;
|
|
570
|
+
gap: 10px;
|
|
571
|
+
margin-bottom: 10px;
|
|
572
|
+
}
|
|
573
|
+
.intervention-icon {
|
|
574
|
+
width: 32px;
|
|
575
|
+
height: 32px;
|
|
576
|
+
background: var(--red-lo);
|
|
577
|
+
border-radius: 8px;
|
|
578
|
+
display: flex;
|
|
579
|
+
align-items: center;
|
|
580
|
+
justify-content: center;
|
|
581
|
+
font-size: 16px;
|
|
582
|
+
}
|
|
583
|
+
.intervention-title {
|
|
584
|
+
font-size: 14px;
|
|
585
|
+
font-weight: 700;
|
|
586
|
+
flex: 1;
|
|
587
|
+
}
|
|
588
|
+
.intervention-desc {
|
|
589
|
+
font-size: 13px;
|
|
590
|
+
color: var(--text-2);
|
|
591
|
+
margin-bottom: 12px;
|
|
592
|
+
line-height: 1.5;
|
|
593
|
+
}
|
|
594
|
+
.intervention-actions {
|
|
595
|
+
display: flex;
|
|
596
|
+
gap: 8px;
|
|
597
|
+
}
|
|
598
|
+
.intervention-badge {
|
|
599
|
+
background: var(--red) !important;
|
|
600
|
+
animation: pulse-red 2s infinite;
|
|
601
|
+
}
|
|
602
|
+
@keyframes pulse-red {
|
|
603
|
+
0%, 100% { opacity: 1; }
|
|
604
|
+
50% { opacity: 0.6; }
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
/* Session Footer */
|
|
608
|
+
.session-footer {
|
|
609
|
+
display: flex;
|
|
610
|
+
align-items: center;
|
|
611
|
+
justify-content: space-between;
|
|
612
|
+
padding: 16px 24px;
|
|
613
|
+
background: var(--panel);
|
|
614
|
+
border: 1px solid var(--border);
|
|
615
|
+
border-radius: var(--radius-lg);
|
|
616
|
+
margin-top: 8px;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
.session-info {
|
|
620
|
+
display: flex;
|
|
621
|
+
gap: 32px;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
.session-item {
|
|
625
|
+
display: flex;
|
|
626
|
+
flex-direction: column;
|
|
627
|
+
gap: 4px;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
.session-label {
|
|
631
|
+
font-family: var(--mono);
|
|
632
|
+
font-size: 10px;
|
|
633
|
+
color: var(--text-3);
|
|
634
|
+
text-transform: uppercase;
|
|
635
|
+
letter-spacing: 0.5px;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
.session-value {
|
|
639
|
+
font-size: 13px;
|
|
640
|
+
font-weight: 600;
|
|
641
|
+
color: var(--text-1);
|
|
642
|
+
display: flex;
|
|
643
|
+
align-items: center;
|
|
644
|
+
gap: 6px;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
.status-indicator {
|
|
648
|
+
font-size: 10px;
|
|
649
|
+
}
|
|
650
|
+
.status-indicator.running { color: var(--green); animation: blink 1s infinite; }
|
|
651
|
+
.status-indicator.idle { color: var(--amber); }
|
|
652
|
+
.status-indicator.error { color: var(--red); }
|
|
653
|
+
|
|
654
|
+
@keyframes blink {
|
|
655
|
+
0%, 100% { opacity: 1; }
|
|
656
|
+
50% { opacity: 0.4; }
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
.session-actions {
|
|
660
|
+
display: flex;
|
|
661
|
+
gap: 8px;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
.btn-danger {
|
|
665
|
+
background: var(--red);
|
|
666
|
+
color: white;
|
|
667
|
+
}
|
|
668
|
+
.btn-danger:hover:not(:disabled) { background: #dc2626; }
|
|
669
|
+
.btn-danger:disabled, .btn-ghost:disabled {
|
|
670
|
+
opacity: 0.5;
|
|
671
|
+
cursor: not-allowed;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
/* Responsive */
|
|
675
|
+
@media (max-width: 1400px) {
|
|
676
|
+
.triple-grid { grid-template-columns: 1fr 1fr; }
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
@media (max-width: 1200px) {
|
|
680
|
+
.metrics-grid { grid-template-columns: repeat(2, 1fr); }
|
|
681
|
+
.main-grid { grid-template-columns: 1fr; }
|
|
682
|
+
.bottom-grid { grid-template-columns: 1fr; }
|
|
683
|
+
.triple-grid { grid-template-columns: 1fr; }
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
@media (max-width: 900px) {
|
|
687
|
+
.shell { grid-template-columns: 1fr; }
|
|
688
|
+
aside { display: none; }
|
|
689
|
+
.session-footer { flex-direction: column; gap: 16px; }
|
|
690
|
+
.session-info { flex-wrap: wrap; gap: 16px; }
|
|
691
|
+
}
|
|
692
|
+
</style>
|
|
693
|
+
</head>
|
|
694
|
+
<body>
|
|
695
|
+
|
|
696
|
+
<div class="shell">
|
|
697
|
+
<!-- Sidebar -->
|
|
698
|
+
<aside>
|
|
699
|
+
<div class="logo">
|
|
700
|
+
<div class="logo-mark">\u{1F52C}</div>
|
|
701
|
+
<div>
|
|
702
|
+
<div class="logo-name">OpenQA</div>
|
|
703
|
+
<div class="logo-version">v2.1.0 \xB7 OSS</div>
|
|
704
|
+
</div>
|
|
705
|
+
</div>
|
|
706
|
+
|
|
707
|
+
<div class="nav-section">
|
|
708
|
+
<div class="nav-label">Overview</div>
|
|
709
|
+
<a class="nav-item active" href="/">
|
|
710
|
+
<span class="icon">\u25A6</span> Dashboard
|
|
711
|
+
</a>
|
|
712
|
+
<a class="nav-item" href="/kanban">
|
|
713
|
+
<span class="icon">\u229E</span> Kanban
|
|
714
|
+
<span class="badge" id="kanban-count">0</span>
|
|
715
|
+
</a>
|
|
716
|
+
|
|
717
|
+
<div class="nav-label">Agents</div>
|
|
718
|
+
<a class="nav-item" href="javascript:void(0)" onclick="scrollToSection('agents-table')">
|
|
719
|
+
<span class="icon">\u25CE</span> Active Agents
|
|
720
|
+
</a>
|
|
721
|
+
<a class="nav-item" href="javascript:void(0)" onclick="switchAgentTab('specialists'); scrollToSection('agents-table')">
|
|
722
|
+
<span class="icon">\u25C7</span> Specialists
|
|
723
|
+
</a>
|
|
724
|
+
<a class="nav-item" href="javascript:void(0)" onclick="scrollToSection('interventions-panel')">
|
|
725
|
+
<span class="icon">\u26A0</span> Interventions
|
|
726
|
+
<span class="badge" id="intervention-count" style="background: var(--red);">0</span>
|
|
727
|
+
</a>
|
|
728
|
+
|
|
729
|
+
<div class="nav-label">Analysis</div>
|
|
730
|
+
<a class="nav-item" href="javascript:void(0)" onclick="scrollToSection('issues-panel')">
|
|
731
|
+
<span class="icon">\u{1F41B}</span> Bug Reports
|
|
732
|
+
</a>
|
|
733
|
+
<a class="nav-item" href="javascript:void(0)" onclick="switchChartTab('performance'); scrollToSection('chart-performance')">
|
|
734
|
+
<span class="icon">\u26A1</span> Performance
|
|
735
|
+
</a>
|
|
736
|
+
<a class="nav-item" href="javascript:void(0)" onclick="scrollToSection('activity-list')">
|
|
737
|
+
<span class="icon">\u{1F4CB}</span> Logs
|
|
738
|
+
</a>
|
|
739
|
+
|
|
740
|
+
<div class="nav-label">System</div>
|
|
741
|
+
<a class="nav-item" href="/config">
|
|
742
|
+
<span class="icon">\u2699</span> Config
|
|
743
|
+
</a>
|
|
744
|
+
<a class="nav-item" href="/config/env">
|
|
745
|
+
<span class="icon">\u{1F527}</span> Environment
|
|
746
|
+
</a>
|
|
747
|
+
</div>
|
|
748
|
+
|
|
749
|
+
<div class="sidebar-footer">
|
|
750
|
+
<div class="status-pill">
|
|
751
|
+
<div class="dot" id="connection-dot"></div>
|
|
752
|
+
<span id="connection-text">Connected</span>
|
|
753
|
+
</div>
|
|
754
|
+
</div>
|
|
755
|
+
</aside>
|
|
756
|
+
|
|
757
|
+
<!-- Main -->
|
|
758
|
+
<main>
|
|
759
|
+
<div class="topbar">
|
|
760
|
+
<div>
|
|
761
|
+
<div class="page-title">Dashboard</div>
|
|
762
|
+
<div class="page-breadcrumb">openqa / overview / today</div>
|
|
763
|
+
</div>
|
|
764
|
+
<div class="topbar-actions">
|
|
765
|
+
<button class="btn-sm btn-ghost">Export</button>
|
|
766
|
+
<button class="btn-sm btn-ghost" onclick="location.reload()">\u21BB Refresh</button>
|
|
767
|
+
<button class="btn-sm btn-primary" onclick="startSession()">\u25B6 Run Session</button>
|
|
768
|
+
</div>
|
|
769
|
+
</div>
|
|
770
|
+
|
|
771
|
+
<div class="content">
|
|
772
|
+
<!-- Metrics -->
|
|
773
|
+
<div class="metrics-grid">
|
|
774
|
+
<div class="metric-card">
|
|
775
|
+
<div class="metric-header">
|
|
776
|
+
<div class="metric-label">Active Agents</div>
|
|
777
|
+
<div class="metric-icon">\u{1F916}</div>
|
|
778
|
+
</div>
|
|
779
|
+
<div class="metric-value" id="active-agents">0</div>
|
|
780
|
+
<div class="metric-change positive" id="agents-change">\u2191 0 from last hour</div>
|
|
781
|
+
</div>
|
|
782
|
+
<div class="metric-card">
|
|
783
|
+
<div class="metric-header">
|
|
784
|
+
<div class="metric-label">Total Actions</div>
|
|
785
|
+
<div class="metric-icon">\u26A1</div>
|
|
786
|
+
</div>
|
|
787
|
+
<div class="metric-value" id="total-actions">0</div>
|
|
788
|
+
<div class="metric-change positive" id="actions-change">\u2191 0% this session</div>
|
|
789
|
+
</div>
|
|
790
|
+
<div class="metric-card">
|
|
791
|
+
<div class="metric-header">
|
|
792
|
+
<div class="metric-label">Bugs Found</div>
|
|
793
|
+
<div class="metric-icon">\u{1F41B}</div>
|
|
794
|
+
</div>
|
|
795
|
+
<div class="metric-value" id="bugs-found">0</div>
|
|
796
|
+
<div class="metric-change negative" id="bugs-change">\u2193 0 from yesterday</div>
|
|
797
|
+
</div>
|
|
798
|
+
<div class="metric-card">
|
|
799
|
+
<div class="metric-header">
|
|
800
|
+
<div class="metric-label">Success Rate</div>
|
|
801
|
+
<div class="metric-icon">\u2713</div>
|
|
802
|
+
</div>
|
|
803
|
+
<div class="metric-value" id="success-rate">\u2014</div>
|
|
804
|
+
<div class="metric-change positive" id="rate-change">\u2191 0 pts improvement</div>
|
|
805
|
+
</div>
|
|
806
|
+
</div>
|
|
807
|
+
|
|
808
|
+
<!-- Charts & Hierarchy -->
|
|
809
|
+
<div class="main-grid">
|
|
810
|
+
<div class="panel">
|
|
811
|
+
<div class="panel-head">
|
|
812
|
+
<span class="panel-title">Performance Metrics</span>
|
|
813
|
+
<div class="tabs">
|
|
814
|
+
<button class="tab active" onclick="switchChartTab('performance')">Performance</button>
|
|
815
|
+
<button class="tab" onclick="switchChartTab('activity')">Activity</button>
|
|
816
|
+
<button class="tab" onclick="switchChartTab('errors')">Error rate</button>
|
|
817
|
+
</div>
|
|
818
|
+
</div>
|
|
819
|
+
<div class="panel-body">
|
|
820
|
+
<div class="chart-container" id="chart-performance">
|
|
821
|
+
<canvas id="performanceChart"></canvas>
|
|
822
|
+
</div>
|
|
823
|
+
<div class="chart-container" id="chart-activity" style="display:none;">
|
|
824
|
+
<canvas id="activityChart"></canvas>
|
|
825
|
+
</div>
|
|
826
|
+
<div class="chart-container" id="chart-errors" style="display:none;">
|
|
827
|
+
<canvas id="errorChart"></canvas>
|
|
828
|
+
</div>
|
|
829
|
+
</div>
|
|
830
|
+
</div>
|
|
831
|
+
|
|
832
|
+
<div class="panel">
|
|
833
|
+
<div class="panel-head">
|
|
834
|
+
<span class="panel-title">Agent Hierarchy</span>
|
|
835
|
+
<span class="panel-badge">live</span>
|
|
836
|
+
</div>
|
|
837
|
+
<div class="panel-body">
|
|
838
|
+
<div class="hierarchy-container" id="hierarchy-container">
|
|
839
|
+
<div class="hierarchy-badge">\u25CF Main Agent</div>
|
|
840
|
+
<!-- SVG hierarchy will be rendered here -->
|
|
841
|
+
</div>
|
|
842
|
+
</div>
|
|
843
|
+
</div>
|
|
844
|
+
</div>
|
|
845
|
+
|
|
846
|
+
<!-- Agents & Activity -->
|
|
847
|
+
<div class="bottom-grid">
|
|
848
|
+
<div class="panel">
|
|
849
|
+
<div class="panel-head">
|
|
850
|
+
<span class="panel-title">Active Agents</span>
|
|
851
|
+
<div class="tabs">
|
|
852
|
+
<button class="tab active" onclick="switchAgentTab('agents')">Agents</button>
|
|
853
|
+
<button class="tab" onclick="switchAgentTab('specialists')">Specialists</button>
|
|
854
|
+
</div>
|
|
855
|
+
</div>
|
|
856
|
+
<div id="agents-table">
|
|
857
|
+
<div class="table-header">
|
|
858
|
+
<div>Agent</div>
|
|
859
|
+
<div>Status</div>
|
|
860
|
+
<div>Tasks</div>
|
|
861
|
+
<div>Perf.</div>
|
|
862
|
+
</div>
|
|
863
|
+
<div id="agents-list">
|
|
864
|
+
<div class="empty-state">Waiting for agent data...</div>
|
|
865
|
+
</div>
|
|
866
|
+
</div>
|
|
867
|
+
</div>
|
|
868
|
+
|
|
869
|
+
<div class="panel">
|
|
870
|
+
<div class="panel-head">
|
|
871
|
+
<span class="panel-title">Recent Activity</span>
|
|
872
|
+
<span class="panel-badge" id="activity-count">0 events</span>
|
|
873
|
+
</div>
|
|
874
|
+
<div class="activity-list" id="activity-list">
|
|
875
|
+
<div class="activity-item">
|
|
876
|
+
<div class="activity-dot info"></div>
|
|
877
|
+
<div class="activity-content">
|
|
878
|
+
<div class="activity-message">Awaiting session start</div>
|
|
879
|
+
<div class="activity-time">System ready \xB7 just now</div>
|
|
880
|
+
</div>
|
|
881
|
+
</div>
|
|
882
|
+
</div>
|
|
883
|
+
</div>
|
|
884
|
+
</div>
|
|
885
|
+
|
|
886
|
+
<!-- Tasks, Issues & Interventions -->
|
|
887
|
+
<div class="triple-grid">
|
|
888
|
+
<div class="panel" id="tasks-panel">
|
|
889
|
+
<div class="panel-head">
|
|
890
|
+
<span class="panel-title">\u{1F4DD} Current Tasks</span>
|
|
891
|
+
<span class="panel-badge" id="tasks-count">0</span>
|
|
892
|
+
</div>
|
|
893
|
+
<div class="task-list" id="tasks-list">
|
|
894
|
+
<div class="empty-state">No active tasks</div>
|
|
895
|
+
</div>
|
|
896
|
+
</div>
|
|
897
|
+
|
|
898
|
+
<div class="panel" id="issues-panel">
|
|
899
|
+
<div class="panel-head">
|
|
900
|
+
<span class="panel-title">\u26A0\uFE0F Issues Found</span>
|
|
901
|
+
<span class="panel-badge" id="issues-count">0</span>
|
|
902
|
+
</div>
|
|
903
|
+
<div class="issue-list" id="issues-list">
|
|
904
|
+
<div class="empty-state">No issues detected</div>
|
|
905
|
+
</div>
|
|
906
|
+
</div>
|
|
907
|
+
|
|
908
|
+
<div class="panel" id="interventions-panel">
|
|
909
|
+
<div class="panel-head">
|
|
910
|
+
<span class="panel-title">\u{1F6A8} Human Interventions</span>
|
|
911
|
+
<span class="panel-badge intervention-badge" id="interventions-count">0</span>
|
|
912
|
+
</div>
|
|
913
|
+
<div class="intervention-list" id="interventions-list">
|
|
914
|
+
<div class="empty-state">No interventions required</div>
|
|
915
|
+
</div>
|
|
916
|
+
</div>
|
|
917
|
+
</div>
|
|
918
|
+
|
|
919
|
+
<!-- Session Info Footer -->
|
|
920
|
+
<div class="session-footer">
|
|
921
|
+
<div class="session-info">
|
|
922
|
+
<div class="session-item">
|
|
923
|
+
<span class="session-label">Session ID</span>
|
|
924
|
+
<span class="session-value" id="session-id">\u2014</span>
|
|
925
|
+
</div>
|
|
926
|
+
<div class="session-item">
|
|
927
|
+
<span class="session-label">Target URL</span>
|
|
928
|
+
<span class="session-value" id="target-url">Not configured</span>
|
|
929
|
+
</div>
|
|
930
|
+
<div class="session-item">
|
|
931
|
+
<span class="session-label">Status</span>
|
|
932
|
+
<span class="session-value">
|
|
933
|
+
<span class="status-indicator" id="agent-status-indicator">\u25CF</span>
|
|
934
|
+
<span id="agent-status-text">Idle</span>
|
|
935
|
+
</span>
|
|
936
|
+
</div>
|
|
937
|
+
</div>
|
|
938
|
+
<div class="session-actions">
|
|
939
|
+
<button class="btn-sm btn-danger" onclick="stopSession()" id="stop-btn" disabled>\u25FC Stop</button>
|
|
940
|
+
<button class="btn-sm btn-ghost" onclick="pauseSession()" id="pause-btn" disabled>\u23F8 Pause</button>
|
|
941
|
+
</div>
|
|
942
|
+
</div>
|
|
943
|
+
</div>
|
|
944
|
+
</main>
|
|
945
|
+
</div>
|
|
946
|
+
|
|
947
|
+
<script>
|
|
948
|
+
let ws;
|
|
949
|
+
let activities = [];
|
|
950
|
+
let performanceChart, activityChart, errorChart;
|
|
951
|
+
let chartData = { actions: [], successRates: [], labels: [] };
|
|
952
|
+
|
|
953
|
+
// WebSocket Connection
|
|
954
|
+
function connectWebSocket() {
|
|
955
|
+
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
956
|
+
ws = new WebSocket(\`\${protocol}//\${window.location.host}\`);
|
|
957
|
+
|
|
958
|
+
ws.onopen = () => {
|
|
959
|
+
document.getElementById('connection-dot').classList.remove('disconnected');
|
|
960
|
+
document.getElementById('connection-text').textContent = 'Connected';
|
|
961
|
+
};
|
|
962
|
+
|
|
963
|
+
ws.onclose = () => {
|
|
964
|
+
document.getElementById('connection-dot').classList.add('disconnected');
|
|
965
|
+
document.getElementById('connection-text').textContent = 'Disconnected';
|
|
966
|
+
setTimeout(connectWebSocket, 3000);
|
|
967
|
+
};
|
|
968
|
+
|
|
969
|
+
ws.onmessage = (event) => {
|
|
970
|
+
const data = JSON.parse(event.data);
|
|
971
|
+
handleMessage(data);
|
|
972
|
+
};
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
function handleMessage(data) {
|
|
976
|
+
switch(data.type) {
|
|
977
|
+
case 'status':
|
|
978
|
+
updateStatus(data.data);
|
|
979
|
+
break;
|
|
980
|
+
case 'session':
|
|
981
|
+
updateMetrics(data.data);
|
|
982
|
+
break;
|
|
983
|
+
case 'agents':
|
|
984
|
+
updateAgents(data.data);
|
|
985
|
+
break;
|
|
986
|
+
case 'activity':
|
|
987
|
+
addActivity(data.data);
|
|
988
|
+
break;
|
|
989
|
+
case 'tasks':
|
|
990
|
+
updateTasks(data.data);
|
|
991
|
+
break;
|
|
992
|
+
case 'issues':
|
|
993
|
+
updateIssues(data.data);
|
|
994
|
+
break;
|
|
995
|
+
case 'intervention':
|
|
996
|
+
addIntervention(data.data);
|
|
997
|
+
break;
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
function updateStatus(status) {
|
|
1002
|
+
const isRunning = status.isRunning;
|
|
1003
|
+
const indicator = document.getElementById('agent-status-indicator');
|
|
1004
|
+
const statusText = document.getElementById('agent-status-text');
|
|
1005
|
+
const stopBtn = document.getElementById('stop-btn');
|
|
1006
|
+
const pauseBtn = document.getElementById('pause-btn');
|
|
1007
|
+
|
|
1008
|
+
// Update sidebar connection text
|
|
1009
|
+
document.getElementById('connection-text').textContent = isRunning ? 'Running' : 'Connected';
|
|
1010
|
+
|
|
1011
|
+
// Update session footer status
|
|
1012
|
+
indicator.className = 'status-indicator ' + (isRunning ? 'running' : 'idle');
|
|
1013
|
+
statusText.textContent = isRunning ? 'Running' : 'Idle';
|
|
1014
|
+
|
|
1015
|
+
// Update target URL
|
|
1016
|
+
if (status.target) {
|
|
1017
|
+
document.getElementById('target-url').textContent = status.target;
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
// Update session ID
|
|
1021
|
+
if (status.sessionId) {
|
|
1022
|
+
document.getElementById('session-id').textContent = status.sessionId.substring(0, 12) + '...';
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
// Enable/disable control buttons
|
|
1026
|
+
stopBtn.disabled = !isRunning;
|
|
1027
|
+
pauseBtn.disabled = !isRunning;
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
function updateMetrics(session) {
|
|
1031
|
+
document.getElementById('active-agents').textContent = session.active_agents || 0;
|
|
1032
|
+
document.getElementById('total-actions').textContent = session.total_actions || 0;
|
|
1033
|
+
document.getElementById('bugs-found').textContent = session.bugs_found || 0;
|
|
1034
|
+
|
|
1035
|
+
const rate = session.success_rate;
|
|
1036
|
+
document.getElementById('success-rate').textContent = rate > 0 ? rate + '%' : '\u2014';
|
|
1037
|
+
|
|
1038
|
+
// Update chart data
|
|
1039
|
+
const now = new Date();
|
|
1040
|
+
const timeLabel = now.getHours().toString().padStart(2, '0') + ':' + now.getMinutes().toString().padStart(2, '0');
|
|
1041
|
+
|
|
1042
|
+
if (chartData.labels.length > 7) {
|
|
1043
|
+
chartData.labels.shift();
|
|
1044
|
+
chartData.actions.shift();
|
|
1045
|
+
chartData.successRates.shift();
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
chartData.labels.push(timeLabel);
|
|
1049
|
+
chartData.actions.push(session.total_actions || 0);
|
|
1050
|
+
chartData.successRates.push(rate || 0);
|
|
1051
|
+
|
|
1052
|
+
if (performanceChart) {
|
|
1053
|
+
performanceChart.data.labels = chartData.labels;
|
|
1054
|
+
performanceChart.data.datasets[0].data = chartData.actions;
|
|
1055
|
+
performanceChart.data.datasets[1].data = chartData.successRates;
|
|
1056
|
+
performanceChart.update('none');
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
function updateAgents(agents) {
|
|
1061
|
+
const container = document.getElementById('agents-list');
|
|
1062
|
+
const countEl = document.getElementById('active-agents');
|
|
1063
|
+
|
|
1064
|
+
if (!agents || agents.length === 0) {
|
|
1065
|
+
container.innerHTML = '<div class="empty-state">Waiting for agent data...</div>';
|
|
1066
|
+
countEl.textContent = '0';
|
|
1067
|
+
return;
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
countEl.textContent = agents.length;
|
|
1071
|
+
|
|
1072
|
+
container.innerHTML = agents.map(agent => \`
|
|
1073
|
+
<div class="table-row">
|
|
1074
|
+
<div class="agent-name">\${agent.name}</div>
|
|
1075
|
+
<div><span class="status-badge \${agent.status}">\${agent.status}</span></div>
|
|
1076
|
+
<div>\${agent.tasks || 0}</div>
|
|
1077
|
+
<div>\${agent.performance || 0}%</div>
|
|
1078
|
+
</div>
|
|
1079
|
+
\`).join('');
|
|
1080
|
+
|
|
1081
|
+
// Update hierarchy
|
|
1082
|
+
updateHierarchy(agents);
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
function addActivity(activity) {
|
|
1086
|
+
activities.unshift(activity);
|
|
1087
|
+
if (activities.length > 20) activities.pop();
|
|
1088
|
+
|
|
1089
|
+
const container = document.getElementById('activity-list');
|
|
1090
|
+
document.getElementById('activity-count').textContent = activities.length + ' events';
|
|
1091
|
+
|
|
1092
|
+
container.innerHTML = activities.map(a => \`
|
|
1093
|
+
<div class="activity-item">
|
|
1094
|
+
<div class="activity-dot \${a.type || 'info'}"></div>
|
|
1095
|
+
<div class="activity-content">
|
|
1096
|
+
<div class="activity-message">\${a.message}</div>
|
|
1097
|
+
<div class="activity-time">\${formatTime(a.timestamp)}</div>
|
|
1098
|
+
</div>
|
|
1099
|
+
</div>
|
|
1100
|
+
\`).join('');
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
function formatTime(timestamp) {
|
|
1104
|
+
if (!timestamp) return 'just now';
|
|
1105
|
+
const date = new Date(timestamp);
|
|
1106
|
+
const now = new Date();
|
|
1107
|
+
const diff = Math.floor((now - date) / 1000);
|
|
1108
|
+
|
|
1109
|
+
if (diff < 60) return 'just now';
|
|
1110
|
+
if (diff < 3600) return Math.floor(diff / 60) + ' min ago';
|
|
1111
|
+
return date.toLocaleTimeString();
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
// Charts
|
|
1115
|
+
function initCharts() {
|
|
1116
|
+
const chartOptions = {
|
|
1117
|
+
responsive: true,
|
|
1118
|
+
maintainAspectRatio: false,
|
|
1119
|
+
plugins: {
|
|
1120
|
+
legend: {
|
|
1121
|
+
display: true,
|
|
1122
|
+
position: 'top',
|
|
1123
|
+
labels: { color: '#8b98a8', font: { size: 11 } }
|
|
1124
|
+
}
|
|
1125
|
+
},
|
|
1126
|
+
scales: {
|
|
1127
|
+
x: {
|
|
1128
|
+
ticks: { color: '#4b5563' },
|
|
1129
|
+
grid: { color: 'rgba(255,255,255,0.04)' }
|
|
1130
|
+
},
|
|
1131
|
+
y: {
|
|
1132
|
+
ticks: { color: '#4b5563' },
|
|
1133
|
+
grid: { color: 'rgba(255,255,255,0.04)' }
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
};
|
|
1137
|
+
|
|
1138
|
+
// Performance Chart
|
|
1139
|
+
const perfCtx = document.getElementById('performanceChart').getContext('2d');
|
|
1140
|
+
performanceChart = new Chart(perfCtx, {
|
|
1141
|
+
type: 'line',
|
|
1142
|
+
data: {
|
|
1143
|
+
labels: chartData.labels.length ? chartData.labels : ['\u2014'],
|
|
1144
|
+
datasets: [{
|
|
1145
|
+
label: 'Actions/min',
|
|
1146
|
+
data: chartData.actions.length ? chartData.actions : [0],
|
|
1147
|
+
borderColor: '#f97316',
|
|
1148
|
+
backgroundColor: 'rgba(249, 115, 22, 0.1)',
|
|
1149
|
+
tension: 0.4,
|
|
1150
|
+
fill: true
|
|
1151
|
+
}, {
|
|
1152
|
+
label: 'Success %',
|
|
1153
|
+
data: chartData.successRates.length ? chartData.successRates : [0],
|
|
1154
|
+
borderColor: '#22c55e',
|
|
1155
|
+
backgroundColor: 'rgba(34, 197, 94, 0.1)',
|
|
1156
|
+
tension: 0.4,
|
|
1157
|
+
fill: true
|
|
1158
|
+
}]
|
|
1159
|
+
},
|
|
1160
|
+
options: chartOptions
|
|
1161
|
+
});
|
|
1162
|
+
|
|
1163
|
+
// Activity Chart
|
|
1164
|
+
const actCtx = document.getElementById('activityChart').getContext('2d');
|
|
1165
|
+
activityChart = new Chart(actCtx, {
|
|
1166
|
+
type: 'bar',
|
|
1167
|
+
data: {
|
|
1168
|
+
labels: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
|
|
1169
|
+
datasets: [{
|
|
1170
|
+
label: 'Tests',
|
|
1171
|
+
data: [0, 0, 0, 0, 0, 0, 0],
|
|
1172
|
+
backgroundColor: '#f97316'
|
|
1173
|
+
}, {
|
|
1174
|
+
label: 'Bugs',
|
|
1175
|
+
data: [0, 0, 0, 0, 0, 0, 0],
|
|
1176
|
+
backgroundColor: '#ef4444'
|
|
1177
|
+
}]
|
|
1178
|
+
},
|
|
1179
|
+
options: chartOptions
|
|
1180
|
+
});
|
|
1181
|
+
|
|
1182
|
+
// Error Chart
|
|
1183
|
+
const errCtx = document.getElementById('errorChart').getContext('2d');
|
|
1184
|
+
errorChart = new Chart(errCtx, {
|
|
1185
|
+
type: 'doughnut',
|
|
1186
|
+
data: {
|
|
1187
|
+
labels: ['Success', 'Warnings', 'Errors'],
|
|
1188
|
+
datasets: [{
|
|
1189
|
+
data: [100, 0, 0],
|
|
1190
|
+
backgroundColor: ['#22c55e', '#f59e0b', '#ef4444']
|
|
1191
|
+
}]
|
|
1192
|
+
},
|
|
1193
|
+
options: {
|
|
1194
|
+
responsive: true,
|
|
1195
|
+
maintainAspectRatio: false,
|
|
1196
|
+
plugins: {
|
|
1197
|
+
legend: {
|
|
1198
|
+
position: 'right',
|
|
1199
|
+
labels: { color: '#8b98a8', font: { size: 11 } }
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
});
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
function switchChartTab(tab) {
|
|
1207
|
+
// Update tabs in the chart panel
|
|
1208
|
+
const chartTabs = document.querySelectorAll('#chart-performance, #chart-activity, #chart-errors')
|
|
1209
|
+
.item(0)?.closest('.panel')?.querySelectorAll('.tabs .tab');
|
|
1210
|
+
|
|
1211
|
+
if (chartTabs) {
|
|
1212
|
+
chartTabs.forEach((t, i) => {
|
|
1213
|
+
t.classList.remove('active');
|
|
1214
|
+
if ((tab === 'performance' && i === 0) ||
|
|
1215
|
+
(tab === 'activity' && i === 1) ||
|
|
1216
|
+
(tab === 'errors' && i === 2)) {
|
|
1217
|
+
t.classList.add('active');
|
|
1218
|
+
}
|
|
1219
|
+
});
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
document.getElementById('chart-performance').style.display = tab === 'performance' ? 'block' : 'none';
|
|
1223
|
+
document.getElementById('chart-activity').style.display = tab === 'activity' ? 'block' : 'none';
|
|
1224
|
+
document.getElementById('chart-errors').style.display = tab === 'errors' ? 'block' : 'none';
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
function switchAgentTab(tab) {
|
|
1228
|
+
const agentsTabs = document.querySelectorAll('#agents-table .tabs .tab');
|
|
1229
|
+
agentsTabs.forEach(t => t.classList.remove('active'));
|
|
1230
|
+
|
|
1231
|
+
if (tab === 'specialists') {
|
|
1232
|
+
agentsTabs[1]?.classList.add('active');
|
|
1233
|
+
// Show specialists data
|
|
1234
|
+
const container = document.getElementById('agents-list');
|
|
1235
|
+
container.innerHTML = \`
|
|
1236
|
+
<div class="table-row">
|
|
1237
|
+
<div class="agent-name">Browser Specialist</div>
|
|
1238
|
+
<div><span class="status-badge idle">idle</span></div>
|
|
1239
|
+
<div>0</div>
|
|
1240
|
+
<div>\u2014</div>
|
|
1241
|
+
</div>
|
|
1242
|
+
<div class="table-row">
|
|
1243
|
+
<div class="agent-name">API Tester</div>
|
|
1244
|
+
<div><span class="status-badge idle">idle</span></div>
|
|
1245
|
+
<div>0</div>
|
|
1246
|
+
<div>\u2014</div>
|
|
1247
|
+
</div>
|
|
1248
|
+
<div class="table-row">
|
|
1249
|
+
<div class="agent-name">Auth Specialist</div>
|
|
1250
|
+
<div><span class="status-badge idle">idle</span></div>
|
|
1251
|
+
<div>0</div>
|
|
1252
|
+
<div>\u2014</div>
|
|
1253
|
+
</div>
|
|
1254
|
+
<div class="table-row">
|
|
1255
|
+
<div class="agent-name">UI Tester</div>
|
|
1256
|
+
<div><span class="status-badge idle">idle</span></div>
|
|
1257
|
+
<div>0</div>
|
|
1258
|
+
<div>\u2014</div>
|
|
1259
|
+
</div>
|
|
1260
|
+
<div class="table-row">
|
|
1261
|
+
<div class="agent-name">Security Scanner</div>
|
|
1262
|
+
<div><span class="status-badge idle">idle</span></div>
|
|
1263
|
+
<div>0</div>
|
|
1264
|
+
<div>\u2014</div>
|
|
1265
|
+
</div>
|
|
1266
|
+
\`;
|
|
1267
|
+
} else {
|
|
1268
|
+
agentsTabs[0]?.classList.add('active');
|
|
1269
|
+
// Reload agents data from correct endpoint
|
|
1270
|
+
fetch('/api/dynamic-agents', { credentials: 'include' })
|
|
1271
|
+
.then(r => r.json())
|
|
1272
|
+
.then(updateAgents)
|
|
1273
|
+
.catch(() => {
|
|
1274
|
+
document.getElementById('agents-list').innerHTML = '<div class="empty-state">Waiting for agent data...</div>';
|
|
1275
|
+
});
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
function scrollToSection(elementId) {
|
|
1280
|
+
const element = document.getElementById(elementId);
|
|
1281
|
+
if (element) {
|
|
1282
|
+
element.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
1283
|
+
// Highlight effect
|
|
1284
|
+
element.style.boxShadow = '0 0 0 2px var(--accent)';
|
|
1285
|
+
setTimeout(() => {
|
|
1286
|
+
element.style.boxShadow = '';
|
|
1287
|
+
}, 2000);
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
// Hierarchy
|
|
1292
|
+
function updateHierarchy(agents) {
|
|
1293
|
+
const container = document.getElementById('hierarchy-container');
|
|
1294
|
+
|
|
1295
|
+
const agentTypes = {
|
|
1296
|
+
'Main Agent': { x: 200, y: 30, color: '#f97316', class: 'main' },
|
|
1297
|
+
'Browser Specialist': { x: 80, y: 120, color: '#22c55e', class: 'browser' },
|
|
1298
|
+
'API Tester': { x: 200, y: 120, color: '#f59e0b', class: 'api' },
|
|
1299
|
+
'Auth Specialist': { x: 320, y: 120, color: '#ef4444', class: 'auth' },
|
|
1300
|
+
'UI Tester': { x: 80, y: 210, color: '#8b5cf6', class: 'ui' },
|
|
1301
|
+
'Performance': { x: 200, y: 210, color: '#06b6d4', class: 'perf' },
|
|
1302
|
+
'Security Scanner': { x: 320, y: 210, color: '#ec4899', class: 'security' }
|
|
1303
|
+
};
|
|
1304
|
+
|
|
1305
|
+
let html = '<div class="hierarchy-badge">\u25CF Main Agent</div>';
|
|
1306
|
+
html += '<svg width="100%" height="100%" style="position:absolute;top:0;left:0;">';
|
|
1307
|
+
|
|
1308
|
+
// Draw connections
|
|
1309
|
+
html += '<line x1="240" y1="50" x2="120" y2="120" stroke="#333" stroke-width="2" stroke-dasharray="4"/>';
|
|
1310
|
+
html += '<line x1="240" y1="50" x2="240" y2="120" stroke="#333" stroke-width="2" stroke-dasharray="4"/>';
|
|
1311
|
+
html += '<line x1="240" y1="50" x2="360" y2="120" stroke="#333" stroke-width="2" stroke-dasharray="4"/>';
|
|
1312
|
+
html += '<line x1="120" y1="140" x2="120" y2="210" stroke="#333" stroke-width="2" stroke-dasharray="4"/>';
|
|
1313
|
+
html += '<line x1="240" y1="140" x2="240" y2="210" stroke="#333" stroke-width="2" stroke-dasharray="4"/>';
|
|
1314
|
+
html += '<line x1="360" y1="140" x2="360" y2="210" stroke="#333" stroke-width="2" stroke-dasharray="4"/>';
|
|
1315
|
+
|
|
1316
|
+
html += '</svg>';
|
|
1317
|
+
|
|
1318
|
+
// Draw nodes
|
|
1319
|
+
agents.forEach(agent => {
|
|
1320
|
+
const config = agentTypes[agent.name];
|
|
1321
|
+
if (config) {
|
|
1322
|
+
const isActive = agent.status === 'running';
|
|
1323
|
+
html += \`<div class="agent-node \${config.class}" style="left:\${config.x}px;top:\${config.y}px;opacity:\${isActive ? 1 : 0.5}">
|
|
1324
|
+
\${isActive ? '\u25CF' : '\u25CB'} \${agent.name.split(' ')[0]}
|
|
1325
|
+
</div>\`;
|
|
1326
|
+
}
|
|
1327
|
+
});
|
|
1328
|
+
|
|
1329
|
+
container.innerHTML = html;
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
// Tasks
|
|
1333
|
+
function updateTasks(tasks) {
|
|
1334
|
+
const container = document.getElementById('tasks-list');
|
|
1335
|
+
const countEl = document.getElementById('tasks-count');
|
|
1336
|
+
|
|
1337
|
+
if (!tasks || tasks.length === 0) {
|
|
1338
|
+
container.innerHTML = '<div class="empty-state">No active tasks</div>';
|
|
1339
|
+
countEl.textContent = '0';
|
|
1340
|
+
return;
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
countEl.textContent = tasks.length;
|
|
1344
|
+
|
|
1345
|
+
container.innerHTML = tasks.map(task => {
|
|
1346
|
+
const progressNum = parseInt(task.progress) || 0;
|
|
1347
|
+
return \`
|
|
1348
|
+
<div class="task-item">
|
|
1349
|
+
<div class="task-icon">\${task.status === 'running' ? '\u26A1' : task.status === 'completed' ? '\u2713' : '\u25CB'}</div>
|
|
1350
|
+
<div class="task-content">
|
|
1351
|
+
<div class="task-name">\${task.name}</div>
|
|
1352
|
+
<div class="task-meta">
|
|
1353
|
+
<span>\${task.agent}</span>
|
|
1354
|
+
<span>\${formatTime(task.started_at)}</span>
|
|
1355
|
+
</div>
|
|
1356
|
+
</div>
|
|
1357
|
+
<div class="task-progress">
|
|
1358
|
+
<div class="task-progress-fill" style="width: \${progressNum}%"></div>
|
|
1359
|
+
</div>
|
|
1360
|
+
</div>
|
|
1361
|
+
\`;
|
|
1362
|
+
}).join('');
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
// Issues
|
|
1366
|
+
function updateIssues(issues) {
|
|
1367
|
+
const container = document.getElementById('issues-list');
|
|
1368
|
+
const countEl = document.getElementById('issues-count');
|
|
1369
|
+
|
|
1370
|
+
if (!issues || issues.length === 0) {
|
|
1371
|
+
container.innerHTML = '<div class="empty-state">No issues detected</div>';
|
|
1372
|
+
countEl.textContent = '0';
|
|
1373
|
+
return;
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
countEl.textContent = issues.length;
|
|
1377
|
+
|
|
1378
|
+
container.innerHTML = issues.map(issue => \`
|
|
1379
|
+
<div class="issue-item">
|
|
1380
|
+
<div class="issue-icon \${issue.severity}">\${
|
|
1381
|
+
issue.severity === 'critical' ? '\u{1F534}' :
|
|
1382
|
+
issue.severity === 'high' ? '\u{1F7E0}' :
|
|
1383
|
+
issue.severity === 'medium' ? '\u{1F7E1}' : '\u{1F7E2}'
|
|
1384
|
+
}</div>
|
|
1385
|
+
<div class="issue-content">
|
|
1386
|
+
<div class="issue-title">\${issue.title}</div>
|
|
1387
|
+
<div class="issue-meta">
|
|
1388
|
+
<span class="severity-badge \${issue.severity}">\${issue.severity}</span>
|
|
1389
|
+
<span>\${issue.agent}</span>
|
|
1390
|
+
</div>
|
|
1391
|
+
</div>
|
|
1392
|
+
</div>
|
|
1393
|
+
\`).join('');
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
// Interventions
|
|
1397
|
+
let interventions = [];
|
|
1398
|
+
|
|
1399
|
+
function addIntervention(intervention) {
|
|
1400
|
+
interventions.unshift(intervention);
|
|
1401
|
+
renderInterventions();
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
function renderInterventions() {
|
|
1405
|
+
const container = document.getElementById('interventions-list');
|
|
1406
|
+
const countEl = document.getElementById('interventions-count');
|
|
1407
|
+
const navCountEl = document.getElementById('intervention-count');
|
|
1408
|
+
|
|
1409
|
+
countEl.textContent = interventions.length;
|
|
1410
|
+
navCountEl.textContent = interventions.length;
|
|
1411
|
+
|
|
1412
|
+
if (interventions.length === 0) {
|
|
1413
|
+
container.innerHTML = '<div class="empty-state">No interventions required</div>';
|
|
1414
|
+
return;
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
container.innerHTML = interventions.map(int => \`
|
|
1418
|
+
<div class="intervention-item" id="intervention-\${int.id}">
|
|
1419
|
+
<div class="intervention-header">
|
|
1420
|
+
<div class="intervention-icon">\u{1F6A8}</div>
|
|
1421
|
+
<div class="intervention-title">\${int.title}</div>
|
|
1422
|
+
</div>
|
|
1423
|
+
<div class="intervention-desc">\${int.description}</div>
|
|
1424
|
+
<div class="intervention-actions">
|
|
1425
|
+
<button class="btn-sm btn-primary" onclick="respondIntervention('\${int.id}', 'approve')">\u2713 Approve</button>
|
|
1426
|
+
<button class="btn-sm btn-danger" onclick="respondIntervention('\${int.id}', 'reject')">\u2715 Reject</button>
|
|
1427
|
+
<button class="btn-sm btn-ghost" onclick="respondIntervention('\${int.id}', 'skip')">Skip</button>
|
|
1428
|
+
</div>
|
|
1429
|
+
</div>
|
|
1430
|
+
\`).join('');
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
async function respondIntervention(id, response) {
|
|
1434
|
+
try {
|
|
1435
|
+
await fetch('/api/brain/run-test/' + id, {
|
|
1436
|
+
method: 'POST',
|
|
1437
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1438
|
+
credentials: 'include',
|
|
1439
|
+
body: JSON.stringify({ response })
|
|
1440
|
+
});
|
|
1441
|
+
|
|
1442
|
+
// Remove from local list
|
|
1443
|
+
interventions = interventions.filter(i => i.id !== id);
|
|
1444
|
+
renderInterventions();
|
|
1445
|
+
|
|
1446
|
+
addActivity({
|
|
1447
|
+
type: response === 'approve' ? 'success' : 'warning',
|
|
1448
|
+
message: \`Intervention \${response}ed: \${id}\`,
|
|
1449
|
+
timestamp: new Date().toISOString()
|
|
1450
|
+
});
|
|
1451
|
+
} catch (error) {
|
|
1452
|
+
addActivity({ type: 'error', message: 'Failed to respond to intervention', timestamp: new Date().toISOString() });
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
// Actions
|
|
1457
|
+
async function startSession() {
|
|
1458
|
+
try {
|
|
1459
|
+
const response = await fetch('/api/agent/start', { method: 'POST', credentials: 'include' });
|
|
1460
|
+
const result = await response.json();
|
|
1461
|
+
if (result.success) {
|
|
1462
|
+
addActivity({ type: 'success', message: 'Session started', timestamp: new Date().toISOString() });
|
|
1463
|
+
} else {
|
|
1464
|
+
addActivity({ type: 'error', message: result.error || 'Failed to start session', timestamp: new Date().toISOString() });
|
|
1465
|
+
}
|
|
1466
|
+
} catch (error) {
|
|
1467
|
+
addActivity({ type: 'error', message: 'Failed to start session', timestamp: new Date().toISOString() });
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
async function stopSession() {
|
|
1472
|
+
try {
|
|
1473
|
+
const response = await fetch('/api/agent/stop', { method: 'POST', credentials: 'include' });
|
|
1474
|
+
const result = await response.json();
|
|
1475
|
+
addActivity({
|
|
1476
|
+
type: result.success ? 'warning' : 'error',
|
|
1477
|
+
message: result.success ? 'Session stopped' : (result.error || 'Failed to stop session'),
|
|
1478
|
+
timestamp: new Date().toISOString()
|
|
1479
|
+
});
|
|
1480
|
+
} catch (error) {
|
|
1481
|
+
addActivity({ type: 'error', message: 'Failed to stop session', timestamp: new Date().toISOString() });
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
async function pauseSession() {
|
|
1486
|
+
addActivity({ type: 'info', message: 'Pause requested...', timestamp: new Date().toISOString() });
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
// Load initial data
|
|
1490
|
+
async function loadInitialData() {
|
|
1491
|
+
try {
|
|
1492
|
+
const creds = { credentials: 'include' };
|
|
1493
|
+
const [sessionsRes, bugsRes, tasksRes, issuesRes, statusRes, agentsRes, metricsRes, sessionsHistRes] = await Promise.all([
|
|
1494
|
+
fetch('/api/sessions?limit=1', creds),
|
|
1495
|
+
fetch('/api/bugs', creds),
|
|
1496
|
+
fetch('/api/tasks', creds),
|
|
1497
|
+
fetch('/api/issues', creds),
|
|
1498
|
+
fetch('/api/status', creds),
|
|
1499
|
+
fetch('/api/dynamic-agents', creds),
|
|
1500
|
+
fetch('/api/metrics', creds),
|
|
1501
|
+
fetch('/api/sessions?limit=7', creds),
|
|
1502
|
+
]);
|
|
1503
|
+
|
|
1504
|
+
const sessions = await sessionsRes.json();
|
|
1505
|
+
const bugs = await bugsRes.json();
|
|
1506
|
+
const tasks = await tasksRes.json();
|
|
1507
|
+
const issues = await issuesRes.json();
|
|
1508
|
+
const status = await statusRes.json();
|
|
1509
|
+
const agents = await agentsRes.json();
|
|
1510
|
+
const metrics = await metricsRes.json();
|
|
1511
|
+
const sessionsHistory = await sessionsHistRes.json();
|
|
1512
|
+
|
|
1513
|
+
// Update status indicator
|
|
1514
|
+
updateStatus(status);
|
|
1515
|
+
|
|
1516
|
+
// Update metrics cards
|
|
1517
|
+
if (sessions.length > 0) {
|
|
1518
|
+
const session = sessions[0];
|
|
1519
|
+
updateMetrics({
|
|
1520
|
+
active_agents: agents.filter(a => a.status === 'running').length,
|
|
1521
|
+
total_actions: session.total_actions || 0,
|
|
1522
|
+
bugs_found: session.bugs_found || 0,
|
|
1523
|
+
success_rate: session.total_actions > 0
|
|
1524
|
+
? Math.round(((session.total_actions - (session.bugs_found || 0)) / session.total_actions) * 100)
|
|
1525
|
+
: 0
|
|
1526
|
+
});
|
|
1527
|
+
if (session.id) {
|
|
1528
|
+
document.getElementById('session-id').textContent = session.id.substring(0, 12) + '...';
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
// Update agent list and hierarchy
|
|
1533
|
+
if (agents && agents.length > 0) {
|
|
1534
|
+
updateAgents(agents);
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
// Populate performance chart from session history (last 7 sessions)
|
|
1538
|
+
if (sessionsHistory.length > 0) {
|
|
1539
|
+
const historicSessions = [...sessionsHistory].reverse();
|
|
1540
|
+
chartData.labels = historicSessions.map((s, i) => {
|
|
1541
|
+
const d = new Date(s.started_at);
|
|
1542
|
+
return d.getHours().toString().padStart(2,'0') + ':' + d.getMinutes().toString().padStart(2,'0');
|
|
1543
|
+
});
|
|
1544
|
+
chartData.actions = historicSessions.map(s => s.total_actions || 0);
|
|
1545
|
+
chartData.successRates = historicSessions.map(s =>
|
|
1546
|
+
s.total_actions > 0
|
|
1547
|
+
? Math.round(((s.total_actions - (s.bugs_found || 0)) / s.total_actions) * 100)
|
|
1548
|
+
: 0
|
|
1549
|
+
);
|
|
1550
|
+
if (performanceChart) {
|
|
1551
|
+
performanceChart.data.labels = chartData.labels;
|
|
1552
|
+
performanceChart.data.datasets[0].data = chartData.actions;
|
|
1553
|
+
performanceChart.data.datasets[1].data = chartData.successRates;
|
|
1554
|
+
performanceChart.update('none');
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
// Weekly activity chart \u2014 group sessions by day of week
|
|
1558
|
+
const days = ['Sun','Mon','Tue','Wed','Thu','Fri','Sat'];
|
|
1559
|
+
const testsByDay = [0,0,0,0,0,0,0];
|
|
1560
|
+
const bugsByDay = [0,0,0,0,0,0,0];
|
|
1561
|
+
sessionsHistory.forEach(s => {
|
|
1562
|
+
const day = new Date(s.started_at).getDay();
|
|
1563
|
+
testsByDay[day] += s.total_actions || 0;
|
|
1564
|
+
bugsByDay[day] += s.bugs_found || 0;
|
|
1565
|
+
});
|
|
1566
|
+
if (activityChart) {
|
|
1567
|
+
activityChart.data.labels = days;
|
|
1568
|
+
activityChart.data.datasets[0].data = testsByDay;
|
|
1569
|
+
activityChart.data.datasets[1].data = bugsByDay;
|
|
1570
|
+
activityChart.update('none');
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
// Error/success donut from server-side metrics counters
|
|
1575
|
+
if (metrics && metrics.counters) {
|
|
1576
|
+
const passed = metrics.counters.tests_passed || 0;
|
|
1577
|
+
const failed = metrics.counters.tests_failed || 0;
|
|
1578
|
+
const total = passed + failed;
|
|
1579
|
+
if (total > 0 && errorChart) {
|
|
1580
|
+
errorChart.data.datasets[0].data = [passed, 0, failed];
|
|
1581
|
+
errorChart.update('none');
|
|
1582
|
+
}
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
// Update tasks and issues
|
|
1586
|
+
updateTasks(tasks);
|
|
1587
|
+
updateIssues(issues);
|
|
1588
|
+
|
|
1589
|
+
// Kanban badge = open bugs
|
|
1590
|
+
const openBugs = bugs.filter(b => b.status === 'open' || b.status === 'in-progress');
|
|
1591
|
+
document.getElementById('kanban-count').textContent = openBugs.length || 0;
|
|
1592
|
+
|
|
1593
|
+
// Seed activity feed with recent actions from latest session
|
|
1594
|
+
if (sessions.length > 0) {
|
|
1595
|
+
try {
|
|
1596
|
+
const actionsRes = await fetch(\`/api/sessions/\${sessions[0].id}/actions\`, creds);
|
|
1597
|
+
const actions = await actionsRes.json();
|
|
1598
|
+
actions.slice(0, 10).forEach(a => {
|
|
1599
|
+
addActivity({ type: 'info', message: a.description || a.type, timestamp: a.timestamp });
|
|
1600
|
+
});
|
|
1601
|
+
} catch (_) { /* no actions yet */ }
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
} catch (error) {
|
|
1605
|
+
console.error('Failed to load initial data:', error);
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
// Initialize
|
|
1610
|
+
window.addEventListener('load', () => {
|
|
1611
|
+
initCharts();
|
|
1612
|
+
connectWebSocket();
|
|
1613
|
+
loadInitialData();
|
|
1614
|
+
});
|
|
1615
|
+
</script>
|
|
1616
|
+
|
|
1617
|
+
</body>
|
|
1618
|
+
</html>`;
|
|
1619
|
+
}
|
|
1620
|
+
export {
|
|
1621
|
+
getDashboardHTML
|
|
1622
|
+
};
|