@openqa/cli 1.3.3 → 2.0.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.
Files changed (40) hide show
  1. package/README.md +1 -1
  2. package/dist/agent/brain/diff-analyzer.js +140 -0
  3. package/dist/agent/brain/diff-analyzer.js.map +1 -0
  4. package/dist/agent/brain/llm-cache.js +47 -0
  5. package/dist/agent/brain/llm-cache.js.map +1 -0
  6. package/dist/agent/brain/llm-resilience.js +252 -0
  7. package/dist/agent/brain/llm-resilience.js.map +1 -0
  8. package/dist/agent/config/index.js +588 -0
  9. package/dist/agent/config/index.js.map +1 -0
  10. package/dist/agent/coverage/index.js +74 -0
  11. package/dist/agent/coverage/index.js.map +1 -0
  12. package/dist/agent/export/index.js +158 -0
  13. package/dist/agent/export/index.js.map +1 -0
  14. package/dist/agent/index-v2.js +2795 -0
  15. package/dist/agent/index-v2.js.map +1 -0
  16. package/dist/agent/index.js +387 -55
  17. package/dist/agent/index.js.map +1 -1
  18. package/dist/agent/logger.js +41 -0
  19. package/dist/agent/logger.js.map +1 -0
  20. package/dist/agent/metrics.js +39 -0
  21. package/dist/agent/metrics.js.map +1 -0
  22. package/dist/agent/notifications/index.js +106 -0
  23. package/dist/agent/notifications/index.js.map +1 -0
  24. package/dist/agent/openapi/spec.js +338 -0
  25. package/dist/agent/openapi/spec.js.map +1 -0
  26. package/dist/agent/tools/project-runner.js +481 -0
  27. package/dist/agent/tools/project-runner.js.map +1 -0
  28. package/dist/cli/config.html.js +454 -0
  29. package/dist/cli/daemon.js +7572 -0
  30. package/dist/cli/dashboard.html.js +1619 -0
  31. package/dist/cli/index.js +3624 -1675
  32. package/dist/cli/kanban.html.js +577 -0
  33. package/dist/cli/routes.js +895 -0
  34. package/dist/cli/routes.js.map +1 -0
  35. package/dist/cli/server.js +3564 -1646
  36. package/dist/database/index.js +503 -10
  37. package/dist/database/index.js.map +1 -1
  38. package/dist/database/sqlite.js +281 -0
  39. package/dist/database/sqlite.js.map +1 -0
  40. package/package.json +18 -5
@@ -0,0 +1,1619 @@
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
+ </div>
745
+
746
+ <div class="sidebar-footer">
747
+ <div class="status-pill">
748
+ <div class="dot" id="connection-dot"></div>
749
+ <span id="connection-text">Connected</span>
750
+ </div>
751
+ </div>
752
+ </aside>
753
+
754
+ <!-- Main -->
755
+ <main>
756
+ <div class="topbar">
757
+ <div>
758
+ <div class="page-title">Dashboard</div>
759
+ <div class="page-breadcrumb">openqa / overview / today</div>
760
+ </div>
761
+ <div class="topbar-actions">
762
+ <button class="btn-sm btn-ghost">Export</button>
763
+ <button class="btn-sm btn-ghost" onclick="location.reload()">\u21BB Refresh</button>
764
+ <button class="btn-sm btn-primary" onclick="startSession()">\u25B6 Run Session</button>
765
+ </div>
766
+ </div>
767
+
768
+ <div class="content">
769
+ <!-- Metrics -->
770
+ <div class="metrics-grid">
771
+ <div class="metric-card">
772
+ <div class="metric-header">
773
+ <div class="metric-label">Active Agents</div>
774
+ <div class="metric-icon">\u{1F916}</div>
775
+ </div>
776
+ <div class="metric-value" id="active-agents">0</div>
777
+ <div class="metric-change positive" id="agents-change">\u2191 0 from last hour</div>
778
+ </div>
779
+ <div class="metric-card">
780
+ <div class="metric-header">
781
+ <div class="metric-label">Total Actions</div>
782
+ <div class="metric-icon">\u26A1</div>
783
+ </div>
784
+ <div class="metric-value" id="total-actions">0</div>
785
+ <div class="metric-change positive" id="actions-change">\u2191 0% this session</div>
786
+ </div>
787
+ <div class="metric-card">
788
+ <div class="metric-header">
789
+ <div class="metric-label">Bugs Found</div>
790
+ <div class="metric-icon">\u{1F41B}</div>
791
+ </div>
792
+ <div class="metric-value" id="bugs-found">0</div>
793
+ <div class="metric-change negative" id="bugs-change">\u2193 0 from yesterday</div>
794
+ </div>
795
+ <div class="metric-card">
796
+ <div class="metric-header">
797
+ <div class="metric-label">Success Rate</div>
798
+ <div class="metric-icon">\u2713</div>
799
+ </div>
800
+ <div class="metric-value" id="success-rate">\u2014</div>
801
+ <div class="metric-change positive" id="rate-change">\u2191 0 pts improvement</div>
802
+ </div>
803
+ </div>
804
+
805
+ <!-- Charts & Hierarchy -->
806
+ <div class="main-grid">
807
+ <div class="panel">
808
+ <div class="panel-head">
809
+ <span class="panel-title">Performance Metrics</span>
810
+ <div class="tabs">
811
+ <button class="tab active" onclick="switchChartTab('performance')">Performance</button>
812
+ <button class="tab" onclick="switchChartTab('activity')">Activity</button>
813
+ <button class="tab" onclick="switchChartTab('errors')">Error rate</button>
814
+ </div>
815
+ </div>
816
+ <div class="panel-body">
817
+ <div class="chart-container" id="chart-performance">
818
+ <canvas id="performanceChart"></canvas>
819
+ </div>
820
+ <div class="chart-container" id="chart-activity" style="display:none;">
821
+ <canvas id="activityChart"></canvas>
822
+ </div>
823
+ <div class="chart-container" id="chart-errors" style="display:none;">
824
+ <canvas id="errorChart"></canvas>
825
+ </div>
826
+ </div>
827
+ </div>
828
+
829
+ <div class="panel">
830
+ <div class="panel-head">
831
+ <span class="panel-title">Agent Hierarchy</span>
832
+ <span class="panel-badge">live</span>
833
+ </div>
834
+ <div class="panel-body">
835
+ <div class="hierarchy-container" id="hierarchy-container">
836
+ <div class="hierarchy-badge">\u25CF Main Agent</div>
837
+ <!-- SVG hierarchy will be rendered here -->
838
+ </div>
839
+ </div>
840
+ </div>
841
+ </div>
842
+
843
+ <!-- Agents & Activity -->
844
+ <div class="bottom-grid">
845
+ <div class="panel">
846
+ <div class="panel-head">
847
+ <span class="panel-title">Active Agents</span>
848
+ <div class="tabs">
849
+ <button class="tab active" onclick="switchAgentTab('agents')">Agents</button>
850
+ <button class="tab" onclick="switchAgentTab('specialists')">Specialists</button>
851
+ </div>
852
+ </div>
853
+ <div id="agents-table">
854
+ <div class="table-header">
855
+ <div>Agent</div>
856
+ <div>Status</div>
857
+ <div>Tasks</div>
858
+ <div>Perf.</div>
859
+ </div>
860
+ <div id="agents-list">
861
+ <div class="empty-state">Waiting for agent data...</div>
862
+ </div>
863
+ </div>
864
+ </div>
865
+
866
+ <div class="panel">
867
+ <div class="panel-head">
868
+ <span class="panel-title">Recent Activity</span>
869
+ <span class="panel-badge" id="activity-count">0 events</span>
870
+ </div>
871
+ <div class="activity-list" id="activity-list">
872
+ <div class="activity-item">
873
+ <div class="activity-dot info"></div>
874
+ <div class="activity-content">
875
+ <div class="activity-message">Awaiting session start</div>
876
+ <div class="activity-time">System ready \xB7 just now</div>
877
+ </div>
878
+ </div>
879
+ </div>
880
+ </div>
881
+ </div>
882
+
883
+ <!-- Tasks, Issues & Interventions -->
884
+ <div class="triple-grid">
885
+ <div class="panel" id="tasks-panel">
886
+ <div class="panel-head">
887
+ <span class="panel-title">\u{1F4DD} Current Tasks</span>
888
+ <span class="panel-badge" id="tasks-count">0</span>
889
+ </div>
890
+ <div class="task-list" id="tasks-list">
891
+ <div class="empty-state">No active tasks</div>
892
+ </div>
893
+ </div>
894
+
895
+ <div class="panel" id="issues-panel">
896
+ <div class="panel-head">
897
+ <span class="panel-title">\u26A0\uFE0F Issues Found</span>
898
+ <span class="panel-badge" id="issues-count">0</span>
899
+ </div>
900
+ <div class="issue-list" id="issues-list">
901
+ <div class="empty-state">No issues detected</div>
902
+ </div>
903
+ </div>
904
+
905
+ <div class="panel" id="interventions-panel">
906
+ <div class="panel-head">
907
+ <span class="panel-title">\u{1F6A8} Human Interventions</span>
908
+ <span class="panel-badge intervention-badge" id="interventions-count">0</span>
909
+ </div>
910
+ <div class="intervention-list" id="interventions-list">
911
+ <div class="empty-state">No interventions required</div>
912
+ </div>
913
+ </div>
914
+ </div>
915
+
916
+ <!-- Session Info Footer -->
917
+ <div class="session-footer">
918
+ <div class="session-info">
919
+ <div class="session-item">
920
+ <span class="session-label">Session ID</span>
921
+ <span class="session-value" id="session-id">\u2014</span>
922
+ </div>
923
+ <div class="session-item">
924
+ <span class="session-label">Target URL</span>
925
+ <span class="session-value" id="target-url">Not configured</span>
926
+ </div>
927
+ <div class="session-item">
928
+ <span class="session-label">Status</span>
929
+ <span class="session-value">
930
+ <span class="status-indicator" id="agent-status-indicator">\u25CF</span>
931
+ <span id="agent-status-text">Idle</span>
932
+ </span>
933
+ </div>
934
+ </div>
935
+ <div class="session-actions">
936
+ <button class="btn-sm btn-danger" onclick="stopSession()" id="stop-btn" disabled>\u25FC Stop</button>
937
+ <button class="btn-sm btn-ghost" onclick="pauseSession()" id="pause-btn" disabled>\u23F8 Pause</button>
938
+ </div>
939
+ </div>
940
+ </div>
941
+ </main>
942
+ </div>
943
+
944
+ <script>
945
+ let ws;
946
+ let activities = [];
947
+ let performanceChart, activityChart, errorChart;
948
+ let chartData = { actions: [], successRates: [], labels: [] };
949
+
950
+ // WebSocket Connection
951
+ function connectWebSocket() {
952
+ const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
953
+ ws = new WebSocket(\`\${protocol}//\${window.location.host}\`);
954
+
955
+ ws.onopen = () => {
956
+ document.getElementById('connection-dot').classList.remove('disconnected');
957
+ document.getElementById('connection-text').textContent = 'Connected';
958
+ };
959
+
960
+ ws.onclose = () => {
961
+ document.getElementById('connection-dot').classList.add('disconnected');
962
+ document.getElementById('connection-text').textContent = 'Disconnected';
963
+ setTimeout(connectWebSocket, 3000);
964
+ };
965
+
966
+ ws.onmessage = (event) => {
967
+ const data = JSON.parse(event.data);
968
+ handleMessage(data);
969
+ };
970
+ }
971
+
972
+ function handleMessage(data) {
973
+ switch(data.type) {
974
+ case 'status':
975
+ updateStatus(data.data);
976
+ break;
977
+ case 'session':
978
+ updateMetrics(data.data);
979
+ break;
980
+ case 'agents':
981
+ updateAgents(data.data);
982
+ break;
983
+ case 'activity':
984
+ addActivity(data.data);
985
+ break;
986
+ case 'tasks':
987
+ updateTasks(data.data);
988
+ break;
989
+ case 'issues':
990
+ updateIssues(data.data);
991
+ break;
992
+ case 'intervention':
993
+ addIntervention(data.data);
994
+ break;
995
+ }
996
+ }
997
+
998
+ function updateStatus(status) {
999
+ const isRunning = status.isRunning;
1000
+ const indicator = document.getElementById('agent-status-indicator');
1001
+ const statusText = document.getElementById('agent-status-text');
1002
+ const stopBtn = document.getElementById('stop-btn');
1003
+ const pauseBtn = document.getElementById('pause-btn');
1004
+
1005
+ // Update sidebar connection text
1006
+ document.getElementById('connection-text').textContent = isRunning ? 'Running' : 'Connected';
1007
+
1008
+ // Update session footer status
1009
+ indicator.className = 'status-indicator ' + (isRunning ? 'running' : 'idle');
1010
+ statusText.textContent = isRunning ? 'Running' : 'Idle';
1011
+
1012
+ // Update target URL
1013
+ if (status.target) {
1014
+ document.getElementById('target-url').textContent = status.target;
1015
+ }
1016
+
1017
+ // Update session ID
1018
+ if (status.sessionId) {
1019
+ document.getElementById('session-id').textContent = status.sessionId.substring(0, 12) + '...';
1020
+ }
1021
+
1022
+ // Enable/disable control buttons
1023
+ stopBtn.disabled = !isRunning;
1024
+ pauseBtn.disabled = !isRunning;
1025
+ }
1026
+
1027
+ function updateMetrics(session) {
1028
+ document.getElementById('active-agents').textContent = session.active_agents || 0;
1029
+ document.getElementById('total-actions').textContent = session.total_actions || 0;
1030
+ document.getElementById('bugs-found').textContent = session.bugs_found || 0;
1031
+
1032
+ const rate = session.success_rate;
1033
+ document.getElementById('success-rate').textContent = rate > 0 ? rate + '%' : '\u2014';
1034
+
1035
+ // Update chart data
1036
+ const now = new Date();
1037
+ const timeLabel = now.getHours().toString().padStart(2, '0') + ':' + now.getMinutes().toString().padStart(2, '0');
1038
+
1039
+ if (chartData.labels.length > 7) {
1040
+ chartData.labels.shift();
1041
+ chartData.actions.shift();
1042
+ chartData.successRates.shift();
1043
+ }
1044
+
1045
+ chartData.labels.push(timeLabel);
1046
+ chartData.actions.push(session.total_actions || 0);
1047
+ chartData.successRates.push(rate || 0);
1048
+
1049
+ if (performanceChart) {
1050
+ performanceChart.data.labels = chartData.labels;
1051
+ performanceChart.data.datasets[0].data = chartData.actions;
1052
+ performanceChart.data.datasets[1].data = chartData.successRates;
1053
+ performanceChart.update('none');
1054
+ }
1055
+ }
1056
+
1057
+ function updateAgents(agents) {
1058
+ const container = document.getElementById('agents-list');
1059
+ const countEl = document.getElementById('active-agents');
1060
+
1061
+ if (!agents || agents.length === 0) {
1062
+ container.innerHTML = '<div class="empty-state">Waiting for agent data...</div>';
1063
+ countEl.textContent = '0';
1064
+ return;
1065
+ }
1066
+
1067
+ countEl.textContent = agents.length;
1068
+
1069
+ container.innerHTML = agents.map(agent => \`
1070
+ <div class="table-row">
1071
+ <div class="agent-name">\${agent.name}</div>
1072
+ <div><span class="status-badge \${agent.status}">\${agent.status}</span></div>
1073
+ <div>\${agent.tasks || 0}</div>
1074
+ <div>\${agent.performance || 0}%</div>
1075
+ </div>
1076
+ \`).join('');
1077
+
1078
+ // Update hierarchy
1079
+ updateHierarchy(agents);
1080
+ }
1081
+
1082
+ function addActivity(activity) {
1083
+ activities.unshift(activity);
1084
+ if (activities.length > 20) activities.pop();
1085
+
1086
+ const container = document.getElementById('activity-list');
1087
+ document.getElementById('activity-count').textContent = activities.length + ' events';
1088
+
1089
+ container.innerHTML = activities.map(a => \`
1090
+ <div class="activity-item">
1091
+ <div class="activity-dot \${a.type || 'info'}"></div>
1092
+ <div class="activity-content">
1093
+ <div class="activity-message">\${a.message}</div>
1094
+ <div class="activity-time">\${formatTime(a.timestamp)}</div>
1095
+ </div>
1096
+ </div>
1097
+ \`).join('');
1098
+ }
1099
+
1100
+ function formatTime(timestamp) {
1101
+ if (!timestamp) return 'just now';
1102
+ const date = new Date(timestamp);
1103
+ const now = new Date();
1104
+ const diff = Math.floor((now - date) / 1000);
1105
+
1106
+ if (diff < 60) return 'just now';
1107
+ if (diff < 3600) return Math.floor(diff / 60) + ' min ago';
1108
+ return date.toLocaleTimeString();
1109
+ }
1110
+
1111
+ // Charts
1112
+ function initCharts() {
1113
+ const chartOptions = {
1114
+ responsive: true,
1115
+ maintainAspectRatio: false,
1116
+ plugins: {
1117
+ legend: {
1118
+ display: true,
1119
+ position: 'top',
1120
+ labels: { color: '#8b98a8', font: { size: 11 } }
1121
+ }
1122
+ },
1123
+ scales: {
1124
+ x: {
1125
+ ticks: { color: '#4b5563' },
1126
+ grid: { color: 'rgba(255,255,255,0.04)' }
1127
+ },
1128
+ y: {
1129
+ ticks: { color: '#4b5563' },
1130
+ grid: { color: 'rgba(255,255,255,0.04)' }
1131
+ }
1132
+ }
1133
+ };
1134
+
1135
+ // Performance Chart
1136
+ const perfCtx = document.getElementById('performanceChart').getContext('2d');
1137
+ performanceChart = new Chart(perfCtx, {
1138
+ type: 'line',
1139
+ data: {
1140
+ labels: chartData.labels.length ? chartData.labels : ['\u2014'],
1141
+ datasets: [{
1142
+ label: 'Actions/min',
1143
+ data: chartData.actions.length ? chartData.actions : [0],
1144
+ borderColor: '#f97316',
1145
+ backgroundColor: 'rgba(249, 115, 22, 0.1)',
1146
+ tension: 0.4,
1147
+ fill: true
1148
+ }, {
1149
+ label: 'Success %',
1150
+ data: chartData.successRates.length ? chartData.successRates : [0],
1151
+ borderColor: '#22c55e',
1152
+ backgroundColor: 'rgba(34, 197, 94, 0.1)',
1153
+ tension: 0.4,
1154
+ fill: true
1155
+ }]
1156
+ },
1157
+ options: chartOptions
1158
+ });
1159
+
1160
+ // Activity Chart
1161
+ const actCtx = document.getElementById('activityChart').getContext('2d');
1162
+ activityChart = new Chart(actCtx, {
1163
+ type: 'bar',
1164
+ data: {
1165
+ labels: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
1166
+ datasets: [{
1167
+ label: 'Tests',
1168
+ data: [0, 0, 0, 0, 0, 0, 0],
1169
+ backgroundColor: '#f97316'
1170
+ }, {
1171
+ label: 'Bugs',
1172
+ data: [0, 0, 0, 0, 0, 0, 0],
1173
+ backgroundColor: '#ef4444'
1174
+ }]
1175
+ },
1176
+ options: chartOptions
1177
+ });
1178
+
1179
+ // Error Chart
1180
+ const errCtx = document.getElementById('errorChart').getContext('2d');
1181
+ errorChart = new Chart(errCtx, {
1182
+ type: 'doughnut',
1183
+ data: {
1184
+ labels: ['Success', 'Warnings', 'Errors'],
1185
+ datasets: [{
1186
+ data: [100, 0, 0],
1187
+ backgroundColor: ['#22c55e', '#f59e0b', '#ef4444']
1188
+ }]
1189
+ },
1190
+ options: {
1191
+ responsive: true,
1192
+ maintainAspectRatio: false,
1193
+ plugins: {
1194
+ legend: {
1195
+ position: 'right',
1196
+ labels: { color: '#8b98a8', font: { size: 11 } }
1197
+ }
1198
+ }
1199
+ }
1200
+ });
1201
+ }
1202
+
1203
+ function switchChartTab(tab) {
1204
+ // Update tabs in the chart panel
1205
+ const chartTabs = document.querySelectorAll('#chart-performance, #chart-activity, #chart-errors')
1206
+ .item(0)?.closest('.panel')?.querySelectorAll('.tabs .tab');
1207
+
1208
+ if (chartTabs) {
1209
+ chartTabs.forEach((t, i) => {
1210
+ t.classList.remove('active');
1211
+ if ((tab === 'performance' && i === 0) ||
1212
+ (tab === 'activity' && i === 1) ||
1213
+ (tab === 'errors' && i === 2)) {
1214
+ t.classList.add('active');
1215
+ }
1216
+ });
1217
+ }
1218
+
1219
+ document.getElementById('chart-performance').style.display = tab === 'performance' ? 'block' : 'none';
1220
+ document.getElementById('chart-activity').style.display = tab === 'activity' ? 'block' : 'none';
1221
+ document.getElementById('chart-errors').style.display = tab === 'errors' ? 'block' : 'none';
1222
+ }
1223
+
1224
+ function switchAgentTab(tab) {
1225
+ const agentsTabs = document.querySelectorAll('#agents-table .tabs .tab');
1226
+ agentsTabs.forEach(t => t.classList.remove('active'));
1227
+
1228
+ if (tab === 'specialists') {
1229
+ agentsTabs[1]?.classList.add('active');
1230
+ // Show specialists data
1231
+ const container = document.getElementById('agents-list');
1232
+ container.innerHTML = \`
1233
+ <div class="table-row">
1234
+ <div class="agent-name">Browser Specialist</div>
1235
+ <div><span class="status-badge idle">idle</span></div>
1236
+ <div>0</div>
1237
+ <div>\u2014</div>
1238
+ </div>
1239
+ <div class="table-row">
1240
+ <div class="agent-name">API Tester</div>
1241
+ <div><span class="status-badge idle">idle</span></div>
1242
+ <div>0</div>
1243
+ <div>\u2014</div>
1244
+ </div>
1245
+ <div class="table-row">
1246
+ <div class="agent-name">Auth Specialist</div>
1247
+ <div><span class="status-badge idle">idle</span></div>
1248
+ <div>0</div>
1249
+ <div>\u2014</div>
1250
+ </div>
1251
+ <div class="table-row">
1252
+ <div class="agent-name">UI Tester</div>
1253
+ <div><span class="status-badge idle">idle</span></div>
1254
+ <div>0</div>
1255
+ <div>\u2014</div>
1256
+ </div>
1257
+ <div class="table-row">
1258
+ <div class="agent-name">Security Scanner</div>
1259
+ <div><span class="status-badge idle">idle</span></div>
1260
+ <div>0</div>
1261
+ <div>\u2014</div>
1262
+ </div>
1263
+ \`;
1264
+ } else {
1265
+ agentsTabs[0]?.classList.add('active');
1266
+ // Reload agents data from correct endpoint
1267
+ fetch('/api/dynamic-agents', { credentials: 'include' })
1268
+ .then(r => r.json())
1269
+ .then(updateAgents)
1270
+ .catch(() => {
1271
+ document.getElementById('agents-list').innerHTML = '<div class="empty-state">Waiting for agent data...</div>';
1272
+ });
1273
+ }
1274
+ }
1275
+
1276
+ function scrollToSection(elementId) {
1277
+ const element = document.getElementById(elementId);
1278
+ if (element) {
1279
+ element.scrollIntoView({ behavior: 'smooth', block: 'start' });
1280
+ // Highlight effect
1281
+ element.style.boxShadow = '0 0 0 2px var(--accent)';
1282
+ setTimeout(() => {
1283
+ element.style.boxShadow = '';
1284
+ }, 2000);
1285
+ }
1286
+ }
1287
+
1288
+ // Hierarchy
1289
+ function updateHierarchy(agents) {
1290
+ const container = document.getElementById('hierarchy-container');
1291
+
1292
+ const agentTypes = {
1293
+ 'Main Agent': { x: 200, y: 30, color: '#f97316', class: 'main' },
1294
+ 'Browser Specialist': { x: 80, y: 120, color: '#22c55e', class: 'browser' },
1295
+ 'API Tester': { x: 200, y: 120, color: '#f59e0b', class: 'api' },
1296
+ 'Auth Specialist': { x: 320, y: 120, color: '#ef4444', class: 'auth' },
1297
+ 'UI Tester': { x: 80, y: 210, color: '#8b5cf6', class: 'ui' },
1298
+ 'Performance': { x: 200, y: 210, color: '#06b6d4', class: 'perf' },
1299
+ 'Security Scanner': { x: 320, y: 210, color: '#ec4899', class: 'security' }
1300
+ };
1301
+
1302
+ let html = '<div class="hierarchy-badge">\u25CF Main Agent</div>';
1303
+ html += '<svg width="100%" height="100%" style="position:absolute;top:0;left:0;">';
1304
+
1305
+ // Draw connections
1306
+ html += '<line x1="240" y1="50" x2="120" y2="120" stroke="#333" stroke-width="2" stroke-dasharray="4"/>';
1307
+ html += '<line x1="240" y1="50" x2="240" y2="120" stroke="#333" stroke-width="2" stroke-dasharray="4"/>';
1308
+ html += '<line x1="240" y1="50" x2="360" y2="120" stroke="#333" stroke-width="2" stroke-dasharray="4"/>';
1309
+ html += '<line x1="120" y1="140" x2="120" y2="210" stroke="#333" stroke-width="2" stroke-dasharray="4"/>';
1310
+ html += '<line x1="240" y1="140" x2="240" y2="210" stroke="#333" stroke-width="2" stroke-dasharray="4"/>';
1311
+ html += '<line x1="360" y1="140" x2="360" y2="210" stroke="#333" stroke-width="2" stroke-dasharray="4"/>';
1312
+
1313
+ html += '</svg>';
1314
+
1315
+ // Draw nodes
1316
+ agents.forEach(agent => {
1317
+ const config = agentTypes[agent.name];
1318
+ if (config) {
1319
+ const isActive = agent.status === 'running';
1320
+ html += \`<div class="agent-node \${config.class}" style="left:\${config.x}px;top:\${config.y}px;opacity:\${isActive ? 1 : 0.5}">
1321
+ \${isActive ? '\u25CF' : '\u25CB'} \${agent.name.split(' ')[0]}
1322
+ </div>\`;
1323
+ }
1324
+ });
1325
+
1326
+ container.innerHTML = html;
1327
+ }
1328
+
1329
+ // Tasks
1330
+ function updateTasks(tasks) {
1331
+ const container = document.getElementById('tasks-list');
1332
+ const countEl = document.getElementById('tasks-count');
1333
+
1334
+ if (!tasks || tasks.length === 0) {
1335
+ container.innerHTML = '<div class="empty-state">No active tasks</div>';
1336
+ countEl.textContent = '0';
1337
+ return;
1338
+ }
1339
+
1340
+ countEl.textContent = tasks.length;
1341
+
1342
+ container.innerHTML = tasks.map(task => {
1343
+ const progressNum = parseInt(task.progress) || 0;
1344
+ return \`
1345
+ <div class="task-item">
1346
+ <div class="task-icon">\${task.status === 'running' ? '\u26A1' : task.status === 'completed' ? '\u2713' : '\u25CB'}</div>
1347
+ <div class="task-content">
1348
+ <div class="task-name">\${task.name}</div>
1349
+ <div class="task-meta">
1350
+ <span>\${task.agent}</span>
1351
+ <span>\${formatTime(task.started_at)}</span>
1352
+ </div>
1353
+ </div>
1354
+ <div class="task-progress">
1355
+ <div class="task-progress-fill" style="width: \${progressNum}%"></div>
1356
+ </div>
1357
+ </div>
1358
+ \`;
1359
+ }).join('');
1360
+ }
1361
+
1362
+ // Issues
1363
+ function updateIssues(issues) {
1364
+ const container = document.getElementById('issues-list');
1365
+ const countEl = document.getElementById('issues-count');
1366
+
1367
+ if (!issues || issues.length === 0) {
1368
+ container.innerHTML = '<div class="empty-state">No issues detected</div>';
1369
+ countEl.textContent = '0';
1370
+ return;
1371
+ }
1372
+
1373
+ countEl.textContent = issues.length;
1374
+
1375
+ container.innerHTML = issues.map(issue => \`
1376
+ <div class="issue-item">
1377
+ <div class="issue-icon \${issue.severity}">\${
1378
+ issue.severity === 'critical' ? '\u{1F534}' :
1379
+ issue.severity === 'high' ? '\u{1F7E0}' :
1380
+ issue.severity === 'medium' ? '\u{1F7E1}' : '\u{1F7E2}'
1381
+ }</div>
1382
+ <div class="issue-content">
1383
+ <div class="issue-title">\${issue.title}</div>
1384
+ <div class="issue-meta">
1385
+ <span class="severity-badge \${issue.severity}">\${issue.severity}</span>
1386
+ <span>\${issue.agent}</span>
1387
+ </div>
1388
+ </div>
1389
+ </div>
1390
+ \`).join('');
1391
+ }
1392
+
1393
+ // Interventions
1394
+ let interventions = [];
1395
+
1396
+ function addIntervention(intervention) {
1397
+ interventions.unshift(intervention);
1398
+ renderInterventions();
1399
+ }
1400
+
1401
+ function renderInterventions() {
1402
+ const container = document.getElementById('interventions-list');
1403
+ const countEl = document.getElementById('interventions-count');
1404
+ const navCountEl = document.getElementById('intervention-count');
1405
+
1406
+ countEl.textContent = interventions.length;
1407
+ navCountEl.textContent = interventions.length;
1408
+
1409
+ if (interventions.length === 0) {
1410
+ container.innerHTML = '<div class="empty-state">No interventions required</div>';
1411
+ return;
1412
+ }
1413
+
1414
+ container.innerHTML = interventions.map(int => \`
1415
+ <div class="intervention-item" id="intervention-\${int.id}">
1416
+ <div class="intervention-header">
1417
+ <div class="intervention-icon">\u{1F6A8}</div>
1418
+ <div class="intervention-title">\${int.title}</div>
1419
+ </div>
1420
+ <div class="intervention-desc">\${int.description}</div>
1421
+ <div class="intervention-actions">
1422
+ <button class="btn-sm btn-primary" onclick="respondIntervention('\${int.id}', 'approve')">\u2713 Approve</button>
1423
+ <button class="btn-sm btn-danger" onclick="respondIntervention('\${int.id}', 'reject')">\u2715 Reject</button>
1424
+ <button class="btn-sm btn-ghost" onclick="respondIntervention('\${int.id}', 'skip')">Skip</button>
1425
+ </div>
1426
+ </div>
1427
+ \`).join('');
1428
+ }
1429
+
1430
+ async function respondIntervention(id, response) {
1431
+ try {
1432
+ await fetch('/api/brain/run-test/' + id, {
1433
+ method: 'POST',
1434
+ headers: { 'Content-Type': 'application/json' },
1435
+ credentials: 'include',
1436
+ body: JSON.stringify({ response })
1437
+ });
1438
+
1439
+ // Remove from local list
1440
+ interventions = interventions.filter(i => i.id !== id);
1441
+ renderInterventions();
1442
+
1443
+ addActivity({
1444
+ type: response === 'approve' ? 'success' : 'warning',
1445
+ message: \`Intervention \${response}ed: \${id}\`,
1446
+ timestamp: new Date().toISOString()
1447
+ });
1448
+ } catch (error) {
1449
+ addActivity({ type: 'error', message: 'Failed to respond to intervention', timestamp: new Date().toISOString() });
1450
+ }
1451
+ }
1452
+
1453
+ // Actions
1454
+ async function startSession() {
1455
+ try {
1456
+ const response = await fetch('/api/agent/start', { method: 'POST', credentials: 'include' });
1457
+ const result = await response.json();
1458
+ if (result.success) {
1459
+ addActivity({ type: 'success', message: 'Session started', timestamp: new Date().toISOString() });
1460
+ } else {
1461
+ addActivity({ type: 'error', message: result.error || 'Failed to start session', timestamp: new Date().toISOString() });
1462
+ }
1463
+ } catch (error) {
1464
+ addActivity({ type: 'error', message: 'Failed to start session', timestamp: new Date().toISOString() });
1465
+ }
1466
+ }
1467
+
1468
+ async function stopSession() {
1469
+ try {
1470
+ const response = await fetch('/api/agent/stop', { method: 'POST', credentials: 'include' });
1471
+ const result = await response.json();
1472
+ addActivity({
1473
+ type: result.success ? 'warning' : 'error',
1474
+ message: result.success ? 'Session stopped' : (result.error || 'Failed to stop session'),
1475
+ timestamp: new Date().toISOString()
1476
+ });
1477
+ } catch (error) {
1478
+ addActivity({ type: 'error', message: 'Failed to stop session', timestamp: new Date().toISOString() });
1479
+ }
1480
+ }
1481
+
1482
+ async function pauseSession() {
1483
+ addActivity({ type: 'info', message: 'Pause requested...', timestamp: new Date().toISOString() });
1484
+ }
1485
+
1486
+ // Load initial data
1487
+ async function loadInitialData() {
1488
+ try {
1489
+ const creds = { credentials: 'include' };
1490
+ const [sessionsRes, bugsRes, tasksRes, issuesRes, statusRes, agentsRes, metricsRes, sessionsHistRes] = await Promise.all([
1491
+ fetch('/api/sessions?limit=1', creds),
1492
+ fetch('/api/bugs', creds),
1493
+ fetch('/api/tasks', creds),
1494
+ fetch('/api/issues', creds),
1495
+ fetch('/api/status', creds),
1496
+ fetch('/api/dynamic-agents', creds),
1497
+ fetch('/api/metrics', creds),
1498
+ fetch('/api/sessions?limit=7', creds),
1499
+ ]);
1500
+
1501
+ const sessions = await sessionsRes.json();
1502
+ const bugs = await bugsRes.json();
1503
+ const tasks = await tasksRes.json();
1504
+ const issues = await issuesRes.json();
1505
+ const status = await statusRes.json();
1506
+ const agents = await agentsRes.json();
1507
+ const metrics = await metricsRes.json();
1508
+ const sessionsHistory = await sessionsHistRes.json();
1509
+
1510
+ // Update status indicator
1511
+ updateStatus(status);
1512
+
1513
+ // Update metrics cards
1514
+ if (sessions.length > 0) {
1515
+ const session = sessions[0];
1516
+ updateMetrics({
1517
+ active_agents: agents.filter(a => a.status === 'running').length,
1518
+ total_actions: session.total_actions || 0,
1519
+ bugs_found: session.bugs_found || 0,
1520
+ success_rate: session.total_actions > 0
1521
+ ? Math.round(((session.total_actions - (session.bugs_found || 0)) / session.total_actions) * 100)
1522
+ : 0
1523
+ });
1524
+ if (session.id) {
1525
+ document.getElementById('session-id').textContent = session.id.substring(0, 12) + '...';
1526
+ }
1527
+ }
1528
+
1529
+ // Update agent list and hierarchy
1530
+ if (agents && agents.length > 0) {
1531
+ updateAgents(agents);
1532
+ }
1533
+
1534
+ // Populate performance chart from session history (last 7 sessions)
1535
+ if (sessionsHistory.length > 0) {
1536
+ const historicSessions = [...sessionsHistory].reverse();
1537
+ chartData.labels = historicSessions.map((s, i) => {
1538
+ const d = new Date(s.started_at);
1539
+ return d.getHours().toString().padStart(2,'0') + ':' + d.getMinutes().toString().padStart(2,'0');
1540
+ });
1541
+ chartData.actions = historicSessions.map(s => s.total_actions || 0);
1542
+ chartData.successRates = historicSessions.map(s =>
1543
+ s.total_actions > 0
1544
+ ? Math.round(((s.total_actions - (s.bugs_found || 0)) / s.total_actions) * 100)
1545
+ : 0
1546
+ );
1547
+ if (performanceChart) {
1548
+ performanceChart.data.labels = chartData.labels;
1549
+ performanceChart.data.datasets[0].data = chartData.actions;
1550
+ performanceChart.data.datasets[1].data = chartData.successRates;
1551
+ performanceChart.update('none');
1552
+ }
1553
+
1554
+ // Weekly activity chart \u2014 group sessions by day of week
1555
+ const days = ['Sun','Mon','Tue','Wed','Thu','Fri','Sat'];
1556
+ const testsByDay = [0,0,0,0,0,0,0];
1557
+ const bugsByDay = [0,0,0,0,0,0,0];
1558
+ sessionsHistory.forEach(s => {
1559
+ const day = new Date(s.started_at).getDay();
1560
+ testsByDay[day] += s.total_actions || 0;
1561
+ bugsByDay[day] += s.bugs_found || 0;
1562
+ });
1563
+ if (activityChart) {
1564
+ activityChart.data.labels = days;
1565
+ activityChart.data.datasets[0].data = testsByDay;
1566
+ activityChart.data.datasets[1].data = bugsByDay;
1567
+ activityChart.update('none');
1568
+ }
1569
+ }
1570
+
1571
+ // Error/success donut from server-side metrics counters
1572
+ if (metrics && metrics.counters) {
1573
+ const passed = metrics.counters.tests_passed || 0;
1574
+ const failed = metrics.counters.tests_failed || 0;
1575
+ const total = passed + failed;
1576
+ if (total > 0 && errorChart) {
1577
+ errorChart.data.datasets[0].data = [passed, 0, failed];
1578
+ errorChart.update('none');
1579
+ }
1580
+ }
1581
+
1582
+ // Update tasks and issues
1583
+ updateTasks(tasks);
1584
+ updateIssues(issues);
1585
+
1586
+ // Kanban badge = open bugs
1587
+ const openBugs = bugs.filter(b => b.status === 'open' || b.status === 'in-progress');
1588
+ document.getElementById('kanban-count').textContent = openBugs.length || 0;
1589
+
1590
+ // Seed activity feed with recent actions from latest session
1591
+ if (sessions.length > 0) {
1592
+ try {
1593
+ const actionsRes = await fetch(\`/api/sessions/\${sessions[0].id}/actions\`, creds);
1594
+ const actions = await actionsRes.json();
1595
+ actions.slice(0, 10).forEach(a => {
1596
+ addActivity({ type: 'info', message: a.description || a.type, timestamp: a.timestamp });
1597
+ });
1598
+ } catch (_) { /* no actions yet */ }
1599
+ }
1600
+
1601
+ } catch (error) {
1602
+ console.error('Failed to load initial data:', error);
1603
+ }
1604
+ }
1605
+
1606
+ // Initialize
1607
+ window.addEventListener('load', () => {
1608
+ initCharts();
1609
+ connectWebSocket();
1610
+ loadInitialData();
1611
+ });
1612
+ </script>
1613
+
1614
+ </body>
1615
+ </html>`;
1616
+ }
1617
+ export {
1618
+ getDashboardHTML
1619
+ };